From 74f95ac3381a643fd13c5de62cca208c79fa7902 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 28 Apr 2021 15:24:56 -0400 Subject: [PATCH 001/852] Add switch platform to goalzero (#48612) * Add switch platform to goalzero * fix update interval * Apply some suggested changes * pass device class to parent * Drop passing device_class * Tweaks * Drop underscore prefix --- .coveragerc | 1 + homeassistant/components/goalzero/__init__.py | 18 ++++- .../components/goalzero/binary_sensor.py | 9 ++- homeassistant/components/goalzero/const.py | 10 ++- .../components/goalzero/manifest.json | 2 +- .../components/goalzero/strings.json | 2 +- homeassistant/components/goalzero/switch.py | 71 +++++++++++++++++++ .../components/goalzero/translations/en.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/goalzero/switch.py diff --git a/.coveragerc b/.coveragerc index 05a752764c3..181c551e02a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -358,6 +358,7 @@ omit = homeassistant/components/goalfeed/* homeassistant/components/goalzero/__init__.py homeassistant/components/goalzero/binary_sensor.py + homeassistant/components/goalzero/switch.py homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 34e57eeeac9..b0883d42a5f 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -3,6 +3,8 @@ import logging from goalzero import Yeti, exceptions +from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR +from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant @@ -19,7 +21,7 @@ from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN, MIN_TIME_BETWEEN_ _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor"] +PLATFORMS = [DOMAIN_BINARY_SENSOR, DOMAIN_SWITCH] async def async_setup_entry(hass, entry): @@ -30,7 +32,7 @@ async def async_setup_entry(hass, entry): session = async_get_clientsession(hass) api = Yeti(host, hass.loop, session) try: - await api.get_state() + await api.init_connect() except exceptions.ConnectError as ex: _LOGGER.warning("Failed to connect: %s", ex) raise ConfigEntryNotReady from ex @@ -82,10 +84,20 @@ class YetiEntity(CoordinatorEntity): @property def device_info(self): """Return the device information of the entity.""" + if self.api.data: + sw_version = self.api.data["firmwareVersion"] + else: + sw_version = None + if self.api.sysdata: + model = self.api.sysdata["model"] + else: + model = model or None return { "identifiers": {(DOMAIN, self._server_unique_id)}, - "name": self._name, "manufacturer": "Goal Zero", + "model": model, + "name": self._name, + "sw_version": sw_version, } @property diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index a2af8a18546..59a8a6b3443 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -28,7 +28,14 @@ async def async_setup_entry(hass, entry, async_add_entities): class YetiBinarySensor(YetiEntity, BinarySensorEntity): """Representation of a Goal Zero Yeti sensor.""" - def __init__(self, api, coordinator, name, sensor_name, server_unique_id): + def __init__( + self, + api, + coordinator, + name, + sensor_name, + server_unique_id, + ): """Initialize a Goal Zero Yeti sensor.""" super().__init__(api, coordinator, name, server_unique_id) diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index 3afa1e537c1..826c2621e23 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -15,9 +15,6 @@ DATA_KEY_API = "api" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) BINARY_SENSOR_DICT = { - "v12PortStatus": ["12V Port Status", DEVICE_CLASS_POWER, None], - "usbPortStatus": ["USB Port Status", DEVICE_CLASS_POWER, None], - "acPortStatus": ["AC Port Status", DEVICE_CLASS_POWER, None], "backlight": ["Backlight", None, "mdi:clock-digital"], "app_online": [ "App Online", @@ -25,4 +22,11 @@ BINARY_SENSOR_DICT = { None, ], "isCharging": ["Charging", DEVICE_CLASS_BATTERY_CHARGING, None], + "inputDetected": ["Input Detected", DEVICE_CLASS_POWER, None], +} + +SWITCH_DICT = { + "v12PortStatus": "12V Port Status", + "usbPortStatus": "USB Port Status", + "acPortStatus": "AC Port Status", } diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json index 405fbaf7342..0a1bc4df70d 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -3,7 +3,7 @@ "name": "Goal Zero Yeti", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/goalzero", - "requirements": ["goalzero==0.1.4"], + "requirements": ["goalzero==0.1.7"], "codeowners": ["@tkdrob"], "iot_class": "local_polling" } diff --git a/homeassistant/components/goalzero/strings.json b/homeassistant/components/goalzero/strings.json index bd59cd5e7f5..92813337e77 100644 --- a/homeassistant/components/goalzero/strings.json +++ b/homeassistant/components/goalzero/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Goal Zero Yeti", - "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host IP from your router. DHCP must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.", + "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. DHCP reservation must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.", "data": { "host": "[%key:common::config_flow::data::host%]", "name": "[%key:common::config_flow::data::name%]" diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py new file mode 100644 index 00000000000..dd4c9deae3e --- /dev/null +++ b/homeassistant/components/goalzero/switch.py @@ -0,0 +1,71 @@ +"""Support for Goal Zero Yeti Switches.""" +from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_NAME + +from . import YetiEntity +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN, SWITCH_DICT + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Goal Zero Yeti switch.""" + name = entry.data[CONF_NAME] + goalzero_data = hass.data[DOMAIN][entry.entry_id] + switches = [ + YetiSwitch( + goalzero_data[DATA_KEY_API], + goalzero_data[DATA_KEY_COORDINATOR], + name, + switch_name, + entry.entry_id, + ) + for switch_name in SWITCH_DICT + ] + async_add_entities(switches) + + +class YetiSwitch(YetiEntity, SwitchEntity): + """Representation of a Goal Zero Yeti switch.""" + + def __init__( + self, + api, + coordinator, + name, + switch_name, + server_unique_id, + ): + """Initialize a Goal Zero Yeti switch.""" + super().__init__(api, coordinator, name, server_unique_id) + + self._condition = switch_name + + self._condition_name = SWITCH_DICT[switch_name] + + @property + def name(self): + """Return the name of the switch.""" + return f"{self._name} {self._condition_name}" + + @property + def unique_id(self): + """Return the unique id of the switch.""" + return f"{self._server_unique_id}/{self._condition}" + + @property + def is_on(self): + """Return state of the switch.""" + if self.api.data: + return self.api.data[self._condition] + return None + + async def async_turn_off(self, **kwargs): + """Turn off the switch.""" + payload = {self._condition: 0} + await self.api.post_state(payload=payload) + self.coordinator.async_set_updated_data(data=payload) + + async def async_turn_on(self, **kwargs): + """Turn on the switch.""" + payload = {self._condition: 1} + await self.api.post_state(payload=payload) + self.coordinator.async_set_updated_data(data=payload) diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json index 25aa32e4b75..e6c6e4a7298 100644 --- a/homeassistant/components/goalzero/translations/en.json +++ b/homeassistant/components/goalzero/translations/en.json @@ -14,7 +14,7 @@ "host": "Host", "name": "Name" }, - "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host IP from your router. DHCP must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.", + "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. DHCP reservation must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.", "title": "Goal Zero Yeti" } } diff --git a/requirements_all.txt b/requirements_all.txt index c93f3745100..c6883125d99 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -669,7 +669,7 @@ glances_api==0.2.0 gntp==1.0.3 # homeassistant.components.goalzero -goalzero==0.1.4 +goalzero==0.1.7 # homeassistant.components.gogogate2 gogogate2-api==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04f1f90b03c..de06e914303 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -366,7 +366,7 @@ gios==0.2.1 glances_api==0.2.0 # homeassistant.components.goalzero -goalzero==0.1.4 +goalzero==0.1.7 # homeassistant.components.gogogate2 gogogate2-api==3.0.0 From d749015b96b18e02daa5cd9d98f43775d94e4eef Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 28 Apr 2021 13:34:19 -0600 Subject: [PATCH 002/852] Bump pyairvisual to 5.0.8 (#49823) --- homeassistant/components/airvisual/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index db77716bf41..b94218f6c13 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,7 +3,7 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==5.0.4"], + "requirements": ["pyairvisual==5.0.8"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index c6883125d99..f2965757080 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1274,7 +1274,7 @@ pyaftership==0.1.2 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==5.0.4 +pyairvisual==5.0.8 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de06e914303..c567134a4d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ pyaehw4a1==0.3.9 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==5.0.4 +pyairvisual==5.0.8 # homeassistant.components.almond pyalmond==0.0.2 From ff137fe1865b25d1b18db00d82e512417d3d841b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 28 Apr 2021 22:18:00 +0200 Subject: [PATCH 003/852] Add service target to Neato (#49803) Co-authored-by: Franck Nijhof --- homeassistant/components/neato/services.yaml | 33 ++++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/neato/services.yaml b/homeassistant/components/neato/services.yaml index 2c5b2bd3181..eb0c7bffba9 100644 --- a/homeassistant/components/neato/services.yaml +++ b/homeassistant/components/neato/services.yaml @@ -1,18 +1,45 @@ custom_cleaning: + name: Zone Cleaning service description: Zone Cleaning service call specific to Neato Botvacs. + target: + entity: + integration: neato + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. [Required] - example: "vacuum.neato" mode: + name: Set cleaning mode description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." + default: 2 example: 2 + selector: + number: + min: 1 + max: 2 + mode: box navigation: + name: Set navigation mode description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." + default: 1 example: 1 + selector: + number: + min: 1 + max: 3 + mode: box category: + name: Use cleaning map description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." + default: 4 example: 2 + selector: + number: + min: 2 + max: 4 + step: 2 + mode: box zone: + name: Name of the zone to clean (Only Botvac D7) description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. example: "Kitchen" + selector: + text: From 21872c42fef91e0404299ebbe7deaa9cd7eb86ed Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 28 Apr 2021 22:31:40 +0200 Subject: [PATCH 004/852] Fix color setting in LIFX services (#49822) --- homeassistant/components/lifx/light.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 9f1c5747aa8..e366b810a94 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -200,6 +200,12 @@ def find_hsbk(hass, **kwargs): if ATTR_HS_COLOR in kwargs: hue, saturation = kwargs[ATTR_HS_COLOR] + elif ATTR_RGB_COLOR in kwargs: + hue, saturation = color_util.color_RGB_to_hs(*kwargs[ATTR_RGB_COLOR]) + elif ATTR_XY_COLOR in kwargs: + hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) + + if hue is not None: hue = int(hue / 360 * 65535) saturation = int(saturation / 100 * 65535) kelvin = 3500 From 105504cb893998c55586dcede3675efbe0764933 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 28 Apr 2021 16:43:07 -0400 Subject: [PATCH 005/852] Set ClimaCell API limit to 500 requests/day (#49828) --- homeassistant/components/climacell/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 2c1646afc70..977a5089783 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -42,7 +42,7 @@ DEFAULT_FORECAST_TYPE = DAILY DOMAIN = "climacell" ATTRIBUTION = "Powered by ClimaCell" -MAX_REQUESTS_PER_DAY = 1000 +MAX_REQUESTS_PER_DAY = 500 CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} From 14af6d3884b5d4e319463c59a2633af57f5d6acc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Apr 2021 22:43:40 +0200 Subject: [PATCH 006/852] Remove DHT from Raspberry Pi machine builds (#49829) --- machine/raspberrypi | 15 --------------- machine/raspberrypi2 | 15 --------------- machine/raspberrypi3 | 15 --------------- machine/raspberrypi3-64 | 15 --------------- machine/raspberrypi4 | 15 --------------- machine/raspberrypi4-64 | 15 --------------- script/gen_requirements_all.py | 1 - 7 files changed, 91 deletions(-) diff --git a/machine/raspberrypi b/machine/raspberrypi index d7add9bf63f..c9271aceccb 100644 --- a/machine/raspberrypi +++ b/machine/raspberrypi @@ -44,18 +44,3 @@ RUN apk add --no-cache \ && apk del .build-dependencies \ && rm -rf /usr/src/libcec ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} - -## -# Install DHT -RUN apk add --no-cache --virtual .build-dependencies \ - gcc libc-dev raspberrypi-dev \ - && export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \ - && git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \ - && cd /usr/src/dht \ - && sed -i 's/^pi_version\ =\ None/pi_version\ =\ 1/' setup.py \ - && sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \ - && sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \ - && export MAKEFLAGS="-j$(nproc)" \ - && pip3 install . \ - && apk del .build-dependencies \ - && rm -rf /usr/src/dht diff --git a/machine/raspberrypi2 b/machine/raspberrypi2 index 2643af911a4..d6c01b4ae02 100644 --- a/machine/raspberrypi2 +++ b/machine/raspberrypi2 @@ -44,18 +44,3 @@ RUN apk add --no-cache \ && apk del .build-dependencies \ && rm -rf /usr/src/libcec ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} - -## -# Install DHT -RUN apk add --no-cache --virtual .build-dependencies \ - gcc libc-dev raspberrypi-dev \ - && export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \ - && git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \ - && cd /usr/src/dht \ - && sed -i 's/^pi_version\ =\ None/pi_version\ =\ 2/' setup.py \ - && sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \ - && sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \ - && export MAKEFLAGS="-j$(nproc)" \ - && pip3 install . \ - && apk del .build-dependencies \ - && rm -rf /usr/src/dht diff --git a/machine/raspberrypi3 b/machine/raspberrypi3 index 5aed2308ef6..4509e150584 100644 --- a/machine/raspberrypi3 +++ b/machine/raspberrypi3 @@ -44,18 +44,3 @@ RUN apk add --no-cache \ && apk del .build-dependencies \ && rm -rf /usr/src/libcec ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} - -## -# Install DHT -RUN apk add --no-cache --virtual .build-dependencies \ - gcc libc-dev raspberrypi-dev \ - && export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \ - && git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \ - && cd /usr/src/dht \ - && sed -i 's/^pi_version\ =\ None/pi_version\ =\ 3/' setup.py \ - && sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \ - && sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \ - && export MAKEFLAGS="-j$(nproc)" \ - && pip3 install . \ - && apk del .build-dependencies \ - && rm -rf /usr/src/dht diff --git a/machine/raspberrypi3-64 b/machine/raspberrypi3-64 index 1b31726c879..97064a2377d 100644 --- a/machine/raspberrypi3-64 +++ b/machine/raspberrypi3-64 @@ -44,18 +44,3 @@ RUN apk add --no-cache \ && apk del .build-dependencies \ && rm -rf /usr/src/libcec ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} - -## -# Install DHT -RUN apk add --no-cache --virtual .build-dependencies \ - gcc libc-dev raspberrypi-dev \ - && export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \ - && git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \ - && cd /usr/src/dht \ - && sed -i 's/^pi_version\ =\ None/pi_version\ =\ 3/' setup.py \ - && sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \ - && sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \ - && export MAKEFLAGS="-j$(nproc)" \ - && pip3 install . \ - && apk del .build-dependencies \ - && rm -rf /usr/src/dht diff --git a/machine/raspberrypi4 b/machine/raspberrypi4 index 5aed2308ef6..4509e150584 100644 --- a/machine/raspberrypi4 +++ b/machine/raspberrypi4 @@ -44,18 +44,3 @@ RUN apk add --no-cache \ && apk del .build-dependencies \ && rm -rf /usr/src/libcec ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} - -## -# Install DHT -RUN apk add --no-cache --virtual .build-dependencies \ - gcc libc-dev raspberrypi-dev \ - && export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \ - && git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \ - && cd /usr/src/dht \ - && sed -i 's/^pi_version\ =\ None/pi_version\ =\ 3/' setup.py \ - && sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \ - && sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \ - && export MAKEFLAGS="-j$(nproc)" \ - && pip3 install . \ - && apk del .build-dependencies \ - && rm -rf /usr/src/dht diff --git a/machine/raspberrypi4-64 b/machine/raspberrypi4-64 index 1b31726c879..97064a2377d 100644 --- a/machine/raspberrypi4-64 +++ b/machine/raspberrypi4-64 @@ -44,18 +44,3 @@ RUN apk add --no-cache \ && apk del .build-dependencies \ && rm -rf /usr/src/libcec ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} - -## -# Install DHT -RUN apk add --no-cache --virtual .build-dependencies \ - gcc libc-dev raspberrypi-dev \ - && export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \ - && git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \ - && cd /usr/src/dht \ - && sed -i 's/^pi_version\ =\ None/pi_version\ =\ 3/' setup.py \ - && sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \ - && sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \ - && export MAKEFLAGS="-j$(nproc)" \ - && pip3 install . \ - && apk del .build-dependencies \ - && rm -rf /usr/src/dht diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index af06542d744..23d13ee9de9 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -13,7 +13,6 @@ from script.hassfest.model import Integration COMMENT_REQUIREMENTS = ( "Adafruit_BBIO", - "Adafruit-DHT", "avea", # depends on bluepy "avion", "beacontools", From 16e096de0c90b9148cedb5a443c93c441009ad2e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Apr 2021 23:48:48 +0200 Subject: [PATCH 007/852] Bump version to 2021.6.0dev0 (#49830) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d05a7c03f4..cb0e435c8d1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 -MINOR_VERSION = 5 +MINOR_VERSION = 6 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 27816aa4d55737c7a3ff64f470d66b398581915a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 29 Apr 2021 00:03:34 +0000 Subject: [PATCH 008/852] [ci skip] Translation update --- .../components/denonavr/translations/es.json | 1 + .../devolo_home_control/translations/es.json | 7 +++ .../components/fritz/translations/es.json | 44 +++++++++++++++++++ .../components/fritz/translations/et.json | 2 +- .../components/goalzero/translations/et.json | 2 +- .../components/motioneye/translations/es.json | 25 +++++++++++ .../components/mutesync/translations/ca.json | 16 +++++++ .../components/mutesync/translations/es.json | 16 +++++++ .../components/mutesync/translations/et.json | 16 +++++++ .../components/mutesync/translations/it.json | 16 +++++++ .../components/mutesync/translations/nl.json | 16 +++++++ .../components/mutesync/translations/no.json | 16 +++++++ .../components/mutesync/translations/ru.json | 16 +++++++ .../mutesync/translations/zh-Hant.json | 16 +++++++ .../components/picnic/translations/es.json | 22 ++++++++++ .../components/smarttub/translations/es.json | 4 ++ 16 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/fritz/translations/es.json create mode 100644 homeassistant/components/motioneye/translations/es.json create mode 100644 homeassistant/components/mutesync/translations/ca.json create mode 100644 homeassistant/components/mutesync/translations/es.json create mode 100644 homeassistant/components/mutesync/translations/et.json create mode 100644 homeassistant/components/mutesync/translations/it.json create mode 100644 homeassistant/components/mutesync/translations/nl.json create mode 100644 homeassistant/components/mutesync/translations/no.json create mode 100644 homeassistant/components/mutesync/translations/ru.json create mode 100644 homeassistant/components/mutesync/translations/zh-Hant.json create mode 100644 homeassistant/components/picnic/translations/es.json diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index 83478ff42a1..785f364b1a7 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Mostrar todas las fuentes", + "update_audyssey": "Actualizar la configuraci\u00f3n de Audyssey", "zone2": "Configurar la Zona 2", "zone3": "Configurar la Zona 3" }, diff --git a/homeassistant/components/devolo_home_control/translations/es.json b/homeassistant/components/devolo_home_control/translations/es.json index ef3b2ae0d6d..713f5a53d73 100644 --- a/homeassistant/components/devolo_home_control/translations/es.json +++ b/homeassistant/components/devolo_home_control/translations/es.json @@ -14,6 +14,13 @@ "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico / ID de devolo" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico / ID de devolo" + } } } } diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json new file mode 100644 index 00000000000..db9b2fa5c2a --- /dev/null +++ b/homeassistant/components/fritz/translations/es.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "connection_error": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Descubierto FRITZ!Box: {nombre}\n\nConfigurar FRITZ!Box Tools para controlar tu {nombre}", + "title": "Configurar FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Actualizar credenciales de FRITZ!Box Tools para: {host}.\n\n FRITZ!Box Tools no puede iniciar sesi\u00f3n en tu FRITZ!Box.", + "title": "Actualizando FRITZ!Box Tools - credenciales" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Usuario" + }, + "description": "Configurar FRITZ!Box Tools para controlar tu FRITZ!Box.\nM\u00ednimo necesario: usuario, contrase\u00f1a.", + "title": "Configurar FRITZ!Box Tools - obligatorio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json index e996efd435b..1ab5b27ffd7 100644 --- a/homeassistant/components/fritz/translations/et.json +++ b/homeassistant/components/fritz/translations/et.json @@ -11,7 +11,7 @@ "connection_error": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus" }, - "flow_title": "FRITZ!Box t\u00f6\u00f6riistad: {nimi}", + "flow_title": "FRITZ!Box t\u00f6\u00f6riistad: {name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/goalzero/translations/et.json b/homeassistant/components/goalzero/translations/et.json index 74f84f1d72b..5bc1af97297 100644 --- a/homeassistant/components/goalzero/translations/et.json +++ b/homeassistant/components/goalzero/translations/et.json @@ -14,7 +14,7 @@ "host": "", "name": "Nimi" }, - "description": "Alustuseks pead alla laadima rakenduse Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\n Yeti Wifi-v\u00f5rguga \u00fchendamiseks j\u00e4rgi juhiseid. Seej\u00e4rel hangi oma ruuterilt host IP. DHCP peab olema ruuteri seadetes seadistatud, et tagada, et host-IP ei muutuks. Vaata ruuteri kasutusjuhendit.", + "description": "Alustuseks pead alla laadima rakenduse Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nYeti Wifi-v\u00f5rguga \u00fchendamiseks j\u00e4rgi juhiseid. DHCP peab olema ruuteri seadetes seadistatud nii, et hosti IP ei muutuks. Vaata ruuteri kasutusjuhendit.", "title": "" } } diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json new file mode 100644 index 00000000000..4f749d5c6d8 --- /dev/null +++ b/homeassistant/components/motioneye/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_url": "URL no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "admin_password": "Contrase\u00f1a administrador", + "admin_username": "Usuario administrador", + "surveillance_password": "Contrase\u00f1a vigilancia", + "surveillance_username": "Usuario vigilancia", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/ca.json b/homeassistant/components/mutesync/translations/ca.json new file mode 100644 index 00000000000..c97e9814abb --- /dev/null +++ b/homeassistant/components/mutesync/translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Activa l'autenticaci\u00f3 a Prefer\u00e8ncies de m\u00fctesync > Autenticaci\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/es.json b/homeassistant/components/mutesync/translations/es.json new file mode 100644 index 00000000000..fb32193010e --- /dev/null +++ b/homeassistant/components/mutesync/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Activar la autenticaci\u00f3n en las Preferencias de m\u00fctesync > Autenticaci\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/et.json b/homeassistant/components/mutesync/translations/et.json new file mode 100644 index 00000000000..5f4e2c8739e --- /dev/null +++ b/homeassistant/components/mutesync/translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Luba tuvastamine jaotises m\u00fctesync Preferences > Authentication", + "unknown": "Tundmatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/it.json b/homeassistant/components/mutesync/translations/it.json new file mode 100644 index 00000000000..c1d52c2be26 --- /dev/null +++ b/homeassistant/components/mutesync/translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Abilita l'autenticazione in m\u00fctesync Preferenze > Autenticazione", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/nl.json b/homeassistant/components/mutesync/translations/nl.json new file mode 100644 index 00000000000..1b3dc36f659 --- /dev/null +++ b/homeassistant/components/mutesync/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Activeer authenticatie in m\u00fctesync Voorkeuren > Authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/no.json b/homeassistant/components/mutesync/translations/no.json new file mode 100644 index 00000000000..14e4738567e --- /dev/null +++ b/homeassistant/components/mutesync/translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Aktiver autentisering i m\u00fctesync-innstillinger > Autentisering", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/ru.json b/homeassistant/components/mutesync/translations/ru.json new file mode 100644 index 00000000000..99164a766b6 --- /dev/null +++ b/homeassistant/components/mutesync/translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e m\u00fctesync \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/zh-Hant.json b/homeassistant/components/mutesync/translations/zh-Hant.json new file mode 100644 index 00000000000..c274757f78f --- /dev/null +++ b/homeassistant/components/mutesync/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u65bc m\u00fctesync \u7cfb\u7d71\u504f\u597d\u8a2d\u5b9a > \u8a8d\u8b49\u4e2d\u958b\u555f\u8a8d\u8b49", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json new file mode 100644 index 00000000000..848f72e62d6 --- /dev/null +++ b/homeassistant/components/picnic/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "country_code": "C\u00f3digo del pa\u00eds", + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/es.json b/homeassistant/components/smarttub/translations/es.json index f7c225b02a1..3454bf59837 100644 --- a/homeassistant/components/smarttub/translations/es.json +++ b/homeassistant/components/smarttub/translations/es.json @@ -9,6 +9,10 @@ "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "description": "La integraci\u00f3n de SmartTub necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "email": "Correo electr\u00f3nico", From 1c0fd6107503a73dd2fc3353a8a8758b15499aa8 Mon Sep 17 00:00:00 2001 From: Mike Keesey Date: Wed, 28 Apr 2021 22:27:57 -0600 Subject: [PATCH 009/852] Remove references to hass.data in harmony tests (#49836) Instead, just use the mocks directly. --- tests/components/harmony/test_remote.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/components/harmony/test_remote.py b/tests/components/harmony/test_remote.py index 4222244f00d..df75485e30d 100644 --- a/tests/components/harmony/test_remote.py +++ b/tests/components/harmony/test_remote.py @@ -6,7 +6,6 @@ from aioharmony.const import SendCommandDevice from homeassistant.components.harmony.const import ( DOMAIN, - HARMONY_DATA, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, ) @@ -150,7 +149,7 @@ async def test_remote_toggles(mock_hc, hass, mock_write_config): assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) -async def test_async_send_command(mock_hc, hass, mock_write_config): +async def test_async_send_command(mock_hc, harmony_client, hass, mock_write_config): """Ensure calls to send remote commands properly propagate to devices.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} @@ -160,8 +159,7 @@ async def test_async_send_command(mock_hc, hass, mock_write_config): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - send_commands_mock = data._client.send_commands + send_commands_mock = harmony_client.send_commands # No device provided await _send_commands_and_wait( @@ -283,7 +281,9 @@ async def test_async_send_command(mock_hc, hass, mock_write_config): send_commands_mock.reset_mock() -async def test_async_send_command_custom_delay(mock_hc, hass, mock_write_config): +async def test_async_send_command_custom_delay( + mock_hc, harmony_client, hass, mock_write_config +): """Ensure calls to send remote commands properly propagate to devices with custom delays.""" entry = MockConfigEntry( domain=DOMAIN, @@ -298,8 +298,7 @@ async def test_async_send_command_custom_delay(mock_hc, hass, mock_write_config) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - send_commands_mock = data._client.send_commands + send_commands_mock = harmony_client.send_commands # Tell the TV to play by id await _send_commands_and_wait( @@ -324,7 +323,7 @@ async def test_async_send_command_custom_delay(mock_hc, hass, mock_write_config) send_commands_mock.reset_mock() -async def test_change_channel(mock_hc, hass, mock_write_config): +async def test_change_channel(mock_hc, harmony_client, hass, mock_write_config): """Test change channel commands.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} @@ -334,8 +333,7 @@ async def test_change_channel(mock_hc, hass, mock_write_config): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - change_channel_mock = data._client.change_channel + change_channel_mock = harmony_client.change_channel # Tell the remote to change channels await hass.services.async_call( @@ -349,7 +347,7 @@ async def test_change_channel(mock_hc, hass, mock_write_config): change_channel_mock.assert_awaited_once_with(100) -async def test_sync(mock_hc, mock_write_config, hass): +async def test_sync(mock_hc, harmony_client, mock_write_config, hass): """Test the sync command.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} @@ -359,8 +357,7 @@ async def test_sync(mock_hc, mock_write_config, hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - sync_mock = data._client.sync + sync_mock = harmony_client.sync # Tell the remote to change channels await hass.services.async_call( From a0bf95d4b51a581d44e5c1e48037c60945eae10b Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Thu, 29 Apr 2021 05:29:53 +0100 Subject: [PATCH 010/852] Validate if modules in mypy config exist (#49810) --- script/hassfest/mypy_config.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 45fa1eb6539..c42d26ad73e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -3,6 +3,8 @@ from __future__ import annotations import configparser import io +import os +from pathlib import Path from typing import Final from .model import Config, Integration @@ -321,6 +323,22 @@ def generate_and_validate(config: Config) -> str: if module in ignored_modules_set: config.add_error("mypy_config", f"Module '{module}' is in ignored list") + # Validate that all modules exist. + all_modules = strict_modules + IGNORED_MODULES + for module in all_modules: + if module.endswith(".*"): + module_path = Path(module[:-2].replace(".", os.path.sep)) + if not module_path.is_dir(): + config.add_error("mypy_config", f"Module '{module} is not a folder") + else: + module = module.replace(".", os.path.sep) + module_path = Path(f"{module}.py") + if module_path.is_file(): + continue + module_path = Path(module) / "__init__.py" + if not module_path.is_file(): + config.add_error("mypy_config", f"Module '{module} doesn't exist") + mypy_config = configparser.ConfigParser() general_section = "mypy" From 5008c27e7a6f94365180f5e3c991ac7eb31b615d Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Thu, 29 Apr 2021 05:31:08 +0100 Subject: [PATCH 011/852] Relax type annotation for DataUpdateCoordinator data (#49827) --- homeassistant/helpers/update_coordinator.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index f9b97698220..09844640ce7 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -52,7 +52,12 @@ class DataUpdateCoordinator(Generic[T]): self.update_method = update_method self.update_interval = update_interval - self.data: T | None = None + # It's None before the first successful update. + # Components should call async_config_entry_first_refresh + # to make sure the first update was successful. + # Set type to just T to remove annoying checks that data is not None + # when it was already checked during setup. + self.data: T = None # type: ignore[assignment] self._listeners: list[CALLBACK_TYPE] = [] self._job = HassJob(self._handle_refresh_interval) @@ -133,7 +138,7 @@ class DataUpdateCoordinator(Generic[T]): """ await self._debounced_refresh.async_call() - async def _async_update_data(self) -> T | None: + async def _async_update_data(self) -> T: """Fetch the latest data from the source.""" if self.update_method is None: raise NotImplementedError("Update method not implemented") From 25d257b63154bec445e0f43c55782581085d5969 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 29 Apr 2021 08:39:03 +0200 Subject: [PATCH 012/852] Upgrade py-cpuinfo to 8.0.0 (#49833) --- homeassistant/components/cpuspeed/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cpuspeed/manifest.json b/homeassistant/components/cpuspeed/manifest.json index 19973b4e8d2..99c93a6d610 100644 --- a/homeassistant/components/cpuspeed/manifest.json +++ b/homeassistant/components/cpuspeed/manifest.json @@ -2,7 +2,7 @@ "domain": "cpuspeed", "name": "CPU Speed", "documentation": "https://www.home-assistant.io/integrations/cpuspeed", - "requirements": ["py-cpuinfo==7.0.0"], + "requirements": ["py-cpuinfo==8.0.0"], "codeowners": ["@fabaff"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index f2965757080..7affb85e105 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1210,7 +1210,7 @@ pwmled==1.6.7 py-canary==0.5.1 # homeassistant.components.cpuspeed -py-cpuinfo==7.0.0 +py-cpuinfo==8.0.0 # homeassistant.components.melissa py-melissa-climate==2.1.4 From bf57c77d5c0bc145d970d28f4e5b0624d6735cc1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 29 Apr 2021 10:45:17 +0200 Subject: [PATCH 013/852] Add color_mode to demo light (#49694) * Update demo light with color mode support * Add rgbw and rgbww color properties * Update demo light * Tweak * Remove unneeded _clear_colors --- homeassistant/components/demo/light.py | 142 ++++++++++++------ tests/components/demo/test_light.py | 3 - tests/components/emulated_hue/test_hue_api.py | 2 + tests/components/google_assistant/__init__.py | 22 +++ 4 files changed, 118 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 3e949138b67..6680cd23874 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -1,4 +1,6 @@ """Demo light platform that implements lights.""" +from __future__ import annotations + import random from homeassistant.components.light import ( @@ -6,12 +8,13 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, SUPPORT_EFFECT, - SUPPORT_WHITE_VALUE, LightEntity, ) @@ -23,9 +26,7 @@ LIGHT_EFFECT_LIST = ["rainbow", "none"] LIGHT_TEMPS = [240, 380] -SUPPORT_DEMO = ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_WHITE_VALUE -) +SUPPORT_DEMO = {COLOR_MODE_HS, COLOR_MODE_COLOR_TEMP} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -33,27 +34,43 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities( [ DemoLight( - unique_id="light_1", - name="Bed Light", - state=False, available=True, effect_list=LIGHT_EFFECT_LIST, effect=LIGHT_EFFECT_LIST[0], + name="Bed Light", + state=False, + unique_id="light_1", ), DemoLight( - unique_id="light_2", - name="Ceiling Lights", - state=True, available=True, ct=LIGHT_TEMPS[1], + name="Ceiling Lights", + state=True, + unique_id="light_2", ), DemoLight( - unique_id="light_3", - name="Kitchen Lights", - state=True, available=True, hs_color=LIGHT_COLORS[1], - ct=LIGHT_TEMPS[0], + name="Kitchen Lights", + state=True, + unique_id="light_3", + ), + DemoLight( + available=True, + ct=LIGHT_TEMPS[1], + name="Office RGBW Lights", + rgbw_color=(255, 0, 0, 255), + state=True, + supported_color_modes={COLOR_MODE_RGBW}, + unique_id="light_4", + ), + DemoLight( + available=True, + name="Living Room RGBWW Lights", + rgbww_color=(255, 0, 0, 255, 0), + state=True, + supported_color_modes={COLOR_MODE_RGBWW}, + unique_id="light_5", ), ] ) @@ -73,26 +90,39 @@ class DemoLight(LightEntity): name, state, available=False, - hs_color=None, - ct=None, brightness=180, - white=200, + ct=None, effect_list=None, effect=None, + hs_color=None, + rgbw_color=None, + rgbww_color=None, + supported_color_modes=None, ): """Initialize the light.""" - self._unique_id = unique_id - self._name = name - self._state = state - self._hs_color = hs_color - self._ct = ct or random.choice(LIGHT_TEMPS) - self._brightness = brightness - self._white = white - self._features = SUPPORT_DEMO - self._effect_list = effect_list - self._effect = effect self._available = True - self._color_mode = "ct" if ct is not None and hs_color is None else "hs" + self._brightness = brightness + self._ct = ct or random.choice(LIGHT_TEMPS) + self._effect = effect + self._effect_list = effect_list + self._features = 0 + self._hs_color = hs_color + self._name = name + self._rgbw_color = rgbw_color + self._rgbww_color = rgbww_color + self._state = state + self._unique_id = unique_id + if hs_color: + self._color_mode = COLOR_MODE_HS + elif rgbw_color: + self._color_mode = COLOR_MODE_RGBW + elif rgbww_color: + self._color_mode = COLOR_MODE_RGBWW + else: + self._color_mode = COLOR_MODE_COLOR_TEMP + if not supported_color_modes: + supported_color_modes = SUPPORT_DEMO + self._color_modes = supported_color_modes if self._effect_list is not None: self._features |= SUPPORT_EFFECT @@ -134,24 +164,30 @@ class DemoLight(LightEntity): """Return the brightness of this light between 0..255.""" return self._brightness + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + return self._color_mode + @property def hs_color(self) -> tuple: """Return the hs color value.""" - if self._color_mode == "hs": - return self._hs_color - return None + return self._hs_color + + @property + def rgbw_color(self) -> tuple: + """Return the rgbw color value.""" + return self._rgbw_color + + @property + def rgbww_color(self) -> tuple: + """Return the rgbww color value.""" + return self._rgbww_color @property def color_temp(self) -> int: """Return the CT color temperature.""" - if self._color_mode == "ct": - return self._ct - return None - - @property - def white_value(self) -> int: - """Return the white value of this light between 0..255.""" - return self._white + return self._ct @property def effect_list(self) -> list: @@ -173,24 +209,34 @@ class DemoLight(LightEntity): """Flag supported features.""" return self._features + @property + def supported_color_modes(self) -> set | None: + """Flag supported color modes.""" + return self._color_modes + async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" self._state = True + if ATTR_RGBW_COLOR in kwargs: + self._color_mode = COLOR_MODE_RGBW + self._rgbw_color = kwargs[ATTR_RGBW_COLOR] + + if ATTR_RGBWW_COLOR in kwargs: + self._color_mode = COLOR_MODE_RGBWW + self._rgbww_color = kwargs[ATTR_RGBWW_COLOR] + if ATTR_HS_COLOR in kwargs: - self._color_mode = "hs" + self._color_mode = COLOR_MODE_HS self._hs_color = kwargs[ATTR_HS_COLOR] if ATTR_COLOR_TEMP in kwargs: - self._color_mode = "ct" + self._color_mode = COLOR_MODE_COLOR_TEMP self._ct = kwargs[ATTR_COLOR_TEMP] if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - if ATTR_WHITE_VALUE in kwargs: - self._white = kwargs[ATTR_WHITE_VALUE] - if ATTR_EFFECT in kwargs: self._effect = kwargs[ATTR_EFFECT] diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index 4e7f58811d9..7633cbe5ccf 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -11,7 +11,6 @@ from homeassistant.components.light import ( ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_RGB_COLOR, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -54,13 +53,11 @@ async def test_state_attributes(hass): { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_RGB_COLOR: (251, 253, 255), - ATTR_WHITE_VALUE: 254, }, blocking=True, ) state = hass.states.get(ENTITY_LIGHT) - assert state.attributes.get(ATTR_WHITE_VALUE) == 254 assert state.attributes.get(ATTR_RGB_COLOR) == (250, 252, 255) assert state.attributes.get(ATTR_XY_COLOR) == (0.319, 0.326) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index c0adad38c9d..cb0b1f39365 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -91,6 +91,8 @@ ENTITY_IDS_BY_NUMBER = { "22": "scene.light_on", "23": "scene.light_off", "24": "media_player.kitchen", + "25": "light.office_rgbw_lights", + "26": "light.living_room_rgbww_lights", } ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()} diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 0fe89d0fa7b..123ca120243 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -380,4 +380,26 @@ DEMO_DEVICES = [ "type": "action.devices.types.SECURITYSYSTEM", "willReportState": False, }, + { + "id": "light.living_room_rgbww_lights", + "name": {"name": "Living Room RGBWW Lights"}, + "traits": [ + "action.devices.traits.OnOff", + "action.devices.traits.Brightness", + "action.devices.traits.ColorSetting", + ], + "type": "action.devices.types.LIGHT", + "willReportState": False, + }, + { + "id": "light.office_rgbw_lights", + "name": {"name": "Office RGBW Lights"}, + "traits": [ + "action.devices.traits.OnOff", + "action.devices.traits.Brightness", + "action.devices.traits.ColorSetting", + ], + "type": "action.devices.types.LIGHT", + "willReportState": False, + }, ] From b7184b669fbfad3a3d7246514e6d2b979e75194c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 29 Apr 2021 11:11:23 +0200 Subject: [PATCH 014/852] Add onboarded key to analytics WS command (#49751) --- homeassistant/components/analytics/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index c1187af7f17..d41970a79de 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_call_later, async_track_time_interval from .analytics import Analytics -from .const import ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA +from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA async def async_setup(hass: HomeAssistant, _): @@ -46,7 +46,7 @@ async def websocket_analytics( analytics: Analytics = hass.data[DOMAIN] connection.send_result( msg["id"], - {ATTR_PREFERENCES: analytics.preferences}, + {ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded}, ) From 52f3a7249f604b0130047a336083e880d47eb0ab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Apr 2021 11:43:23 +0200 Subject: [PATCH 015/852] hassfest detect built-in domain override for custom integrations (#49845) --- script/hassfest/manifest.py | 18 ++++++++++++++---- script/hassfest/model.py | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 8b3489facf6..016e3a0a322 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -1,6 +1,7 @@ """Manifest validation.""" from __future__ import annotations +from pathlib import Path from urllib.parse import urlparse import voluptuous as vol @@ -8,7 +9,7 @@ from voluptuous.humanize import humanize_error from homeassistant.loader import validate_custom_integration_version -from .model import Integration +from .model import Config, Integration DOCUMENTATION_URL_SCHEMA = "https" DOCUMENTATION_URL_HOST = "www.home-assistant.io" @@ -227,7 +228,7 @@ def validate_version(integration: Integration): return -def validate_manifest(integration: Integration): +def validate_manifest(integration: Integration, core_components_dir: Path) -> None: """Validate manifest.""" if not integration.manifest: return @@ -245,6 +246,14 @@ def validate_manifest(integration: Integration): if integration.manifest["domain"] != integration.path.name: integration.add_error("manifest", "Domain does not match dir name") + if ( + not integration.core + and (core_components_dir / integration.manifest["domain"]).exists() + ): + integration.add_warning( + "manifest", "Domain collides with built-in core integration" + ) + if ( integration.manifest["domain"] in NO_IOT_CLASS and "iot_class" in integration.manifest @@ -261,7 +270,8 @@ def validate_manifest(integration: Integration): validate_version(integration) -def validate(integrations: dict[str, Integration], config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle all integrations manifests.""" + core_components_dir = config.root / "homeassistant/components" for integration in integrations.values(): - validate_manifest(integration) + validate_manifest(integration, core_components_dir) diff --git a/script/hassfest/model.py b/script/hassfest/model.py index eee25df079d..10bc10626a2 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -100,7 +100,7 @@ class Integration: """Add an error.""" self.errors.append(Error(*args, **kwargs)) - def add_warning(self, *args, **kwargs): + def add_warning(self, *args: Any, **kwargs: Any) -> None: """Add an warning.""" self.warnings.append(Error(*args, **kwargs)) From de6c9e67b193de715baf63124981f4d5c42df47f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Apr 2021 11:50:29 +0200 Subject: [PATCH 016/852] Upgrade black to 21.4b2 (#49841) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 20792593114..ffe394683ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 21.4b0 + rev: 21.4b2 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 5d646cc81f3..b5dac41af36 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.4b0 +black==21.4b2 codespell==2.0.0 flake8-comprehensions==3.4.0 flake8-docstrings==1.6.0 From 0301706fc631ad1f2cd2532667ba9dfe2f856198 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Thu, 29 Apr 2021 11:28:14 +0100 Subject: [PATCH 017/852] Define AddEntitiesCallback type (#49812) --- homeassistant/components/bsblan/climate.py | 6 +++--- homeassistant/components/directv/media_player.py | 4 ++-- homeassistant/components/directv/remote.py | 5 +++-- homeassistant/components/fritzbox/climate.py | 5 ++--- homeassistant/components/guardian/binary_sensor.py | 5 ++--- homeassistant/components/guardian/sensor.py | 5 ++--- homeassistant/components/guardian/switch.py | 5 ++--- homeassistant/components/hassio/binary_sensor.py | 6 ++---- homeassistant/components/hassio/sensor.py | 6 ++---- homeassistant/components/huawei_lte/binary_sensor.py | 5 +++-- homeassistant/components/huawei_lte/device_tracker.py | 7 ++++--- homeassistant/components/huawei_lte/sensor.py | 3 ++- homeassistant/components/huawei_lte/switch.py | 5 +++-- homeassistant/components/ipp/sensor.py | 6 +++--- homeassistant/components/motioneye/camera.py | 5 +++-- homeassistant/components/mysensors/binary_sensor.py | 7 ++++--- homeassistant/components/mysensors/climate.py | 7 ++++--- homeassistant/components/mysensors/cover.py | 6 ++++-- homeassistant/components/mysensors/light.py | 7 ++++--- homeassistant/components/mysensors/sensor.py | 7 ++++--- homeassistant/components/mysensors/switch.py | 7 ++++--- homeassistant/components/nightscout/sensor.py | 5 ++--- homeassistant/components/nzbget/sensor.py | 5 ++--- homeassistant/components/nzbget/switch.py | 6 ++---- homeassistant/components/plum_lightpad/light.py | 5 ++--- homeassistant/components/recollect_waste/sensor.py | 7 +++---- homeassistant/components/roku/remote.py | 5 ++--- homeassistant/components/solaredge/sensor.py | 7 +++---- homeassistant/components/sonarr/sensor.py | 6 +++--- homeassistant/components/sonos/media_player.py | 5 ++++- homeassistant/components/zha/sensor.py | 7 +++++-- homeassistant/components/zwave_js/binary_sensor.py | 7 +++++-- homeassistant/components/zwave_js/climate.py | 7 +++++-- homeassistant/components/zwave_js/cover.py | 7 +++++-- homeassistant/components/zwave_js/fan.py | 7 +++++-- homeassistant/components/zwave_js/light.py | 7 +++++-- homeassistant/components/zwave_js/lock.py | 7 +++++-- homeassistant/components/zwave_js/number.py | 7 ++++--- homeassistant/components/zwave_js/sensor.py | 7 +++++-- homeassistant/components/zwave_js/switch.py | 7 +++++-- homeassistant/helpers/entity_platform.py | 11 +++++++++++ 41 files changed, 145 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index f55472e105b..5ab63fb4036 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, Callable +from typing import Any from bsblan import BSBLan, BSBLanError, Info, State @@ -27,7 +27,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_IDENTIFIERS, @@ -76,7 +76,7 @@ BSBLAN_TO_HA_PRESET = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up BSBLan device based on a config entry.""" bsblan: BSBLan = hass.data[DOMAIN][entry.entry_id][DATA_BSBLAN_CLIENT] diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 65a120ba2ce..5d7f7d1185b 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Callable from directv import DIRECTV @@ -27,6 +26,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from . import DIRECTVEntity @@ -66,7 +66,7 @@ SUPPORT_DTV_CLIENT = ( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list, bool], None], + async_add_entities: AddEntitiesCallback, ) -> bool: """Set up the DirecTV config entry.""" dtv = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index dc28e287f54..424b5ba4ec6 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -4,13 +4,14 @@ from __future__ import annotations from collections.abc import Iterable from datetime import timedelta import logging -from typing import Any, Callable +from typing import Any from directv import DIRECTV, DIRECTVError from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DIRECTVEntity from .const import DOMAIN @@ -23,7 +24,7 @@ SCAN_INTERVAL = timedelta(minutes=2) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list, bool], None], + async_add_entities: AddEntitiesCallback, ) -> bool: """Load DirecTV remote based on a config entry.""" dtv = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 121c379dc5c..991f57a4269 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -1,6 +1,4 @@ """Support for AVM Fritz!Box smarthome thermostate devices.""" -from typing import Callable - from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, @@ -23,6 +21,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FritzBoxEntity from .const import ( @@ -53,7 +52,7 @@ OFF_REPORT_SET_TEMPERATURE = 0.0 async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Fritzbox smarthome thermostat from ConfigEntry.""" entities = [] diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index e8c736eabe5..869acc094d5 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -1,8 +1,6 @@ """Binary sensors for the Elexa Guardian integration.""" from __future__ import annotations -from typing import Callable - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOISTURE, @@ -12,6 +10,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PairedSensorEntity, ValveControllerEntity @@ -43,7 +42,7 @@ VALVE_CONTROLLER_SENSORS = [SENSOR_KIND_AP_INFO, SENSOR_KIND_LEAK_DETECTED] async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 48807c9cfeb..2d62fe2c613 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -1,8 +1,6 @@ """Sensors for the Elexa Guardian integration.""" from __future__ import annotations -from typing import Callable - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -14,6 +12,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PairedSensorEntity, ValveControllerEntity @@ -48,7 +47,7 @@ VALVE_CONTROLLER_SENSORS = [SENSOR_KIND_TEMPERATURE, SENSOR_KIND_UPTIME] async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index c574f283bdd..ea6888bafbd 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,8 +1,6 @@ """Switches for the Elexa Guardian integration.""" from __future__ import annotations -from typing import Callable - from aioguardian import Client from aioguardian.errors import GuardianError import voluptuous as vol @@ -12,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_FILENAME, CONF_PORT, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import ValveControllerEntity @@ -40,7 +39,7 @@ SERVICE_UPGRADE_FIRMWARE = "upgrade_firmware" async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" platform = entity_platform.current_platform.get() diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index b6faf566807..01930b5ec0e 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -1,12 +1,10 @@ """Binary sensor platform for Hass.io addons.""" from __future__ import annotations -from typing import Callable - from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR from .const import ATTR_UPDATE_AVAILABLE @@ -16,7 +14,7 @@ from .entity import HassioAddonEntity, HassioOSEntity async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Binary sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index c41c0dc5090..e81980d78e1 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -1,12 +1,10 @@ """Sensor platform for Hass.io addons.""" from __future__ import annotations -from typing import Callable - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR from .const import ATTR_VERSION, ATTR_VERSION_LATEST @@ -16,7 +14,7 @@ from .entity import HassioAddonEntity, HassioOSEntity async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 6cb7c8d2ed7..556ed6f5b43 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any import attr from huawei_lte_api.enums.cradle import ConnectionStatusEnum @@ -15,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HuaweiLteBaseEntity from .const import ( @@ -30,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 3a1dcfe83af..61d2bf30fb9 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging import re -from typing import Any, Callable, Dict, List, cast +from typing import Any, Dict, List, cast import attr from stringcase import snakecase @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HuaweiLteBaseEntity, Router from .const import ( @@ -53,7 +54,7 @@ def _get_hosts( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" @@ -130,7 +131,7 @@ def _is_us(host: _HostType) -> bool: def async_add_new_entities( hass: HomeAssistant, router_url: str, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, tracked: set[str], ) -> None: """Add new entities that are not already being tracked.""" diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 5f322e924ec..7396502793e 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import HuaweiLteBaseEntity @@ -356,7 +357,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index d5da6accdb3..ff4109943bc 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any import attr @@ -15,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH @@ -25,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index bce0fb2bbb8..0d6dbdff065 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -2,13 +2,13 @@ from __future__ import annotations from datetime import timedelta -from typing import Any, Callable +from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LOCATION, DEVICE_CLASS_TIMESTAMP, PERCENTAGE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from . import IPPDataUpdateCoordinator, IPPEntity @@ -29,7 +29,7 @@ from .const import ( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up IPP sensor based on a config entry.""" coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index 5f64616e1a4..77ea8d4d5e7 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any import aiohttp from motioneye_client.client import MotionEyeClient @@ -30,6 +30,7 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -58,7 +59,7 @@ PLATFORMS = ["camera"] async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up motionEye from a config entry.""" entry_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 161f5cab8c7..2077f38c758 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,6 +1,4 @@ """Support for MySensors binary sensors.""" -from typing import Callable - from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, @@ -18,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback SENSORS = { "S_DOOR": "door", @@ -32,7 +31,9 @@ SENSORS = { async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index a3104677fa2..f958f2274e0 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -1,6 +1,4 @@ """MySensors platform that offers a Climate (MySensors-HVAC) component.""" -from typing import Callable - from homeassistant.components import mysensors from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -21,6 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback DICT_HA_TO_MYS = { HVAC_MODE_AUTO: "AutoChangeOver", @@ -40,7 +39,9 @@ OPERATION_LIST = [HVAC_MODE_OFF, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT] async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index bade01f42d8..031efc97209 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -1,7 +1,6 @@ """Support for MySensors covers.""" from enum import Enum, unique import logging -from typing import Callable from homeassistant.components import mysensors from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity @@ -11,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback _LOGGER = logging.getLogger(__name__) @@ -26,7 +26,9 @@ class CoverState(Enum): async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 3262487d18e..aea99e3ee35 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -1,6 +1,4 @@ """Support for MySensors lights.""" -from typing import Callable - from homeassistant.components import mysensors from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -18,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from homeassistant.util.color import rgb_hex_to_rgb_list @@ -25,7 +24,9 @@ SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" device_class_map = { diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index a63f143f1d7..48ab6e5d3a2 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,6 +1,4 @@ """Support for MySensors sensors.""" -from typing import Callable - from awesomeversion import AwesomeVersion from homeassistant.components import mysensors @@ -27,6 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback SENSORS = { "V_TEMP": [None, "mdi:thermometer"], @@ -64,7 +63,9 @@ SENSORS = { async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index a410cc64df4..32a6a9a1202 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -1,6 +1,4 @@ """Support for MySensors switches.""" -from typing import Callable - import voluptuous as vol from homeassistant.components import mysensors @@ -8,6 +6,7 @@ from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import on_unload from ...config_entries import ConfigEntry @@ -22,7 +21,9 @@ SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema( async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" device_class_map = { diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index ea2ea549cec..183755298d6 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from asyncio import TimeoutError as AsyncIOTimeoutError from datetime import timedelta import logging -from typing import Callable from aiohttp import ClientError from py_nightscout import Api as NightscoutAPI @@ -13,7 +12,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DATE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, DOMAIN @@ -27,7 +26,7 @@ DEFAULT_NAME = "Blood Glucose" async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Glucose Sensor.""" api = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 6ddac8b977e..49506f72976 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Callable from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -14,7 +13,7 @@ from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from . import NZBGetEntity @@ -44,7 +43,7 @@ SENSOR_TYPES = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up NZBGet sensor based on a config entry.""" coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py index 811f3233bb7..605454246eb 100644 --- a/homeassistant/components/nzbget/switch.py +++ b/homeassistant/components/nzbget/switch.py @@ -1,13 +1,11 @@ """Support for NZBGet switches.""" from __future__ import annotations -from typing import Callable - from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NZBGetEntity from .const import DATA_COORDINATOR, DOMAIN @@ -17,7 +15,7 @@ from .coordinator import NZBGetDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up NZBGet sensor based on a config entry.""" coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 90558eb2523..f358d81dfef 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from typing import Callable from plumlightpad import Plum @@ -16,7 +15,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from .const import DOMAIN @@ -25,7 +24,7 @@ from .const import DOMAIN async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Plum Lightpad dimmer lights and glow rings.""" diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index b95f1d6e8fa..68c810bc90d 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,8 +1,6 @@ """Support for ReCollect Waste sensors.""" from __future__ import annotations -from typing import Callable - from aiorecollect.client import PickupType import voluptuous as vol @@ -16,6 +14,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -57,7 +56,7 @@ def async_get_pickup_type_names( async def async_setup_platform( hass: HomeAssistant, config: dict, - async_add_entities: Callable, + async_add_entities: AddEntitiesCallback, discovery_info: dict = None, ): """Import Recollect Waste configuration from YAML.""" @@ -75,7 +74,7 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up ReCollect Waste sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index a4f35294fd5..7eb8396d6fa 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -1,11 +1,10 @@ """Support for the Roku remote.""" from __future__ import annotations -from typing import Callable - from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler from .const import DOMAIN @@ -14,7 +13,7 @@ from .const import DOMAIN async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list, bool], None], + async_add_entities: AddEntitiesCallback, ) -> bool: """Load Roku remote based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 2195c10cc1d..82b0f427753 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -2,10 +2,9 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Iterable from datetime import date, datetime, timedelta import logging -from typing import Any, Callable +from typing import Any from requests.exceptions import ConnectTimeout, HTTPError from solaredge import Solaredge @@ -16,7 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,7 +38,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Add an solarEdge entry.""" # Add the needed sensors to hass diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index e7ec3e7844c..392c026f49b 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, Callable +from typing import Any from sonarr import Sonarr, SonarrConnectionError, SonarrError @@ -11,7 +11,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_GIGABYTES from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from . import SonarrEntity @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Sonarr sensors based on a config entry.""" options = entry.options diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 73d144f6b0c..6540c4fdb01 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -64,6 +64,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.network import is_internal_request from homeassistant.util.dt import utcnow @@ -146,7 +147,9 @@ UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Sonos from a config entry.""" platform = entity_platform.current_platform.get() diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index d40638ecd71..816db67816a 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import numbers -from typing import Any, Callable +from typing import Any from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, @@ -28,6 +28,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from .core import discovery @@ -72,7 +73,9 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Zigbee Home Automation sensor from config entry.""" entities_to_create = hass.data[DATA_ZHA][DOMAIN] diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index b97975b0507..ad186b69fe4 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Callable, TypedDict +from typing import TypedDict from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass @@ -25,6 +25,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -219,7 +220,9 @@ PROPERTY_SENSOR_MAPPINGS: list[PropertySensorMapping] = [ async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave binary sensor from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 0cad9de8065..b5e60614dfc 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -1,7 +1,7 @@ """Representation of Z-Wave thermostats.""" from __future__ import annotations -from typing import Any, Callable, cast +from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( @@ -53,6 +53,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.temperature import convert_temperature from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN @@ -98,7 +99,9 @@ ATTR_FAN_STATE = "fan_state" async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave climate from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 25c69335ed1..4a73fa2bcab 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import Value as ZwaveValue @@ -18,6 +18,7 @@ from homeassistant.components.cover import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -36,7 +37,9 @@ BARRIER_STATE_OPEN = 255 async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave Cover from Config Entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 100e400f9f7..89b99e90110 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -2,7 +2,7 @@ from __future__ import annotations import math -from typing import Any, Callable +from typing import Any from zwave_js_server.client import Client as ZwaveClient @@ -14,6 +14,7 @@ from homeassistant.components.fan import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( int_states_in_range, percentage_to_ranged_value, @@ -30,7 +31,9 @@ SPEED_RANGE = (1, 99) # off is not included async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave Fan from Config Entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 0b146d7d00b..8dd5afea2a9 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ColorComponent, CommandClass @@ -24,6 +24,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN @@ -45,7 +46,9 @@ MULTI_COLOR_MAP = { async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave Light from Config Entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 0647885345b..437ebf509a5 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient @@ -23,6 +23,7 @@ from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -46,7 +47,9 @@ SERVICE_CLEAR_LOCK_USERCODE = "clear_lock_usercode" async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave lock from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index f418ee3d35b..f427f7fac20 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -1,14 +1,13 @@ """Support for Z-Wave controls using the number platform.""" from __future__ import annotations -from typing import Callable - from zwave_js_server.client import Client as ZwaveClient from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -16,7 +15,9 @@ from .entity import ZWaveBaseEntity async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave Number entity from Config Entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index d1e18763b5b..b23b11f3424 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Callable, cast +from typing import cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass, ConfigurationValueType @@ -25,6 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -34,7 +35,9 @@ LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave sensor from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index e64ea57703d..0be5d1d7f61 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any from zwave_js_server.client import Client as ZwaveClient @@ -10,6 +10,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntit from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -23,7 +24,9 @@ BARRIER_EVENT_SIGNALING_ON = 255 async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave sensor from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index e87960db779..6d7581f6f2b 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -10,6 +10,8 @@ from logging import Logger from types import ModuleType from typing import TYPE_CHECKING, Callable +from typing_extensions import Protocol + from homeassistant import config_entries from homeassistant.const import ( ATTR_RESTORED, @@ -58,6 +60,15 @@ PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds _LOGGER = logging.getLogger(__name__) +class AddEntitiesCallback(Protocol): + """Protocol type for EntityPlatform.add_entities callback.""" + + def __call__( + self, new_entities: Iterable[Entity], update_before_add: bool = False + ) -> None: + """Define add_entities type.""" + + class EntityPlatform: """Manage the entities for a single platform.""" From 8be6605be95d28abe96a335efdd866ed57298e7b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Apr 2021 13:37:55 +0200 Subject: [PATCH 018/852] Remove example entry from PR template (#49842) --- .github/PULL_REQUEST_TEMPLATE.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 05726ab79e5..7c169580cb2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -36,19 +36,6 @@ - [ ] Breaking change (fix/feature causing existing functionality to break) - [ ] Code quality improvements to existing code or addition of tests -## Example entry for `configuration.yaml`: - - -```yaml -# Example configuration.yaml - -``` - ## Additional information ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 7d392ab37c5..a10d59bad4a 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -22,7 +22,6 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self) -> None: """Initialize the velbus config flow.""" diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py index d81528fe0d1..31455468ceb 100644 --- a/homeassistant/components/vesync/config_flow.py +++ b/homeassistant/components/vesync/config_flow.py @@ -21,7 +21,6 @@ class VeSyncFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Instantiate config flow.""" diff --git a/homeassistant/components/vilfo/config_flow.py b/homeassistant/components/vilfo/config_flow.py index 569ce7992fa..9483542f19b 100644 --- a/homeassistant/components/vilfo/config_flow.py +++ b/homeassistant/components/vilfo/config_flow.py @@ -106,7 +106,6 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Vilfo Router.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 55504a753f1..545ab87f47a 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -175,7 +175,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Vizio config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @staticmethod @callback diff --git a/homeassistant/components/volumio/config_flow.py b/homeassistant/components/volumio/config_flow.py index 80ec2f05d91..45c424b356e 100644 --- a/homeassistant/components/volumio/config_flow.py +++ b/homeassistant/components/volumio/config_flow.py @@ -36,7 +36,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Volumio.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize flow.""" diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index 10262e12804..cdd83a35aa1 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -93,7 +93,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Waze Travel Time.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback diff --git a/homeassistant/components/wiffi/config_flow.py b/homeassistant/components/wiffi/config_flow.py index 768f66bf8de..f9456f25df2 100644 --- a/homeassistant/components/wiffi/config_flow.py +++ b/homeassistant/components/wiffi/config_flow.py @@ -18,7 +18,6 @@ class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Wiffi server setup config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH @staticmethod @callback diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index 2706db07871..4bfc331a543 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -4,7 +4,7 @@ from urllib.parse import urlparse import pywilight from homeassistant.components import ssdp -from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST from . import DOMAIN @@ -22,7 +22,6 @@ class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a WiLight config flow.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the WiLight flow.""" diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index f841c61fbcb..59c2d741269 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -6,7 +6,6 @@ import logging import voluptuous as vol from withings_api.common import AuthScope -from homeassistant import config_entries from homeassistant.components.withings import const from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.helpers import config_entry_oauth2_flow @@ -19,7 +18,7 @@ class WithingsFlowHandler( """Handle a config flow.""" DOMAIN = const.DOMAIN - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + # Temporarily holds authorization data during the profile step. _current_data: dict[str, None | str | int] = {} diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index e3f0d8224e6..a06ab3d37ab 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -4,11 +4,7 @@ from __future__ import annotations import voluptuous as vol from wled import WLED, WLEDConnectionError -from homeassistant.config_entries import ( - CONN_CLASS_LOCAL_POLL, - SOURCE_ZEROCONF, - ConfigFlow, -) +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -21,7 +17,6 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a WLED config flow.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" diff --git a/homeassistant/components/wolflink/config_flow.py b/homeassistant/components/wolflink/config_flow.py index 20dbd8ef9b7..491b7a1232d 100644 --- a/homeassistant/components/wolflink/config_flow.py +++ b/homeassistant/components/wolflink/config_flow.py @@ -22,7 +22,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Wolf SmartSet Service.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize with empty username and password.""" diff --git a/homeassistant/components/xbox/config_flow.py b/homeassistant/components/xbox/config_flow.py index 1d1f0cd8465..ef00d1381e5 100644 --- a/homeassistant/components/xbox/config_flow.py +++ b/homeassistant/components/xbox/config_flow.py @@ -1,7 +1,6 @@ """Config flow for xbox.""" import logging -from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN @@ -13,7 +12,6 @@ class OAuth2FlowHandler( """Config flow to handle xbox OAuth2 authentication.""" DOMAIN = DOMAIN - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @property def logger(self) -> logging.Logger: diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index c080aec508d..68c688d3eb1 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -46,7 +46,6 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Xiaomi Aqara config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize.""" diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 9320972abcb..59eee0e6e04 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -36,7 +36,6 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Xiaomi Miio config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize.""" diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 0473cc1042c..98767860fd8 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -28,7 +28,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Yeelight.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @staticmethod @callback diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index a1e161a8132..00aaf7c3625 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -31,7 +31,6 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 2 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize flow instance.""" diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py index b40876bffc3..ce7aebd801a 100644 --- a/homeassistant/components/zwave/config_flow.py +++ b/homeassistant/components/zwave/config_flow.py @@ -18,7 +18,6 @@ class ZwaveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Z-Wave config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the Z-Wave config flow.""" diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 58cb37edcfc..0b54494654b 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -74,7 +74,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Z-Wave JS.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self) -> None: """Set up flow instance.""" From 8eb27374c6827c698551ca6b4907938c004bb6a6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 1 May 2021 09:04:44 +0200 Subject: [PATCH 080/852] Clean up connection classes in integrations P-S (#49893) --- homeassistant/components/panasonic_viera/config_flow.py | 1 - homeassistant/components/philips_js/config_flow.py | 1 - homeassistant/components/pi_hole/config_flow.py | 1 - homeassistant/components/picnic/config_flow.py | 1 - homeassistant/components/plaato/config_flow.py | 1 - homeassistant/components/plex/config_flow.py | 1 - homeassistant/components/plugwise/config_flow.py | 1 - homeassistant/components/point/config_flow.py | 1 - homeassistant/components/poolsense/config_flow.py | 1 - homeassistant/components/powerwall/config_flow.py | 1 - homeassistant/components/profiler/config_flow.py | 1 - homeassistant/components/progettihwsw/config_flow.py | 1 - homeassistant/components/ps4/config_flow.py | 1 - homeassistant/components/pvpc_hourly_pricing/config_flow.py | 1 - homeassistant/components/rachio/config_flow.py | 1 - homeassistant/components/rainmachine/config_flow.py | 1 - homeassistant/components/recollect_waste/config_flow.py | 1 - homeassistant/components/rfxtrx/config_flow.py | 1 - homeassistant/components/ring/config_flow.py | 1 - homeassistant/components/risco/config_flow.py | 1 - homeassistant/components/rituals_perfume_genie/config_flow.py | 1 - homeassistant/components/roku/config_flow.py | 3 +-- homeassistant/components/roomba/config_flow.py | 1 - homeassistant/components/roon/config_flow.py | 1 - homeassistant/components/ruckus_unleashed/config_flow.py | 1 - homeassistant/components/samsungtv/config_flow.py | 1 - homeassistant/components/screenlogic/config_flow.py | 1 - homeassistant/components/sense/config_flow.py | 1 - homeassistant/components/sentry/config_flow.py | 1 - homeassistant/components/sharkiq/config_flow.py | 1 - homeassistant/components/shelly/config_flow.py | 2 +- homeassistant/components/shopping_list/config_flow.py | 1 - homeassistant/components/simplisafe/config_flow.py | 1 - homeassistant/components/sma/config_flow.py | 1 - homeassistant/components/smappee/config_flow.py | 2 -- homeassistant/components/smart_meter_texas/config_flow.py | 1 - homeassistant/components/smarthab/config_flow.py | 1 - homeassistant/components/smartthings/config_flow.py | 1 - homeassistant/components/smarttub/config_flow.py | 1 - homeassistant/components/smhi/config_flow.py | 1 - homeassistant/components/sms/config_flow.py | 1 - homeassistant/components/solaredge/config_flow.py | 1 - homeassistant/components/solarlog/config_flow.py | 1 - homeassistant/components/soma/config_flow.py | 1 - homeassistant/components/somfy/config_flow.py | 1 - homeassistant/components/somfy_mylink/config_flow.py | 1 - homeassistant/components/sonarr/config_flow.py | 3 +-- homeassistant/components/songpal/config_flow.py | 1 - homeassistant/components/speedtestdotnet/config_flow.py | 1 - homeassistant/components/spider/config_flow.py | 1 - homeassistant/components/spotify/config_flow.py | 2 -- homeassistant/components/squeezebox/config_flow.py | 1 - homeassistant/components/srp_energy/config_flow.py | 1 - homeassistant/components/starline/config_flow.py | 1 - homeassistant/components/subaru/config_flow.py | 1 - homeassistant/components/syncthru/config_flow.py | 1 - homeassistant/components/synology_dsm/config_flow.py | 1 - 57 files changed, 3 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 50a030b91da..93c33deb4dc 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -29,7 +29,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Panasonic Viera.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the Panasonic Viera config flow.""" diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 8f0bcd161fc..327fa135e61 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -39,7 +39,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Philips TV.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self) -> None: """Initialize flow.""" diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 60d53c4f904..68f0ecbbb2c 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -33,7 +33,6 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Pi-hole config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the config flow.""" diff --git a/homeassistant/components/picnic/config_flow.py b/homeassistant/components/picnic/config_flow.py index 108325df45a..09a1d524283 100644 --- a/homeassistant/components/picnic/config_flow.py +++ b/homeassistant/components/picnic/config_flow.py @@ -70,7 +70,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Picnic.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 2cb1f4ce326..b1d81db1de5 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -31,7 +31,6 @@ class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handles a Plaato config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize.""" diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 24303dedecd..e18d72337ca 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -82,7 +82,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Plex config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH @staticmethod @callback diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index e17c85a7978..6ad06f3bfd5 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -78,7 +78,6 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Plugwise Smile.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the Plugwise config flow.""" diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index 6e735d69c06..d12274f4f9a 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -44,7 +44,6 @@ class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize flow.""" diff --git a/homeassistant/components/poolsense/config_flow.py b/homeassistant/components/poolsense/config_flow.py index 653ba026ebf..7ab9691d134 100644 --- a/homeassistant/components/poolsense/config_flow.py +++ b/homeassistant/components/poolsense/config_flow.py @@ -17,7 +17,6 @@ class PoolSenseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for PoolSense.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize PoolSense config flow.""" diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 640993af74d..538382542ef 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -53,7 +53,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Tesla Powerwall.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the powerwall flow.""" diff --git a/homeassistant/components/profiler/config_flow.py b/homeassistant/components/profiler/config_flow.py index 259c300239c..b63246ce386 100644 --- a/homeassistant/components/profiler/config_flow.py +++ b/homeassistant/components/profiler/config_flow.py @@ -10,7 +10,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Profiler.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/progettihwsw/config_flow.py b/homeassistant/components/progettihwsw/config_flow.py index dca29668a84..9dcda920a64 100644 --- a/homeassistant/components/progettihwsw/config_flow.py +++ b/homeassistant/components/progettihwsw/config_flow.py @@ -44,7 +44,6 @@ class ProgettiHWSWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for ProgettiHWSW Automation.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize class variables.""" diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index 616660b6972..1084aca1e8b 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -35,7 +35,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a PlayStation 4 config flow.""" VERSION = CONFIG_ENTRY_VERSION - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the config flow.""" diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py index 10591e5b82c..971a13acc2f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py +++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py @@ -11,7 +11,6 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=_DOMAIN_NAME): """Handle a config flow for `pvpc_hourly_pricing` to select the tariff.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 306b05d09a6..7d73344aadc 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -55,7 +55,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Rachio.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index e076a105576..37b7da4b56b 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -23,7 +23,6 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a RainMachine config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @staticmethod @callback diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py index 62b42e2bddf..1a53eb78d5e 100644 --- a/homeassistant/components/recollect_waste/config_flow.py +++ b/homeassistant/components/recollect_waste/config_flow.py @@ -21,7 +21,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for ReCollect Waste.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index da4d6447e76..91afd9da999 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -444,7 +444,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for RFXCOM RFXtrx.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH async def async_step_user(self, user_input=None): """Step when user initializes a integration.""" diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index a23a08b2a54..d4cc6796bf1 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -36,7 +36,6 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Ring.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL user_pass = None diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 76b6105df01..0bc9c49707a 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -58,7 +58,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Risco.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @core.callback diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index 86ef2d915f2..f1f037941b3 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -26,7 +26,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Rituals Perfume Genie.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index c39397ce8bc..0accf352fb1 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL, ) -from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult @@ -47,7 +47,6 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a Roku config flow.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL def __init__(self): """Set up the instance.""" diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 376447157c8..0f3bc44eba6 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -63,7 +63,6 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Roomba configuration flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the roomba flow.""" diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py index b1db9e39a25..799d50bdaab 100644 --- a/homeassistant/components/roon/config_flow.py +++ b/homeassistant/components/roon/config_flow.py @@ -105,7 +105,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for roon.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the Roon flow.""" diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py index 26be0e5bed9..463c7b1d550 100644 --- a/homeassistant/components/ruckus_unleashed/config_flow.py +++ b/homeassistant/components/ruckus_unleashed/config_flow.py @@ -46,7 +46,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Ruckus Unleashed.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 209b89f541a..ed728b19eba 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -48,7 +48,6 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Samsung TV config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize flow.""" diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index 05eaedf5ab7..c9b13a586ef 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -71,7 +71,6 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow to setup screen logic devices.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize ScreenLogic ConfigFlow.""" diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 4f88834eaca..e2a534f5fe9 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -43,7 +43,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Sense.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/sentry/config_flow.py b/homeassistant/components/sentry/config_flow.py index 0fb65badd0f..bcb540a1687 100644 --- a/homeassistant/components/sentry/config_flow.py +++ b/homeassistant/components/sentry/config_flow.py @@ -37,7 +37,6 @@ class SentryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Sentry config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 962d29d7775..8fef217a609 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -43,7 +43,6 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Shark IQ.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def _async_validate_input(self, user_input): """Validate form input.""" diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index a2eaa21bf1d..5bf8277066c 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -59,7 +59,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Shelly.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + host = None info = None device_info = None diff --git a/homeassistant/components/shopping_list/config_flow.py b/homeassistant/components/shopping_list/config_flow.py index 974174640be..23f66cecebb 100644 --- a/homeassistant/components/shopping_list/config_flow.py +++ b/homeassistant/components/shopping_list/config_flow.py @@ -8,7 +8,6 @@ class ShoppingListFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for ShoppingList component.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 09e0b96e742..ba51356f770 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -29,7 +29,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a SimpliSafe config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize the config flow.""" diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index b7fb1066e65..a5147098c9f 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -52,7 +52,6 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for SMA.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self) -> None: """Initialize.""" diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py index caa1bbf58f7..b13e540bae3 100644 --- a/homeassistant/components/smappee/config_flow.py +++ b/homeassistant/components/smappee/config_flow.py @@ -4,7 +4,6 @@ import logging from pysmappee import helper, mqtt import voluptuous as vol -from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS from homeassistant.helpers import config_entry_oauth2_flow @@ -25,7 +24,6 @@ class SmappeeFlowHandler( """Config Smappee config flow.""" DOMAIN = DOMAIN - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_oauth_create_entry(self, data): """Create an entry for the flow.""" diff --git a/homeassistant/components/smart_meter_texas/config_flow.py b/homeassistant/components/smart_meter_texas/config_flow.py index 9f6df058cc7..296040d85f0 100644 --- a/homeassistant/components/smart_meter_texas/config_flow.py +++ b/homeassistant/components/smart_meter_texas/config_flow.py @@ -48,7 +48,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Smart Meter Texas.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/smarthab/config_flow.py b/homeassistant/components/smarthab/config_flow.py index 9a4ec3ef325..826454ab4d8 100644 --- a/homeassistant/components/smarthab/config_flow.py +++ b/homeassistant/components/smarthab/config_flow.py @@ -16,7 +16,6 @@ class SmartHabConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """SmartHab config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def _show_setup_form(self, user_input=None, errors=None): """Show the setup form to the user.""" diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index f6cca7e0276..b69ef5d43a1 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -44,7 +44,6 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle configuration of SmartThings integrations.""" VERSION = 2 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH def __init__(self): """Create a new instance of the flow handler.""" diff --git a/homeassistant/components/smarttub/config_flow.py b/homeassistant/components/smarttub/config_flow.py index 933f5a92367..652aec746a1 100644 --- a/homeassistant/components/smarttub/config_flow.py +++ b/homeassistant/components/smarttub/config_flow.py @@ -22,7 +22,6 @@ class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """SmartTub configuration flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self) -> None: """Instantiate config flow.""" diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 1dd8f4b15ca..5fde538b744 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -25,7 +25,6 @@ class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for SMHI component.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self) -> None: """Initialize SMHI forecast configuration flow.""" diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py index 9546fba0773..37b78ee3ea3 100644 --- a/homeassistant/components/sms/config_flow.py +++ b/homeassistant/components/sms/config_flow.py @@ -40,7 +40,6 @@ class SMSFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for SMS integration.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 4f2a7207d67..222bc27cdb2 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -29,7 +29,6 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self) -> None: """Initialize the config flow.""" diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index 39a752855aa..cced913222a 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -28,7 +28,6 @@ class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for solarlog.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self) -> None: """Initialize the config flow.""" diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py index afb5d05b77e..fcc5b238d70 100644 --- a/homeassistant/components/soma/config_flow.py +++ b/homeassistant/components/soma/config_flow.py @@ -19,7 +19,6 @@ 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.""" diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index a302cdd8887..016ad63c0d8 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -13,7 +13,6 @@ class SomfyFlowHandler( """Config flow to handle Somfy OAuth2 authentication.""" DOMAIN = DOMAIN - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @property def logger(self) -> logging.Logger: diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index 739251e041f..e97fe088173 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -51,7 +51,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Somfy MyLink.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED def __init__(self): """Initialize the somfy_mylink flow.""" diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index b68cc33712a..b7636ca4db1 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -7,7 +7,7 @@ from typing import Any from sonarr import Sonarr, SonarrAccessRestricted, SonarrError import voluptuous as vol -from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -62,7 +62,6 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Sonarr.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the flow.""" diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py index 22f9f0932ed..ae7c9406a92 100644 --- a/homeassistant/components/songpal/config_flow.py +++ b/homeassistant/components/songpal/config_flow.py @@ -31,7 +31,6 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Songpal configuration flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the flow.""" diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py index 280a7e391c3..49654b6c02b 100644 --- a/homeassistant/components/speedtestdotnet/config_flow.py +++ b/homeassistant/components/speedtestdotnet/config_flow.py @@ -21,7 +21,6 @@ class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle Speedtest.net config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @staticmethod @callback diff --git a/homeassistant/components/spider/config_flow.py b/homeassistant/components/spider/config_flow.py index c8c31221a50..7b31acb453d 100644 --- a/homeassistant/components/spider/config_flow.py +++ b/homeassistant/components/spider/config_flow.py @@ -24,7 +24,6 @@ class SpiderConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Spider config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize the Spider flow.""" diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index 3b7724a21a9..33e5e67a244 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -7,7 +7,6 @@ from typing import Any from spotipy import Spotify import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import persistent_notification from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow @@ -22,7 +21,6 @@ class SpotifyFlowHandler( DOMAIN = DOMAIN VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self) -> None: """Instantiate config flow.""" diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index adfa5895b7d..f81ff505583 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -59,7 +59,6 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Logitech Squeezebox.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize an instance of the squeezebox config flow.""" diff --git a/homeassistant/components/srp_energy/config_flow.py b/homeassistant/components/srp_energy/config_flow.py index 51b6f80bb33..f3df156f746 100644 --- a/homeassistant/components/srp_energy/config_flow.py +++ b/homeassistant/components/srp_energy/config_flow.py @@ -16,7 +16,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=SRP_ENERGY_DOMAIN): """Handle a config flow for SRP Energy.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL config = { vol.Required(CONF_ID): str, diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index 9f8e0339210..25e01da4ee7 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -28,7 +28,6 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a StarLine config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize flow.""" diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py index 91ed8ad4214..980608893ac 100644 --- a/homeassistant/components/subaru/config_flow.py +++ b/homeassistant/components/subaru/config_flow.py @@ -26,7 +26,6 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Subaru.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize config flow.""" diff --git a/homeassistant/components/syncthru/config_flow.py b/homeassistant/components/syncthru/config_flow.py index cb0243f98fc..31bf8c97edc 100644 --- a/homeassistant/components/syncthru/config_flow.py +++ b/homeassistant/components/syncthru/config_flow.py @@ -19,7 +19,6 @@ class SyncThruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Samsung SyncThru config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL url: str name: str diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index b3b26e892a8..3cbc58d4cf4 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -79,7 +79,6 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback From 671aabf9f409dbdcf500c425a755873a59774a70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Apr 2021 21:58:48 -1000 Subject: [PATCH 081/852] Remove unused imports in fritz, nest, and somfy to fix CI (#49940) --- homeassistant/components/fritz/config_flow.py | 1 - homeassistant/components/nest/config_flow.py | 1 - homeassistant/components/somfy/config_flow.py | 1 - 3 files changed, 3 deletions(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index fa41dc1e44c..a8afff6e41e 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -5,7 +5,6 @@ from urllib.parse import urlparse from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME, diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 93be1da407c..cd705d816c2 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -21,7 +21,6 @@ import os import async_timeout import voluptuous as vol -from homeassistant import config_entries from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_entry_oauth2_flow diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index 016ad63c0d8..dd803f86af7 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Somfy.""" import logging -from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN From e597202b24d8e2e2c4dcf898096380c1f36f0cf4 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Sat, 1 May 2021 14:49:17 +0300 Subject: [PATCH 082/852] Update LG Netcast to use new backend library (#49927) * Update LG Netcast to use new backend library Bump lgnetcast to 0.3.3 * Add codeowner in LG Netcast --- CODEOWNERS | 1 + homeassistant/components/lg_netcast/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6bb6221ec32..241b9c8ce5d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -258,6 +258,7 @@ homeassistant/components/kulersky/* @emlove homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus +homeassistant/components/lg_netcast/* @Drafteed homeassistant/components/life360/* @pnbruckner homeassistant/components/linux_battery/* @fabaff homeassistant/components/litejet/* @joncar diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index d214cebc636..7ee8eb063ff 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -2,7 +2,7 @@ "domain": "lg_netcast", "name": "LG Netcast", "documentation": "https://www.home-assistant.io/integrations/lg_netcast", - "requirements": ["pylgnetcast-homeassistant==0.2.0.dev0"], - "codeowners": [], + "requirements": ["pylgnetcast==0.3.3"], + "codeowners": ["@Drafteed"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9ab79071031..5722382180c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1512,7 +1512,7 @@ pylast==4.2.0 pylaunches==1.0.0 # homeassistant.components.lg_netcast -pylgnetcast-homeassistant==0.2.0.dev0 +pylgnetcast==0.3.3 # homeassistant.components.forked_daapd pylibrespot-java==0.1.0 From 20152313db5e5fef05fd8acda7474358f30f801d Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 1 May 2021 16:00:40 +0300 Subject: [PATCH 083/852] Fix light services descriptions (#49951) --- homeassistant/components/light/services.yaml | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index fe96f3a6777..dc12a72215d 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -20,11 +20,25 @@ turn_on: mode: slider rgb_color: name: RGB-color - description: Color for the light in RGB-format. + description: A list containing three integers between 0 and 255 representing the RGB (red, green, blue) color for the light. advanced: true example: "[255, 100, 100]" selector: object: + rgbw_color: + name: RGBW-color + description: A list containing four integers between 0 and 255 representing the RGBW (red, green, blue, white) color for the light. + advanced: true + example: "[255, 100, 100, 50]" + selector: + object: + rgbww_color: + name: RGBWW-color + description: A list containing five integers between 0 and 255 representing the RGBWW (red, green, blue, cold white, warm white) color for the light. + advanced: true + example: "[255, 100, 100, 50, 70]" + selector: + object: color_name: name: Color name description: A human readable color name. @@ -221,17 +235,6 @@ turn_on: step: 100 unit_of_measurement: K mode: slider - white_value: - name: White level - description: Number between 0..255 indicating level of white. - advanced: true - example: "250" - selector: - number: - min: 0 - max: 255 - step: 1 - mode: slider brightness: name: Brightness value description: From 60ae230499f4d65a0aae03b333958783c425c5d2 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 1 May 2021 16:13:43 +0200 Subject: [PATCH 084/852] Please mypy (axis). (#49949) * Please mypy (axis). * Update homeassistant/components/axis/config_flow.py Co-authored-by: Robert Svensson Co-authored-by: Robert Svensson --- homeassistant/components/axis/config_flow.py | 2 +- mypy.ini | 2 +- script/hassfest/mypy_config.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 12922a1f536..8753114d86e 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -156,7 +156,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): return await self._process_discovered_device( { CONF_HOST: discovery_info[IP_ADDRESS], - CONF_MAC: format_mac(discovery_info.get(MAC_ADDRESS)), + CONF_MAC: format_mac(discovery_info.get(MAC_ADDRESS, "")), CONF_NAME: discovery_info.get(HOSTNAME), CONF_PORT: DEFAULT_PORT, } diff --git a/mypy.ini b/mypy.ini index ba2ab3ad437..a5884060804 100644 --- a/mypy.ini +++ b/mypy.ini @@ -61,5 +61,5 @@ warn_return_any = false warn_unreachable = false warn_unused_ignores = false -[mypy-homeassistant.components.adguard.*,homeassistant.components.aemet.*,homeassistant.components.airly.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.amcrest.*,homeassistant.components.analytics.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aurora.*,homeassistant.components.awair.*,homeassistant.components.axis.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.blueprint.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.bsblan.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.config.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.deconz.*,homeassistant.components.demo.*,homeassistant.components.denonavr.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dhcp.*,homeassistant.components.directv.*,homeassistant.components.doorbird.*,homeassistant.components.dsmr.*,homeassistant.components.dynalite.*,homeassistant.components.eafm.*,homeassistant.components.edl21.*,homeassistant.components.elkm1.*,homeassistant.components.emonitor.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.esphome.*,homeassistant.components.evohome.*,homeassistant.components.fan.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.flo.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.freebox.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.garmin_connect.*,homeassistant.components.geniushub.*,homeassistant.components.gios.*,homeassistant.components.glances.*,homeassistant.components.gogogate2.*,homeassistant.components.google_assistant.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.gpmdp.*,homeassistant.components.gree.*,homeassistant.components.growatt_server.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.hdmi_cec.*,homeassistant.components.here_travel_time.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.honeywell.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.ihc.*,homeassistant.components.image.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.insteon.*,homeassistant.components.ipp.*,homeassistant.components.isy994.*,homeassistant.components.izone.*,homeassistant.components.kaiterra.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.lifx.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.lovelace.*,homeassistant.components.luftdaten.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lyric.*,homeassistant.components.marytts.*,homeassistant.components.media_source.*,homeassistant.components.melcloud.*,homeassistant.components.meteo_france.*,homeassistant.components.metoffice.*,homeassistant.components.minecraft_server.*,homeassistant.components.mobile_app.*,homeassistant.components.modbus.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mqtt.*,homeassistant.components.mullvad.*,homeassistant.components.mysensors.*,homeassistant.components.n26.*,homeassistant.components.neato.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netio.*,homeassistant.components.nightscout.*,homeassistant.components.nilu.*,homeassistant.components.nmap_tracker.*,homeassistant.components.norway_air.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nuki.*,homeassistant.components.nws.*,homeassistant.components.nzbget.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onvif.*,homeassistant.components.ovo_energy.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_viera.*,homeassistant.components.philips_js.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.plaato.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.point.*,homeassistant.components.profiler.*,homeassistant.components.proxmoxve.*,homeassistant.components.rachio.*,homeassistant.components.rainmachine.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.reddit.*,homeassistant.components.ring.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.roku.*,homeassistant.components.rpi_power.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.sabnzbd.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.search.*,homeassistant.components.sense.*,homeassistant.components.sentry.*,homeassistant.components.sesame.*,homeassistant.components.sharkiq.*,homeassistant.components.shell_command.*,homeassistant.components.shelly.*,homeassistant.components.sma.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.solaredge.*,homeassistant.components.solarlog.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.spotify.*,homeassistant.components.stream.*,homeassistant.components.stt.*,homeassistant.components.surepetcare.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tasmota.*,homeassistant.components.tcp.*,homeassistant.components.telegram_bot.*,homeassistant.components.template.*,homeassistant.components.tesla.*,homeassistant.components.timer.*,homeassistant.components.todoist.*,homeassistant.components.toon.*,homeassistant.components.tplink.*,homeassistant.components.trace.*,homeassistant.components.tradfri.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.unifi.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.velbus.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.vizio.*,homeassistant.components.volumio.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.wink.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wunderground.*,homeassistant.components.xbox.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.yamaha.*,homeassistant.components.yeelight.*,homeassistant.components.zerproc.*,homeassistant.components.zha.*,homeassistant.components.zwave.*] +[mypy-homeassistant.components.adguard.*,homeassistant.components.aemet.*,homeassistant.components.airly.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.amcrest.*,homeassistant.components.analytics.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aurora.*,homeassistant.components.awair.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.blueprint.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.bsblan.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.config.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.deconz.*,homeassistant.components.demo.*,homeassistant.components.denonavr.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dhcp.*,homeassistant.components.directv.*,homeassistant.components.doorbird.*,homeassistant.components.dsmr.*,homeassistant.components.dynalite.*,homeassistant.components.eafm.*,homeassistant.components.edl21.*,homeassistant.components.elkm1.*,homeassistant.components.emonitor.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.esphome.*,homeassistant.components.evohome.*,homeassistant.components.fan.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.flo.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.freebox.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.garmin_connect.*,homeassistant.components.geniushub.*,homeassistant.components.gios.*,homeassistant.components.glances.*,homeassistant.components.gogogate2.*,homeassistant.components.google_assistant.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.gpmdp.*,homeassistant.components.gree.*,homeassistant.components.growatt_server.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.hdmi_cec.*,homeassistant.components.here_travel_time.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.honeywell.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.ihc.*,homeassistant.components.image.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.insteon.*,homeassistant.components.ipp.*,homeassistant.components.isy994.*,homeassistant.components.izone.*,homeassistant.components.kaiterra.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.lifx.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.lovelace.*,homeassistant.components.luftdaten.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lyric.*,homeassistant.components.marytts.*,homeassistant.components.media_source.*,homeassistant.components.melcloud.*,homeassistant.components.meteo_france.*,homeassistant.components.metoffice.*,homeassistant.components.minecraft_server.*,homeassistant.components.mobile_app.*,homeassistant.components.modbus.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mqtt.*,homeassistant.components.mullvad.*,homeassistant.components.mysensors.*,homeassistant.components.n26.*,homeassistant.components.neato.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netio.*,homeassistant.components.nightscout.*,homeassistant.components.nilu.*,homeassistant.components.nmap_tracker.*,homeassistant.components.norway_air.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nuki.*,homeassistant.components.nws.*,homeassistant.components.nzbget.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onvif.*,homeassistant.components.ovo_energy.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_viera.*,homeassistant.components.philips_js.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.plaato.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.point.*,homeassistant.components.profiler.*,homeassistant.components.proxmoxve.*,homeassistant.components.rachio.*,homeassistant.components.rainmachine.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.reddit.*,homeassistant.components.ring.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.roku.*,homeassistant.components.rpi_power.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.sabnzbd.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.search.*,homeassistant.components.sense.*,homeassistant.components.sentry.*,homeassistant.components.sesame.*,homeassistant.components.sharkiq.*,homeassistant.components.shell_command.*,homeassistant.components.shelly.*,homeassistant.components.sma.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.solaredge.*,homeassistant.components.solarlog.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.spotify.*,homeassistant.components.stream.*,homeassistant.components.stt.*,homeassistant.components.surepetcare.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tasmota.*,homeassistant.components.tcp.*,homeassistant.components.telegram_bot.*,homeassistant.components.template.*,homeassistant.components.tesla.*,homeassistant.components.timer.*,homeassistant.components.todoist.*,homeassistant.components.toon.*,homeassistant.components.tplink.*,homeassistant.components.trace.*,homeassistant.components.tradfri.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.unifi.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.velbus.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.vizio.*,homeassistant.components.volumio.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.wink.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wunderground.*,homeassistant.components.xbox.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.yamaha.*,homeassistant.components.yeelight.*,homeassistant.components.zerproc.*,homeassistant.components.zha.*,homeassistant.components.zwave.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 9f1633d8494..0066c230fed 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -26,7 +26,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.atag.*", "homeassistant.components.aurora.*", "homeassistant.components.awair.*", - "homeassistant.components.axis.*", "homeassistant.components.azure_devops.*", "homeassistant.components.azure_event_hub.*", "homeassistant.components.blueprint.*", From 2440f25aaf7863a83b266a5b728a062498a354b4 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 1 May 2021 17:43:03 +0300 Subject: [PATCH 085/852] Shelly light color mode bugfix (#49948) --- homeassistant/components/shelly/light.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 61cb961e224..f3e80ee87b0 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -74,9 +74,10 @@ class ShellyLight(ShellyBlockEntity, LightEntity): if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): self._min_kelvin = KELVIN_MIN_VALUE_COLOR - self._supported_color_modes.add(COLOR_MODE_RGB) if hasattr(block, "white"): self._supported_color_modes.add(COLOR_MODE_RGBW) + else: + self._supported_color_modes.add(COLOR_MODE_RGB) if hasattr(block, "colorTemp"): self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) @@ -146,7 +147,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return COLOR_MODE_ONOFF @property - def rgb_color(self) -> tuple[int, int, int] | None: + def rgb_color(self) -> tuple[int, int, int]: """Return the rgb color value [int, int, int].""" if self.control_result: red = self.control_result["red"] @@ -156,17 +157,17 @@ class ShellyLight(ShellyBlockEntity, LightEntity): red = self.block.red green = self.block.green blue = self.block.blue - return [red, green, blue] + return (red, green, blue) @property - def rgbw_color(self) -> tuple[int, int, int, int] | None: + def rgbw_color(self) -> tuple[int, int, int, int]: """Return the rgbw color value [int, int, int, int].""" if self.control_result: white = self.control_result["white"] else: white = self.block.white - return [*self.rgb_color, white] + return (*self.rgb_color, white) @property def color_temp(self) -> int | None: From ebee5f7808acead8ac5b657cb3bc7b963eaaeebb Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 1 May 2021 21:01:56 +0200 Subject: [PATCH 086/852] Fix ihc typing (#49946) --- homeassistant/components/ihc/light.py | 2 +- mypy.ini | 2 +- script/hassfest/mypy_config.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ihc/light.py b/homeassistant/components/ihc/light.py index 86aaca9e296..c8fe9ef54de 100644 --- a/homeassistant/components/ihc/light.py +++ b/homeassistant/components/ihc/light.py @@ -60,7 +60,7 @@ class IhcLight(IHCDevice, LightEntity): self._ihc_on_id = ihc_on_id self._brightness = 0 self._dimmable = dimmable - self._state = None + self._state = False @property def brightness(self) -> int: diff --git a/mypy.ini b/mypy.ini index a5884060804..e5050ba7f34 100644 --- a/mypy.ini +++ b/mypy.ini @@ -61,5 +61,5 @@ warn_return_any = false warn_unreachable = false warn_unused_ignores = false -[mypy-homeassistant.components.adguard.*,homeassistant.components.aemet.*,homeassistant.components.airly.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.amcrest.*,homeassistant.components.analytics.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aurora.*,homeassistant.components.awair.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.blueprint.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.bsblan.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.config.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.deconz.*,homeassistant.components.demo.*,homeassistant.components.denonavr.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dhcp.*,homeassistant.components.directv.*,homeassistant.components.doorbird.*,homeassistant.components.dsmr.*,homeassistant.components.dynalite.*,homeassistant.components.eafm.*,homeassistant.components.edl21.*,homeassistant.components.elkm1.*,homeassistant.components.emonitor.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.esphome.*,homeassistant.components.evohome.*,homeassistant.components.fan.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.flo.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.freebox.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.garmin_connect.*,homeassistant.components.geniushub.*,homeassistant.components.gios.*,homeassistant.components.glances.*,homeassistant.components.gogogate2.*,homeassistant.components.google_assistant.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.gpmdp.*,homeassistant.components.gree.*,homeassistant.components.growatt_server.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.hdmi_cec.*,homeassistant.components.here_travel_time.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.honeywell.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.ihc.*,homeassistant.components.image.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.insteon.*,homeassistant.components.ipp.*,homeassistant.components.isy994.*,homeassistant.components.izone.*,homeassistant.components.kaiterra.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.lifx.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.lovelace.*,homeassistant.components.luftdaten.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lyric.*,homeassistant.components.marytts.*,homeassistant.components.media_source.*,homeassistant.components.melcloud.*,homeassistant.components.meteo_france.*,homeassistant.components.metoffice.*,homeassistant.components.minecraft_server.*,homeassistant.components.mobile_app.*,homeassistant.components.modbus.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mqtt.*,homeassistant.components.mullvad.*,homeassistant.components.mysensors.*,homeassistant.components.n26.*,homeassistant.components.neato.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netio.*,homeassistant.components.nightscout.*,homeassistant.components.nilu.*,homeassistant.components.nmap_tracker.*,homeassistant.components.norway_air.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nuki.*,homeassistant.components.nws.*,homeassistant.components.nzbget.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onvif.*,homeassistant.components.ovo_energy.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_viera.*,homeassistant.components.philips_js.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.plaato.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.point.*,homeassistant.components.profiler.*,homeassistant.components.proxmoxve.*,homeassistant.components.rachio.*,homeassistant.components.rainmachine.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.reddit.*,homeassistant.components.ring.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.roku.*,homeassistant.components.rpi_power.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.sabnzbd.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.search.*,homeassistant.components.sense.*,homeassistant.components.sentry.*,homeassistant.components.sesame.*,homeassistant.components.sharkiq.*,homeassistant.components.shell_command.*,homeassistant.components.shelly.*,homeassistant.components.sma.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.solaredge.*,homeassistant.components.solarlog.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.spotify.*,homeassistant.components.stream.*,homeassistant.components.stt.*,homeassistant.components.surepetcare.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tasmota.*,homeassistant.components.tcp.*,homeassistant.components.telegram_bot.*,homeassistant.components.template.*,homeassistant.components.tesla.*,homeassistant.components.timer.*,homeassistant.components.todoist.*,homeassistant.components.toon.*,homeassistant.components.tplink.*,homeassistant.components.trace.*,homeassistant.components.tradfri.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.unifi.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.velbus.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.vizio.*,homeassistant.components.volumio.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.wink.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wunderground.*,homeassistant.components.xbox.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.yamaha.*,homeassistant.components.yeelight.*,homeassistant.components.zerproc.*,homeassistant.components.zha.*,homeassistant.components.zwave.*] +[mypy-homeassistant.components.adguard.*,homeassistant.components.aemet.*,homeassistant.components.airly.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.amcrest.*,homeassistant.components.analytics.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aurora.*,homeassistant.components.awair.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.blueprint.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.bsblan.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.config.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.deconz.*,homeassistant.components.demo.*,homeassistant.components.denonavr.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dhcp.*,homeassistant.components.directv.*,homeassistant.components.doorbird.*,homeassistant.components.dsmr.*,homeassistant.components.dynalite.*,homeassistant.components.eafm.*,homeassistant.components.edl21.*,homeassistant.components.elkm1.*,homeassistant.components.emonitor.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.esphome.*,homeassistant.components.evohome.*,homeassistant.components.fan.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.flo.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.freebox.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.garmin_connect.*,homeassistant.components.geniushub.*,homeassistant.components.gios.*,homeassistant.components.glances.*,homeassistant.components.gogogate2.*,homeassistant.components.google_assistant.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.gpmdp.*,homeassistant.components.gree.*,homeassistant.components.growatt_server.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.hdmi_cec.*,homeassistant.components.here_travel_time.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.honeywell.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.image.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.insteon.*,homeassistant.components.ipp.*,homeassistant.components.isy994.*,homeassistant.components.izone.*,homeassistant.components.kaiterra.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.lifx.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.lovelace.*,homeassistant.components.luftdaten.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lyric.*,homeassistant.components.marytts.*,homeassistant.components.media_source.*,homeassistant.components.melcloud.*,homeassistant.components.meteo_france.*,homeassistant.components.metoffice.*,homeassistant.components.minecraft_server.*,homeassistant.components.mobile_app.*,homeassistant.components.modbus.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mqtt.*,homeassistant.components.mullvad.*,homeassistant.components.mysensors.*,homeassistant.components.n26.*,homeassistant.components.neato.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netio.*,homeassistant.components.nightscout.*,homeassistant.components.nilu.*,homeassistant.components.nmap_tracker.*,homeassistant.components.norway_air.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nuki.*,homeassistant.components.nws.*,homeassistant.components.nzbget.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onvif.*,homeassistant.components.ovo_energy.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_viera.*,homeassistant.components.philips_js.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.plaato.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.point.*,homeassistant.components.profiler.*,homeassistant.components.proxmoxve.*,homeassistant.components.rachio.*,homeassistant.components.rainmachine.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.reddit.*,homeassistant.components.ring.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.roku.*,homeassistant.components.rpi_power.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.sabnzbd.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.search.*,homeassistant.components.sense.*,homeassistant.components.sentry.*,homeassistant.components.sesame.*,homeassistant.components.sharkiq.*,homeassistant.components.shell_command.*,homeassistant.components.shelly.*,homeassistant.components.sma.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.solaredge.*,homeassistant.components.solarlog.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.spotify.*,homeassistant.components.stream.*,homeassistant.components.stt.*,homeassistant.components.surepetcare.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tasmota.*,homeassistant.components.tcp.*,homeassistant.components.telegram_bot.*,homeassistant.components.template.*,homeassistant.components.tesla.*,homeassistant.components.timer.*,homeassistant.components.todoist.*,homeassistant.components.toon.*,homeassistant.components.tplink.*,homeassistant.components.trace.*,homeassistant.components.tradfri.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.unifi.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.velbus.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.vizio.*,homeassistant.components.volumio.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.wink.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wunderground.*,homeassistant.components.xbox.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.yamaha.*,homeassistant.components.yeelight.*,homeassistant.components.zerproc.*,homeassistant.components.zha.*,homeassistant.components.zwave.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 0066c230fed..345b2c71827 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -104,7 +104,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.humidifier.*", "homeassistant.components.iaqualink.*", "homeassistant.components.icloud.*", - "homeassistant.components.ihc.*", "homeassistant.components.image.*", "homeassistant.components.incomfort.*", "homeassistant.components.influxdb.*", From ef2b8bbca8517584f86f49c7ab630d53caea2574 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 1 May 2021 15:55:04 -0400 Subject: [PATCH 087/852] Bump up ZHA dependencies (#49959) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e64dee8d0a2..42859f301b7 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.23.1", + "bellows==0.24.0", "pyserial==3.5", "pyserial-asyncio==0.5", "zha-quirks==0.0.57", diff --git a/requirements_all.txt b/requirements_all.txt index 5722382180c..10d66338f1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -346,7 +346,7 @@ beautifulsoup4==4.9.3 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.23.1 +bellows==0.24.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d8c83396d6..0f2e2d0a3a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -202,7 +202,7 @@ azure-eventhub==5.1.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.23.1 +bellows==0.24.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.15 From 002b068c0a0316b9a4f86c962b7b913a4581baa4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 May 2021 11:17:52 -1000 Subject: [PATCH 088/852] Remove YAML support from sense (#49935) --- homeassistant/components/sense/__init__.py | 63 +++---------------- homeassistant/components/sense/config_flow.py | 7 --- homeassistant/components/sense/const.py | 3 - tests/components/sense/test_config_flow.py | 3 - 4 files changed, 10 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 162b7cd75cf..e431fe2487b 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -1,4 +1,5 @@ """Support for monitoring a Sense energy sensor.""" +import asyncio from datetime import timedelta import logging @@ -7,9 +8,8 @@ from sense_energy import ( SenseAPITimeoutException, SenseAuthenticationException, ) -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EMAIL, CONF_PASSWORD, @@ -19,42 +19,25 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( ACTIVE_UPDATE_RATE, - DEFAULT_TIMEOUT, DOMAIN, - EVENT_STOP_REMOVE, SENSE_DATA, SENSE_DEVICE_UPDATE, SENSE_DEVICES_DATA, SENSE_DISCOVERED_DEVICES_DATA, SENSE_TIMEOUT_EXCEPTIONS, SENSE_TRENDS_COORDINATOR, - TRACK_TIME_REMOVE, ) _LOGGER = logging.getLogger(__name__) PLATFORMS = ["binary_sensor", "sensor"] -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - class SenseDevicesData: """Data for each sense device.""" @@ -65,36 +48,13 @@ class SenseDevicesData: def set_devices_data(self, devices): """Store a device update.""" - self._data_by_device = {} - for device in devices: - self._data_by_device[device["id"]] = device + self._data_by_device = {device["id"]: device for device in devices} def get_device_by_id(self, sense_device_id): """Get the latest device data.""" return self._data_by_device.get(sense_device_id) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Sense component.""" - hass.data.setdefault(DOMAIN, {}) - conf = config.get(DOMAIN) - if not conf: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_EMAIL: conf[CONF_EMAIL], - CONF_PASSWORD: conf[CONF_PASSWORD], - CONF_TIMEOUT: conf[CONF_TIMEOUT], - }, - ) - ) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Sense from a config entry.""" @@ -136,9 +96,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # This can take longer than 60s and we already know # sense is online since get_discovered_device_data was # successful so we do it later. - hass.loop.create_task(trends_coordinator.async_request_refresh()) + asyncio.create_task(trends_coordinator.async_request_refresh()) - data = hass.data[DOMAIN][entry.entry_id] = { + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { SENSE_DATA: gateway, SENSE_DEVICES_DATA: sense_devices_data, SENSE_TRENDS_COORDINATOR: trends_coordinator, @@ -167,9 +127,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): def _remove_update_callback_at_stop(event): remove_update_callback() - data[TRACK_TIME_REMOVE] = remove_update_callback - data[EVENT_STOP_REMOVE] = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _remove_update_callback_at_stop + entry.async_on_unload(remove_update_callback) + entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _remove_update_callback_at_stop + ) ) return True @@ -178,11 +140,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - data = hass.data[DOMAIN][entry.entry_id] - data[EVENT_STOP_REMOVE]() - data[TRACK_TIME_REMOVE]() - if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index e2a534f5fe9..6bd33291d7f 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -63,10 +63,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - - async def async_step_import(self, user_input): - """Handle import.""" - await self.async_set_unique_id(user_input[CONF_EMAIL]) - self._abort_if_unique_id_configured() - - return await self.async_step_user(user_input) diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index a6e8b88b342..783fcb5508a 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -14,9 +14,6 @@ SENSE_DEVICES_DATA = "sense_devices_data" SENSE_DISCOVERED_DEVICES_DATA = "sense_discovered_devices" SENSE_TRENDS_COORDINATOR = "sense_trends_coordinator" -TRACK_TIME_REMOVE = "track_time_remove_callback" -EVENT_STOP_REMOVE = "event_stop_remove_callback" - ACTIVE_NAME = "Energy" ACTIVE_TYPE = "active" diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index 41cfdc017dd..55348cca838 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -17,8 +17,6 @@ async def test_form(hass): assert result["errors"] == {} with patch("sense_energy.ASyncSenseable.authenticate", return_value=True,), patch( - "homeassistant.components.sense.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.sense.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -35,7 +33,6 @@ async def test_form(hass): "email": "test-email", "password": "test-password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 9e432392406d76c57b15039e986003aa2eadf4c8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 1 May 2021 15:18:36 -0600 Subject: [PATCH 089/852] Bump simplisafe-python to 9.6.10 (#49962) --- 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 0a46e1d5280..d1016934694 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.9"], + "requirements": ["simplisafe-python==9.6.10"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 10d66338f1a..4dae0f719df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2070,7 +2070,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.9 +simplisafe-python==9.6.10 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f2e2d0a3a5..d683004039c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1101,7 +1101,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.9 +simplisafe-python==9.6.10 # homeassistant.components.slack slackclient==2.5.0 From 7ac05110ca57f6af6d3a201ce07388b4c3ab1cee Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 2 May 2021 00:03:52 +0200 Subject: [PATCH 090/852] Catch non payload modbus messages (#49910) --- homeassistant/components/modbus/modbus.py | 40 +++++++++-------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index f04c019e6a6..c1fbe7a9eb7 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -5,7 +5,6 @@ import threading from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient from pymodbus.constants import Defaults from pymodbus.exceptions import ModbusException -from pymodbus.pdu import ExceptionResponse, IllegalFunctionRequest from pymodbus.transaction import ModbusRtuFramer from homeassistant.const import ( @@ -237,8 +236,8 @@ class ModbusHub: result = self._client.read_coils(address, count, **kwargs) except ModbusException as exception_error: self._log_error(exception_error) - return None - if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + result = exception_error + if not hasattr(result, "registers"): self._log_error(result) return None self._in_error = False @@ -251,9 +250,8 @@ class ModbusHub: try: result = self._client.read_discrete_inputs(address, count, **kwargs) except ModbusException as exception_error: - self._log_error(exception_error) - return None - if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + result = exception_error + if not hasattr(result, "registers"): self._log_error(result) return None self._in_error = False @@ -266,9 +264,8 @@ class ModbusHub: try: result = self._client.read_input_registers(address, count, **kwargs) except ModbusException as exception_error: - self._log_error(exception_error) - return None - if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + result = exception_error + if not hasattr(result, "registers"): self._log_error(result) return None self._in_error = False @@ -281,9 +278,8 @@ class ModbusHub: try: result = self._client.read_holding_registers(address, count, **kwargs) except ModbusException as exception_error: - self._log_error(exception_error) - return None - if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + result = exception_error + if not hasattr(result, "registers"): self._log_error(result) return None self._in_error = False @@ -296,9 +292,8 @@ class ModbusHub: try: result = self._client.write_coil(address, value, **kwargs) except ModbusException as exception_error: - self._log_error(exception_error) - return False - if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + result = exception_error + if not hasattr(result, "registers"): self._log_error(result) return False self._in_error = False @@ -311,9 +306,8 @@ class ModbusHub: try: result = self._client.write_coils(address, values, **kwargs) except ModbusException as exception_error: - self._log_error(exception_error) - return False - if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + result = exception_error + if not hasattr(result, "registers"): self._log_error(result) return False self._in_error = False @@ -326,9 +320,8 @@ class ModbusHub: try: result = self._client.write_register(address, value, **kwargs) except ModbusException as exception_error: - self._log_error(exception_error) - return False - if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + result = exception_error + if not hasattr(result, "registers"): self._log_error(result) return False self._in_error = False @@ -341,9 +334,8 @@ class ModbusHub: try: result = self._client.write_registers(address, values, **kwargs) except ModbusException as exception_error: - self._log_error(exception_error) - return False - if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + result = exception_error + if not hasattr(result, "registers"): self._log_error(result) return False self._in_error = False From 1b5596b4c2e11fc88504e9f78b6d9031dd048ee1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 2 May 2021 00:15:27 +0200 Subject: [PATCH 091/852] Remove ServiceCallType alias from codebase (#49844) --- .../components/homematicip_cloud/services.py | 27 +++++++------------ .../components/input_boolean/__init__.py | 6 ++--- .../components/input_datetime/__init__.py | 6 ++--- .../components/input_number/__init__.py | 6 ++--- .../components/input_select/__init__.py | 6 ++--- .../components/input_text/__init__.py | 6 ++--- homeassistant/components/lovelace/__init__.py | 6 ++--- .../components/switcher_kis/switch.py | 7 +++-- homeassistant/components/timer/__init__.py | 6 ++--- homeassistant/helpers/typing.py | 15 ++++++----- 10 files changed, 42 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index 34e564cff69..bafe7599f06 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -11,14 +11,13 @@ from homematicip.base.helpers import handle_config import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.service import ( async_register_admin_service, verify_domain_control, ) -from homeassistant.helpers.typing import ServiceCallType from .const import DOMAIN as HMIPC_DOMAIN @@ -115,7 +114,7 @@ async def async_setup_services(hass: HomeAssistant) -> None: return @verify_domain_control(hass, HMIPC_DOMAIN) - async def async_call_hmipc_service(service: ServiceCallType): + async def async_call_hmipc_service(service: ServiceCall): """Call correct HomematicIP Cloud service.""" service_name = service.service @@ -205,7 +204,7 @@ async def async_unload_services(hass: HomeAssistant): async def _async_activate_eco_mode_with_duration( - hass: HomeAssistant, service: ServiceCallType + hass: HomeAssistant, service: ServiceCall ) -> None: """Service to activate eco mode with duration.""" duration = service.data[ATTR_DURATION] @@ -221,7 +220,7 @@ async def _async_activate_eco_mode_with_duration( async def _async_activate_eco_mode_with_period( - hass: HomeAssistant, service: ServiceCallType + hass: HomeAssistant, service: ServiceCall ) -> None: """Service to activate eco mode with period.""" endtime = service.data[ATTR_ENDTIME] @@ -236,9 +235,7 @@ async def _async_activate_eco_mode_with_period( await hap.home.activate_absence_with_period(endtime) -async def _async_activate_vacation( - hass: HomeAssistant, service: ServiceCallType -) -> None: +async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> None: """Service to activate vacation.""" endtime = service.data[ATTR_ENDTIME] temperature = service.data[ATTR_TEMPERATURE] @@ -253,9 +250,7 @@ async def _async_activate_vacation( await hap.home.activate_vacation(endtime, temperature) -async def _async_deactivate_eco_mode( - hass: HomeAssistant, service: ServiceCallType -) -> None: +async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None: """Service to deactivate eco mode.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -268,9 +263,7 @@ async def _async_deactivate_eco_mode( await hap.home.deactivate_absence() -async def _async_deactivate_vacation( - hass: HomeAssistant, service: ServiceCallType -) -> None: +async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None: """Service to deactivate vacation.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -284,7 +277,7 @@ async def _async_deactivate_vacation( async def _set_active_climate_profile( - hass: HomeAssistant, service: ServiceCallType + hass: HomeAssistant, service: ServiceCall ) -> None: """Service to set the active climate profile.""" entity_id_list = service.data[ATTR_ENTITY_ID] @@ -302,7 +295,7 @@ async def _set_active_climate_profile( await group.set_active_profile(climate_profile_index) -async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCallType) -> None: +async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> None: """Service to dump the configuration of a Homematic IP Access Point.""" config_path = service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] @@ -324,7 +317,7 @@ async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCallType) config_file.write_text(json_state, encoding="utf8") -async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCallType): +async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall): """Service to reset the energy counter.""" entity_id_list = service.data[ATTR_ENTITY_ID] diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index e030a530253..399ab73783b 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity @@ -24,7 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, ServiceCallType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass DOMAIN = "input_boolean" @@ -112,7 +112,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - async def reload_service_handler(service_call: ServiceCallType) -> None: + async def reload_service_handler(service_call: ServiceCall) -> None: """Remove all input booleans and load new ones from config.""" conf = await component.async_prepare_reload(skip_reset=True) if conf is None: diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 68b7f9f32d5..f423367019e 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -15,14 +15,14 @@ from homeassistant.const import ( CONF_NAME, SERVICE_RELOAD, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, ServiceCallType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -131,7 +131,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - async def reload_service_handler(service_call: ServiceCallType) -> None: + async def reload_service_handler(service_call: ServiceCall) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) if conf is None: diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index a895326c677..7c6a34f6e5b 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -15,14 +15,14 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, ServiceCallType +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -142,7 +142,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - async def reload_service_handler(service_call: ServiceCallType) -> None: + async def reload_service_handler(service_call: ServiceCall) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) if conf is None: diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 374254a5052..b53b907ca10 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -13,14 +13,14 @@ from homeassistant.const import ( CONF_NAME, SERVICE_RELOAD, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, ServiceCallType +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -117,7 +117,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - async def reload_service_handler(service_call: ServiceCallType) -> None: + async def reload_service_handler(service_call: ServiceCall) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) if conf is None: diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 2f9f6cb47ba..f4daec0a4d4 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -15,14 +15,14 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, ServiceCallType +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -142,7 +142,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - async def reload_service_handler(service_call: ServiceCallType) -> None: + async def reload_service_handler(service_call: ServiceCall) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) if conf is None: diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index a5f0e043139..e16f1399c40 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -6,11 +6,11 @@ import voluptuous as vol from homeassistant.components import frontend from homeassistant.config import async_hass_config_yaml, async_process_component_config from homeassistant.const import CONF_FILENAME, CONF_MODE, CONF_RESOURCES -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.typing import ConfigType, ServiceCallType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from . import dashboard, resources, websocket @@ -74,7 +74,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): frontend.async_register_built_in_panel(hass, DOMAIN, config={"mode": mode}) - async def reload_resources_service_handler(service_call: ServiceCallType) -> None: + async def reload_resources_service_handler(service_call: ServiceCall) -> None: """Reload yaml resources.""" try: conf = await async_hass_config_yaml(hass) diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 5bad50a7985..7a646d3de4a 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -16,10 +16,9 @@ from aioswitcher.devices import SwitcherV2Device import voluptuous as vol from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import ServiceCallType from . import ( ATTR_AUTO_OFF_SET, @@ -63,7 +62,7 @@ async def async_setup_platform( if discovery_info is None: return - async def async_set_auto_off_service(entity, service_call: ServiceCallType) -> None: + async def async_set_auto_off_service(entity, service_call: ServiceCall) -> None: """Use for handling setting device auto-off service calls.""" async with SwitcherV2Api( hass.loop, @@ -75,7 +74,7 @@ async def async_setup_platform( await swapi.set_auto_shutdown(service_call.data[CONF_AUTO_OFF]) async def async_turn_on_with_timer_service( - entity, service_call: ServiceCallType + entity, service_call: ServiceCall ) -> None: """Use for handling turning device on with a timer service calls.""" async with SwitcherV2Api( diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 9a2b053a8e3..ded59a0a6d1 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_NAME, SERVICE_RELOAD, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, ServiceCallType +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -130,7 +130,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - async def reload_service_handler(service_call: ServiceCallType) -> None: + async def reload_service_handler(service_call: ServiceCall) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) if conf is None: diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 58f999c5adc..7d01b0b6a77 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -9,17 +9,10 @@ ConfigType = Dict[str, Any] ContextType = homeassistant.core.Context DiscoveryInfoType = Dict[str, Any] EventType = homeassistant.core.Event -ServiceCallType = homeassistant.core.ServiceCall ServiceDataType = Dict[str, Any] StateType = Union[None, str, int, float] TemplateVarsType = Optional[Mapping[str, Any]] -# HomeAssistantType is not to be used, -# It is not present in the core code base. -# It is kept in order not to break custom components -# In due time it will be removed. -HomeAssistantType = homeassistant.core.HomeAssistant - # Custom type for recorder Queries QueryType = Any @@ -31,3 +24,11 @@ class UndefinedType(Enum): UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access + +# The following types should not used and +# are not present in the core code base. +# They are kept in order not to break custom integrations +# that may rely on them. +# In due time they will be removed. +HomeAssistantType = homeassistant.core.HomeAssistant +ServiceCallType = homeassistant.core.ServiceCall From 4bebedb658b230c4bfce613273cf4ab3f112ed1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 May 2021 12:26:10 -1000 Subject: [PATCH 092/852] Bump pysonos to 0.0.44 to fix client session race (#49964) Fixes #49954 --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index a2424ae4d69..00ee92ed079 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.43"], + "requirements": ["pysonos==0.0.44"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 4dae0f719df..ddde6b15107 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1741,7 +1741,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.43 +pysonos==0.0.44 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d683004039c..0282c879ad4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -959,7 +959,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.43 +pysonos==0.0.44 # homeassistant.components.spc pyspcwebgw==0.4.0 From ddd7e79ee9a98cbfb7faac42c5ff7dbba86c7e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 2 May 2021 01:33:31 +0300 Subject: [PATCH 093/852] Improve device registry internal typing (#49924) --- homeassistant/helpers/device_registry.py | 94 ++++++++++++------------ 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 024b11476e7..a448fd1c198 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections import OrderedDict import logging import time -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, NamedTuple, cast import attr @@ -37,11 +37,6 @@ CONNECTION_NETWORK_MAC = "mac" CONNECTION_UPNP = "upnp" CONNECTION_ZIGBEE = "zigbee" -IDX_CONNECTIONS = "connections" -IDX_IDENTIFIERS = "identifiers" -REGISTERED_DEVICE = "registered" -DELETED_DEVICE = "deleted" - DISABLED_CONFIG_ENTRY = "config_entry" DISABLED_INTEGRATION = "integration" DISABLED_USER = "user" @@ -49,6 +44,11 @@ DISABLED_USER = "user" ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 +class _DeviceIndex(NamedTuple): + identifiers: dict[tuple[str, ...], str] + connections: dict[tuple[str, str], str] + + @attr.s(slots=True, frozen=True) class DeviceEntry: """Device Registry Entry.""" @@ -133,12 +133,30 @@ def format_mac(mac: str) -> str: return mac +def _async_get_device_id_from_index( + devices_index: _DeviceIndex, + identifiers: set[tuple[str, ...]], + connections: set[tuple[str, str]] | None, +) -> str | None: + """Check if device has previously been registered.""" + for identifier in identifiers: + if identifier in devices_index.identifiers: + return devices_index.identifiers[identifier] + if not connections: + return None + for connection in _normalize_connections(connections): + if connection in devices_index.connections: + return devices_index.connections[connection] + return None + + class DeviceRegistry: """Class to hold a registry of devices.""" devices: dict[str, DeviceEntry] deleted_devices: dict[str, DeletedDeviceEntry] - _devices_index: dict[str, dict[str, dict[tuple[str, ...], str]]] + _registered_index: _DeviceIndex + _deleted_index: _DeviceIndex def __init__(self, hass: HomeAssistant) -> None: """Initialize the device registry.""" @@ -158,8 +176,8 @@ class DeviceRegistry: connections: set[tuple[str, str]] | None = None, ) -> DeviceEntry | None: """Check if device is registered.""" - device_id = self._async_get_device_id_from_index( - REGISTERED_DEVICE, identifiers, connections + device_id = _async_get_device_id_from_index( + self._registered_index, identifiers, connections ) if device_id is None: return None @@ -171,38 +189,20 @@ class DeviceRegistry: connections: set[tuple[str, str]] | None, ) -> DeletedDeviceEntry | None: """Check if device is deleted.""" - device_id = self._async_get_device_id_from_index( - DELETED_DEVICE, identifiers, connections + device_id = _async_get_device_id_from_index( + self._deleted_index, identifiers, connections ) if device_id is None: return None return self.deleted_devices[device_id] - def _async_get_device_id_from_index( - self, - index: str, - identifiers: set[tuple[str, ...]], - connections: set[tuple[str, str]] | None, - ) -> str | None: - """Check if device has previously been registered.""" - devices_index = self._devices_index[index] - for identifier in identifiers: - if identifier in devices_index[IDX_IDENTIFIERS]: - return devices_index[IDX_IDENTIFIERS][identifier] - if not connections: - return None - for connection in _normalize_connections(connections): - if connection in devices_index[IDX_CONNECTIONS]: - return devices_index[IDX_CONNECTIONS][connection] - return None - def _add_device(self, device: DeviceEntry | DeletedDeviceEntry) -> None: """Add a device and index it.""" if isinstance(device, DeletedDeviceEntry): - devices_index = self._devices_index[DELETED_DEVICE] + devices_index = self._deleted_index self.deleted_devices[device.id] = device else: - devices_index = self._devices_index[REGISTERED_DEVICE] + devices_index = self._registered_index self.devices[device.id] = device _add_device_to_index(devices_index, device) @@ -210,10 +210,10 @@ class DeviceRegistry: def _remove_device(self, device: DeviceEntry | DeletedDeviceEntry) -> None: """Remove a device and remove it from the index.""" if isinstance(device, DeletedDeviceEntry): - devices_index = self._devices_index[DELETED_DEVICE] + devices_index = self._deleted_index self.deleted_devices.pop(device.id) else: - devices_index = self._devices_index[REGISTERED_DEVICE] + devices_index = self._registered_index self.devices.pop(device.id) _remove_device_from_index(devices_index, device) @@ -222,24 +222,22 @@ class DeviceRegistry: """Update a device and the index.""" self.devices[new_device.id] = new_device - devices_index = self._devices_index[REGISTERED_DEVICE] + devices_index = self._registered_index _remove_device_from_index(devices_index, old_device) _add_device_to_index(devices_index, new_device) def _clear_index(self) -> None: """Clear the index.""" - self._devices_index = { - REGISTERED_DEVICE: {IDX_IDENTIFIERS: {}, IDX_CONNECTIONS: {}}, - DELETED_DEVICE: {IDX_IDENTIFIERS: {}, IDX_CONNECTIONS: {}}, - } + self._registered_index = _DeviceIndex(identifiers={}, connections={}) + self._deleted_index = _DeviceIndex(identifiers={}, connections={}) def _rebuild_index(self) -> None: """Create the index after loading devices.""" self._clear_index() for device in self.devices.values(): - _add_device_to_index(self._devices_index[REGISTERED_DEVICE], device) + _add_device_to_index(self._registered_index, device) for deleted_device in self.deleted_devices.values(): - _add_device_to_index(self._devices_index[DELETED_DEVICE], deleted_device) + _add_device_to_index(self._deleted_index, deleted_device) @callback def async_get_or_create( @@ -786,24 +784,24 @@ def _normalize_connections(connections: set[tuple[str, str]]) -> set[tuple[str, def _add_device_to_index( - devices_index: dict[str, dict[tuple[str, ...], str]], + devices_index: _DeviceIndex, device: DeviceEntry | DeletedDeviceEntry, ) -> None: """Add a device to the index.""" for identifier in device.identifiers: - devices_index[IDX_IDENTIFIERS][identifier] = device.id + devices_index.identifiers[identifier] = device.id for connection in device.connections: - devices_index[IDX_CONNECTIONS][connection] = device.id + devices_index.connections[connection] = device.id def _remove_device_from_index( - devices_index: dict[str, dict[tuple[str, ...], str]], + devices_index: _DeviceIndex, device: DeviceEntry | DeletedDeviceEntry, ) -> None: """Remove a device from the index.""" for identifier in device.identifiers: - if identifier in devices_index[IDX_IDENTIFIERS]: - del devices_index[IDX_IDENTIFIERS][identifier] + if identifier in devices_index.identifiers: + del devices_index.identifiers[identifier] for connection in device.connections: - if connection in devices_index[IDX_CONNECTIONS]: - del devices_index[IDX_CONNECTIONS][connection] + if connection in devices_index.connections: + del devices_index.connections[connection] From 29d72714f3786d959805ec23d259b09a00e1b936 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sun, 2 May 2021 00:37:19 +0200 Subject: [PATCH 094/852] Replace dict with DeviceInfo (#49950) * Replace dict with DeviceInfo * Clean up Co-authored-by: Martin Hjelmare --- homeassistant/components/adguard/__init__.py | 5 ++--- .../components/asuswrt/device_tracker.py | 3 ++- homeassistant/components/asuswrt/router.py | 3 ++- homeassistant/components/asuswrt/sensor.py | 3 ++- homeassistant/components/atag/__init__.py | 3 ++- homeassistant/components/awair/sensor.py | 3 ++- .../components/azure_devops/__init__.py | 5 ++--- .../components/bmw_connected_drive/__init__.py | 4 ++-- homeassistant/components/bsblan/climate.py | 3 ++- homeassistant/components/climacell/__init__.py | 3 ++- homeassistant/components/directv/__init__.py | 5 ++--- homeassistant/components/dsmr/sensor.py | 4 ++-- .../components/dynalite/dynalitebase.py | 4 ++-- homeassistant/components/emonitor/sensor.py | 3 ++- homeassistant/components/firmata/entity.py | 3 ++- homeassistant/components/flo/entity.py | 4 ++-- .../components/freebox/device_tracker.py | 3 ++- homeassistant/components/freebox/router.py | 3 ++- homeassistant/components/freebox/sensor.py | 5 +++-- homeassistant/components/freebox/switch.py | 4 ++-- .../components/fritz/device_tracker.py | 4 ++-- .../components/garmin_connect/sensor.py | 4 ++-- homeassistant/components/guardian/__init__.py | 3 ++- homeassistant/components/hassio/entity.py | 5 +++-- .../homematicip_cloud/alarm_control_panel.py | 4 ++-- .../homematicip_cloud/binary_sensor.py | 3 ++- .../components/homematicip_cloud/climate.py | 3 ++- .../homematicip_cloud/generic_entity.py | 4 ++-- homeassistant/components/iaqualink/__init__.py | 5 ++--- .../components/icloud/device_tracker.py | 3 ++- homeassistant/components/icloud/sensor.py | 3 ++- homeassistant/components/ipp/__init__.py | 4 ++-- .../components/kostal_plenticore/sensor.py | 5 +++-- homeassistant/components/litterrobot/entity.py | 3 ++- homeassistant/components/lyric/__init__.py | 4 ++-- .../components/minecraft_server/__init__.py | 5 ++--- homeassistant/components/motioneye/camera.py | 3 ++- homeassistant/components/mysensors/device.py | 5 ++--- homeassistant/components/notion/__init__.py | 3 ++- .../components/onewire/onewire_entities.py | 4 ++-- homeassistant/components/ovo_energy/__init__.py | 4 ++-- homeassistant/components/plugwise/gateway.py | 4 ++-- .../components/rainmachine/__init__.py | 3 ++- homeassistant/components/roku/__init__.py | 4 ++-- .../ruckus_unleashed/device_tracker.py | 3 ++- homeassistant/components/sharkiq/vacuum.py | 3 ++- homeassistant/components/sma/sensor.py | 3 ++- homeassistant/components/smarttub/entity.py | 3 ++- homeassistant/components/sonarr/__init__.py | 5 ++--- homeassistant/components/sonos/entity.py | 5 ++--- .../components/spotify/media_player.py | 4 ++-- .../components/synology_dsm/__init__.py | 5 +++-- homeassistant/components/synology_dsm/camera.py | 4 ++-- homeassistant/components/synology_dsm/switch.py | 4 ++-- homeassistant/components/toon/models.py | 17 ++++++++--------- homeassistant/components/twentemilieu/sensor.py | 6 +++--- homeassistant/components/unifi/switch.py | 3 ++- homeassistant/components/unifi/unifi_client.py | 3 ++- homeassistant/components/upnp/sensor.py | 3 ++- .../components/verisure/alarm_control_panel.py | 6 +++--- .../components/verisure/binary_sensor.py | 8 ++++---- homeassistant/components/verisure/camera.py | 6 +++--- homeassistant/components/verisure/lock.py | 6 +++--- homeassistant/components/verisure/sensor.py | 10 +++++----- homeassistant/components/verisure/switch.py | 6 +++--- homeassistant/components/vizio/media_player.py | 4 ++-- homeassistant/components/wemo/entity.py | 5 ++--- homeassistant/components/wled/__init__.py | 4 ++-- homeassistant/components/yeelight/__init__.py | 4 ++-- homeassistant/components/zha/entity.py | 2 +- 70 files changed, 159 insertions(+), 138 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index b848dcefc8c..0a4a79b65f5 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError import voluptuous as vol @@ -33,7 +32,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity _LOGGER = logging.getLogger(__name__) @@ -194,7 +193,7 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity): """Defines a AdGuard Home device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this AdGuard Home instance.""" return { "identifiers": { diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index abaa6c1965d..a0c7ec0e27a 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .const import DATA_ASUSWRT, DOMAIN from .router import AsusWrtRouter @@ -105,7 +106,7 @@ class AsusWrtDevice(ScannerEntity): return self._device.mac @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" data = { "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 9fc7ce41d05..f82bf74e4a3 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -24,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -367,7 +368,7 @@ class AsusWrtRouter: return req_reload @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "identifiers": {(DOMAIN, "AsusWRT")}, diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 7a3ffccc00b..6ec077620f6 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -167,6 +168,6 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): return {"hostname": self._router.host} @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return self._router.device_info diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 710685f91ae..e6347563bc2 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -11,6 +11,7 @@ from homeassistant.components.water_heater import DOMAIN as WATER_HEATER from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -77,7 +78,7 @@ class AtagEntity(CoordinatorEntity): self._name = DOMAIN.title() @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return info for device registry.""" device = self.coordinator.data.id version = self.coordinator.data.apiversion diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index ade6ddccc8a..968587c3b10 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_ACCESS from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -209,7 +210,7 @@ class AwairSensor(CoordinatorEntity, SensorEntity): return attrs @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Device information.""" info = { "identifiers": {(DOMAIN, self._device.uuid)}, diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 017b1246503..ba9020e3e88 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from aioazuredevops.client import DevOpsClient import aiohttp @@ -17,7 +16,7 @@ from homeassistant.components.azure_devops.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity _LOGGER = logging.getLogger(__name__) @@ -103,7 +102,7 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity): """Defines a Azure DevOps device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this Azure DevOps instance.""" return { "identifiers": { diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index d513ae7c460..79461082acd 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import track_utc_time_change from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -315,7 +315,7 @@ class BMWConnectedDriveBaseEntity(Entity): } @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return info for device registry.""" return { "identifiers": {(DOMAIN, self._vehicle.vin)}, diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 5ab63fb4036..7533e7e07f9 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -27,6 +27,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -231,7 +232,7 @@ class BSBLanClimate(ClimateEntity): self._temperature_unit = state.current_temperature.unit @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this BSBLan device.""" return { ATTR_IDENTIFIERS: {(DOMAIN, self._info.device_identification)}, diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 81198f8d98c..85a23ef10a9 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -27,6 +27,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -358,7 +359,7 @@ class ClimaCellEntity(CoordinatorEntity): return ATTRIBUTION @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device registry information.""" return { "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])}, diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 45f4eeeda37..b79a55394d5 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import timedelta -from typing import Any from directv import DIRECTV, DIRECTVError @@ -12,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( ATTR_IDENTIFIERS, @@ -72,7 +71,7 @@ class DIRECTVEntity(Entity): return self._name @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this DirecTV receiver.""" return { ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 3885302329a..237f3b2f929 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -7,7 +7,6 @@ from contextlib import suppress from datetime import timedelta from functools import partial import logging -from typing import Any from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader @@ -24,6 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import Throttle from .const import ( @@ -362,7 +362,7 @@ class DSMREntity(SensorEntity): return self._unique_id @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "identifiers": {(DOMAIN, self._device_serial)}, diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index 2cc28002a2c..371f2aa8508 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -7,7 +7,7 @@ from homeassistant.components.dynalite.bridge import DynaliteBridge from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN, LOGGER @@ -60,7 +60,7 @@ class DynaliteBase(Entity): return self._device.available @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Device info for this entity.""" return { "identifiers": {(DOMAIN, self._device.unique_id)}, diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index 3b075f7cbaa..d06e77f74e7 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -5,6 +5,7 @@ from aioemonitor.monitor import EmonitorChannel from homeassistant.components.sensor import DEVICE_CLASS_POWER, SensorEntity from homeassistant.const import POWER_WATT from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -98,7 +99,7 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): return self.coordinator.data.network.mac_address @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return info about the emonitor device.""" return { "name": name_short_mac(self.mac_address[-6:]), diff --git a/homeassistant/components/firmata/entity.py b/homeassistant/components/firmata/entity.py index 8f843d29272..7a576c09cd1 100644 --- a/homeassistant/components/firmata/entity.py +++ b/homeassistant/components/firmata/entity.py @@ -2,6 +2,7 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import DeviceInfo from .board import FirmataPinType from .const import DOMAIN, FIRMATA_MANUFACTURER @@ -16,7 +17,7 @@ class FirmataEntity: self._api = api @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return device info.""" return { "connections": {}, diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 878c4188815..26aef603a22 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as FLO_DOMAIN from .device import FloDeviceDataUpdateCoordinator @@ -37,7 +37,7 @@ class FloEntity(Entity): return self._unique_id @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return { "identifiers": {(FLO_DOMAIN, self._device.id)}, diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index d2814a1c126..2ad262dd2bd 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .const import DEFAULT_DEVICE_NAME, DEVICE_ICONS, DOMAIN from .router import FreeboxRouter @@ -111,7 +112,7 @@ class FreeboxDevice(ScannerEntity): return self._attrs @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 3f5a4e53528..9438b3eadc6 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -17,6 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify @@ -180,7 +181,7 @@ class FreeboxRouter: self._api = None @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 8f097b2d73a..8c4e611827e 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo import homeassistant.util.dt as dt_util from .const import ( @@ -130,7 +131,7 @@ class FreeboxSensor(SensorEntity): return self._device_class @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return self._router.device_info @@ -208,7 +209,7 @@ class FreeboxDiskSensor(FreeboxSensor): self._unique_id = f"{self._router.mac} {sensor_type} {self._disk['id']} {self._partition['id']}" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "identifiers": {(DOMAIN, self._disk["id"])}, diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index ebe573be9ed..a07aed4da77 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -2,13 +2,13 @@ from __future__ import annotations import logging -from typing import Any from freebox_api.exceptions import InsufficientPermissionsError from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN from .router import FreeboxRouter @@ -50,7 +50,7 @@ class FreeboxWifiSwitch(SwitchEntity): return self._state @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return self._router.device_info diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 8ccce78964f..23657429f68 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any import voluptuous as vol @@ -18,6 +17,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from .common import FritzBoxTools @@ -154,7 +154,7 @@ class FritzBoxTracker(ScannerEntity): return SOURCE_TYPE_ROUTER @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 5cabb96c8e9..0d946d5e88e 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from garminconnect import ( GarminConnectAuthenticationError, @@ -14,6 +13,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from .alarm_util import calculate_next_active_alarms from .const import ATTRIBUTION, DOMAIN, GARMIN_ENTITY_LIST @@ -138,7 +138,7 @@ class GarminConnectSensor(SensorEntity): return attributes @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information.""" return { "identifiers": {(DOMAIN, self._unique_id)}, diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 6c76da3373d..89e038b047e 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -232,7 +233,7 @@ class GuardianEntity(CoordinatorEntity): return self._device_class @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" return self._device_info diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 5f35235bb5d..4885ba8979f 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any from homeassistant.const import ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import DOMAIN, HassioDataUpdateCoordinator @@ -49,7 +50,7 @@ class HassioAddonEntity(CoordinatorEntity): return f"{self.addon_slug}_{self.attribute_name}" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return {"identifiers": {(DOMAIN, self.addon_slug)}} @@ -90,6 +91,6 @@ class HassioOSEntity(CoordinatorEntity): return f"home_assistant_os_{self.attribute_name}" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return {"identifiers": {(DOMAIN, "OS")}} diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index f4776d52743..87a8056b4b6 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from homematicip.functionalHomes import SecurityAndAlarmHome @@ -19,6 +18,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN as HMIPC_DOMAIN from .hap import HomematicipHAP @@ -45,7 +45,7 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): _LOGGER.info("Setting up %s", self.name) @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return { "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")}, diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 4f15a8c7200..673dd6e9ea3 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -45,6 +45,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -168,7 +169,7 @@ class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEnt return name if not self._home.name else f"{self._home.name} {name}" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" # Adds a sensor to the existing HAP device return { diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 05234cd43a6..7ba90e0a9e4 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -27,6 +27,7 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -73,7 +74,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): self._simple_heating = self._first_radiator_thermostat @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return { "identifiers": {(HMIPC_DOMAIN, self._device.id)}, diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 856e47a1dee..b9dd46d49d7 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -10,7 +10,7 @@ from homematicip.aio.group import AsyncGroup from homeassistant.const import ATTR_ID from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as HMIPC_DOMAIN from .hap import HomematicipHAP @@ -92,7 +92,7 @@ class HomematicipGenericEntity(Entity): _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" # Only physical devices should be HA devices. if isinstance(self._device, AsyncDevice): diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 37dc0e39f3d..895733c5f35 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from functools import wraps import logging -from typing import Any import aiohttp.client_exceptions from iaqualink import ( @@ -35,7 +34,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -234,7 +233,7 @@ class AqualinkEntity(Entity): return self.dev.system.online @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "identifiers": {(DOMAIN, self.unique_id)}, diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 0615d6fcc7f..808689bc00a 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -8,6 +8,7 @@ from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .account import IcloudAccount, IcloudDevice from .const import ( @@ -112,7 +113,7 @@ class IcloudTrackerEntity(TrackerEntity): return self._device.extra_state_attributes @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "identifiers": {(DOMAIN, self._device.unique_id)}, diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 7c13171688e..6a7304dfc9d 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -8,6 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.icon import icon_for_battery_level from .account import IcloudAccount, IcloudDevice @@ -97,7 +98,7 @@ class IcloudDeviceBatterySensor(SensorEntity): return self._device.extra_state_attributes @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "identifiers": {(DOMAIN, self._device.unique_id)}, diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index d4ae0e0e1cb..198fdaa602a 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any from pyipp import IPP, IPPError, Printer as IPPPrinter @@ -18,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -145,7 +145,7 @@ class IPPEntity(CoordinatorEntity): return self._enabled_default @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this IPP device.""" if self._device_id is None: return None diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index f9d25f65d90..717dfacbfdf 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -113,7 +114,7 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): sensor_name: str, sensor_data: dict[str, Any], formatter: Callable[[str], Any], - device_info: dict[str, Any], + device_info: DeviceInfo, ): """Create a new Sensor Entity for Plenticore process data.""" super().__init__(coordinator) @@ -149,7 +150,7 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): await super().async_will_remove_from_hass() @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device info.""" return self._device_info diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 89a8c80a0df..015e781c38a 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -10,6 +10,7 @@ from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.dt as dt_util @@ -43,7 +44,7 @@ class LitterRobotEntity(CoordinatorEntity): return f"{self.robot.serial}-{self.entity_type}" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information for a Litter-Robot.""" return { "identifiers": {(DOMAIN, self.robot.serial)}, diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 9f6d38ad4e7..e958567940a 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any from aiohttp.client_exceptions import ClientResponseError from aiolyric import Lyric @@ -23,6 +22,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -183,7 +183,7 @@ class LyricDeviceEntity(LyricEntity): """Defines a Honeywell Lyric device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this Honeywell Lyric instance.""" return { "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_id)}, diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 5d507006b05..06fe466e8a4 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import Any from mcstatus.server import MinecraftServer as MCStatus @@ -14,7 +13,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -247,7 +246,7 @@ class MinecraftServerEntity(Entity): return self._unique_id @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information.""" return self._device_info diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index 77ea8d4d5e7..80c51753858 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -30,6 +30,7 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -203,6 +204,6 @@ class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity): return self._motion_detection_enabled @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return {"identifiers": {self._device_identifier}} diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 4e770f70bf0..b2e03dd037f 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -3,7 +3,6 @@ from __future__ import annotations from functools import partial import logging -from typing import Any from mysensors import BaseAsyncGateway, Sensor from mysensors.sensor import ChildSensor @@ -11,7 +10,7 @@ from mysensors.sensor import ChildSensor from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( CHILD_CALLBACK, @@ -109,7 +108,7 @@ class MySensorsDevice: return f"{self.gateway_id}-{self.node_id}-{self.child_id}-{self.value_type}" @property - def device_info(self) -> dict[str, Any] | None: + def device_info(self) -> DeviceInfo: """Return a dict that allows home assistant to puzzle all entities belonging to a node together.""" return { "identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")}, diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index edadca64ec4..a6dfe7e73a9 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -14,6 +14,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -175,7 +176,7 @@ class NotionEntity(CoordinatorEntity): return self._attrs @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" bridge = self.coordinator.data["bridges"].get(self._bridge_id, {}) sensor = self.coordinator.data["sensors"][self._sensor_id] diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index 10c2b0c24a7..2581958eeb5 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -6,7 +6,7 @@ from typing import Any from pyownet import protocol -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( SENSOR_TYPE_COUNT, @@ -65,7 +65,7 @@ class OneWireBaseEntity(Entity): return self._unique_id @property - def device_info(self) -> dict[str, Any] | None: + def device_info(self) -> DeviceInfo | None: """Return device specific attributes.""" return self._device_info diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index d94e337e3d3..18414db7292 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import Any import aiohttp import async_timeout @@ -14,6 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -136,7 +136,7 @@ class OVOEnergyDeviceEntity(OVOEnergyEntity): """Defines a OVO Energy device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this OVO Energy instance.""" return { "identifiers": {(DOMAIN, self._client.account_id)}, diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index a6d8960edf2..41e3caacbff 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging -from typing import Any import async_timeout from plugwise.exceptions import ( @@ -26,6 +25,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -190,7 +190,7 @@ class SmileGateway(CoordinatorEntity): return self._name @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" device_information = { "identifiers": {(DOMAIN, self._dev_id)}, diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 4e709e319f6..8c72922699e 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -201,7 +202,7 @@ class RainMachineEntity(CoordinatorEntity): return self._device_class @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" return { "identifiers": {(DOMAIN, self._controller.mac)}, diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 72ecd0a8d05..6b7f3237d1e 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any from rokuecp import Roku, RokuConnectionError, RokuError from rokuecp.models import Device @@ -15,6 +14,7 @@ from homeassistant.const import ATTR_NAME, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -133,7 +133,7 @@ class RokuEntity(CoordinatorEntity): return self._name @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this Roku device.""" if self._device_id is None: return None diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index a5bc266f045..a776930b5ac 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -7,6 +7,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -117,7 +118,7 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity): return SOURCE_TYPE_ROUTER @property - def device_info(self) -> dict | None: + def device_info(self) -> DeviceInfo | None: """Return the device information.""" if self.is_connected: return { diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index dd6e6766706..09dc4ec3efb 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -23,6 +23,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, SHARK @@ -118,7 +119,7 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): return self.sharkiq.oem_model_number @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Device info dictionary.""" return { "identifiers": {(DOMAIN, self.serial_number)}, diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 4e950651ab0..ad530367904 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -182,7 +183,7 @@ class SMAsensor(CoordinatorEntity, SensorEntity): ) @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "identifiers": {(DOMAIN, self._config_entry_unique_id)}, diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index 7cdd04ac173..58f400597b3 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -3,6 +3,7 @@ import logging import smarttub +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -36,7 +37,7 @@ class SmartTubEntity(CoordinatorEntity): return f"{self.spa.id}-{self._entity_name}" @property - def device_info(self) -> str: + def device_info(self) -> DeviceInfo: """Return device info.""" return { "identifiers": {(DOMAIN, self.spa.id)}, diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 3299842c48c..bf5b2456b66 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any from sonarr import Sonarr, SonarrAccessRestricted, SonarrError @@ -19,7 +18,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( ATTR_IDENTIFIERS, @@ -139,7 +138,7 @@ class SonarrEntity(Entity): return self._enabled_default @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo | None: """Return device information about the application.""" if self._device_id is None: return None diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 26ee74dff2c..f7319e483d3 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from pysonos.core import SoCo @@ -11,7 +10,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DOMAIN, @@ -56,7 +55,7 @@ class SonosEntity(Entity): return self.speaker.soco @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return information about the device.""" return { "identifiers": {(DOMAIN, self.soco.uid)}, diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index dc7963de690..a30868fe913 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -5,7 +5,6 @@ from asyncio import run_coroutine_threadsafe import datetime as dt from datetime import timedelta import logging -from typing import Any import requests from spotipy import Spotify, SpotifyException @@ -54,6 +53,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utc_from_timestamp @@ -272,7 +272,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return self._id @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" if self._me is not None: model = self._me["product"] diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 058c810b157..c4d0d822d19 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -40,6 +40,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -620,7 +621,7 @@ class SynologyDSMBaseEntity(CoordinatorEntity): return {ATTR_ATTRIBUTION: ATTRIBUTION} @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "identifiers": {(DOMAIN, self._api.information.serial)}, @@ -690,7 +691,7 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): return bool(self._api.storage) @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 80cf70de8a9..6183125ee8f 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.exceptions import ( @@ -13,6 +12,7 @@ from synology_dsm.exceptions import ( from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi, SynologyDSMBaseEntity @@ -81,7 +81,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): return self.coordinator.data["cameras"][self._camera_id] @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "identifiers": { diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 51736663d50..817c38674d5 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -2,13 +2,13 @@ from __future__ import annotations import logging -from typing import Any from synology_dsm.api.surveillance_station import SynoSurveillanceStation from homeassistant.components.switch import ToggleEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi, SynologyDSMBaseEntity @@ -97,7 +97,7 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity): return bool(self._api.surveillance_station) @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "identifiers": { diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index 8aee2fe27e1..18b44db45a8 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -1,8 +1,7 @@ """DataUpdate Coordinator, and base Entity and Device models for Toon.""" from __future__ import annotations -from typing import Any - +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -47,7 +46,7 @@ class ToonDisplayDeviceEntity(ToonEntity): """Defines a Toon display device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this thermostat.""" agreement = self.coordinator.data.agreement model = agreement.display_hardware_version.rpartition("/")[0] @@ -65,7 +64,7 @@ class ToonElectricityMeterDeviceEntity(ToonEntity): """Defines a Electricity Meter device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { @@ -79,7 +78,7 @@ class ToonGasMeterDeviceEntity(ToonEntity): """Defines a Gas Meter device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { @@ -93,7 +92,7 @@ class ToonWaterMeterDeviceEntity(ToonEntity): """Defines a Water Meter device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { @@ -107,7 +106,7 @@ class ToonSolarDeviceEntity(ToonEntity): """Defines a Solar Device device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { @@ -121,7 +120,7 @@ class ToonBoilerModuleDeviceEntity(ToonEntity): """Defines a Boiler Module device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { @@ -136,7 +135,7 @@ class ToonBoilerDeviceEntity(ToonEntity): """Defines a Boiler device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index ad552a4b341..29a3012c29f 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -1,7 +1,7 @@ """Support for Twente Milieu sensors.""" from __future__ import annotations -from typing import Any, Callable +from typing import Callable from twentemilieu import ( WASTE_TYPE_NON_RECYCLABLE, @@ -18,7 +18,7 @@ from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DATA_UPDATE, DOMAIN @@ -147,7 +147,7 @@ class TwenteMilieuSensor(SensorEntity): self._state = next_pickup.date().isoformat() @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about Twente Milieu.""" return { "identifiers": {(DOMAIN, self._unique_id)}, diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 59e8c9fa149..e419e2b4410 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -18,6 +18,7 @@ from aiounifi.events import ( from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.restore_state import RestoreEntity @@ -362,7 +363,7 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity): await self.remove_item({self.key}) @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return a service description for device registry.""" return { "identifiers": {(DOMAIN, f"unifi_controller_{self._item.site_id}")}, diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py index 9710f3ace29..3340616dada 100644 --- a/homeassistant/components/unifi/unifi_client.py +++ b/homeassistant/components/unifi/unifi_client.py @@ -1,5 +1,6 @@ """Base class for UniFi clients.""" from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo from .unifi_entity_base import UniFiBase @@ -44,7 +45,7 @@ class UniFiClient(UniFiBase): return self.controller.available @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return a client description for device registry.""" return { "connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}, diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 1c298947356..54744490a86 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -165,7 +166,7 @@ class UpnpSensor(CoordinatorEntity, SensorEntity): return self._sensor_type["unit"] @property - def device_info(self) -> Mapping[str, Any]: + def device_info(self) -> DeviceInfo: """Get device info.""" return { "connections": {(dr.CONNECTION_UPNP, self._device.udn)}, diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index b84affe9e8d..0367a8d81b1 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable -from typing import Any, Callable +from typing import Callable from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -15,7 +15,7 @@ from homeassistant.components.alarm_control_panel.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ALARM_STATE_TO_HA, CONF_GIID, DOMAIN, LOGGER @@ -50,7 +50,7 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): return self.coordinator.entry.data[CONF_GIID] @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" return { "name": "Verisure Alarm", diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 758636bee98..7f61eb194fa 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Any, Callable +from typing import Callable from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DOMAIN @@ -59,7 +59,7 @@ class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): return f"{self.serial_number}_door_window" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["door_window"][self.serial_number]["area"] return { @@ -108,7 +108,7 @@ class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): return f"{self.coordinator.entry.data[CONF_GIID]}_ethernet" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" return { "name": "Verisure Alarm", diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index a4442d2ae4b..b23551ddd65 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Iterable import errno import os -from typing import Any, Callable +from typing import Callable from verisure import Error as VerisureError @@ -12,7 +12,7 @@ from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import current_platform from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -73,7 +73,7 @@ class VerisureSmartcam(CoordinatorEntity, Camera): return self.serial_number @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["cameras"][self.serial_number]["area"] return { diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index bcd5ac214ee..ee12fcca2cc 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable -from typing import Any, Callable +from typing import Callable from verisure import Error as VerisureError @@ -11,7 +11,7 @@ from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import current_platform from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -81,7 +81,7 @@ class VerisureDoorlock(CoordinatorEntity, LockEntity): return self.serial_number @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["locks"][self.serial_number]["area"] return { diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 72b061bd628..ac79cc16091 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Any, Callable +from typing import Callable from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, @@ -12,7 +12,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DEVICE_TYPE_NAME, DOMAIN @@ -76,7 +76,7 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" device_type = self.coordinator.data["climate"][self.serial_number].get( "deviceType" @@ -140,7 +140,7 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): return DEVICE_CLASS_HUMIDITY @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" device_type = self.coordinator.data["climate"][self.serial_number].get( "deviceType" @@ -199,7 +199,7 @@ class VerisureMouseDetection(CoordinatorEntity, SensorEntity): return f"{self.serial_number}_mice" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["mice"][self.serial_number]["area"] return { diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 1284ed5fde4..85be983e44a 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -3,12 +3,12 @@ from __future__ import annotations from collections.abc import Iterable from time import monotonic -from typing import Any, Callable +from typing import Callable from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DOMAIN @@ -53,7 +53,7 @@ class VerisureSmartplug(CoordinatorEntity, SwitchEntity): return self.serial_number @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["smart_plugs"][self.serial_number]["area"] return { diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 57d770b26ae..042347a975e 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -33,7 +33,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -423,7 +423,7 @@ class VizioDevice(MediaPlayerEntity): return self._config_entry.unique_id @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device registry information.""" return { "identifiers": {(DOMAIN, self._config_entry.unique_id)}, diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index a315f9daf02..810ad74b953 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -5,13 +5,12 @@ import asyncio from collections.abc import Generator import contextlib import logging -from typing import Any import async_timeout from pywemo import WeMoDevice from pywemo.exceptions import ActionException -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as WEMO_DOMAIN @@ -127,7 +126,7 @@ class WemoSubscriptionEntity(WemoEntity): return self.wemo.serialnumber @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "name": self.name, diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 8c8c6d887e7..b875f65bf42 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any from wled import WLED, Device as WLEDDevice, WLEDConnectionError, WLEDError @@ -14,6 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -165,7 +165,7 @@ class WLEDDeviceEntity(WLEDEntity): """Defines a WLED device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this WLED device.""" return { ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index a51323b516e..845e8e5711a 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -553,7 +553,7 @@ class YeelightEntity(Entity): return self._unique_id @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "identifiers": {(DOMAIN, self._unique_id)}, diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 2e5fe935435..860183a79b6 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -83,7 +83,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): return self._should_poll @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> entity.DeviceInfo: """Return a device description for device registry.""" zha_device_info = self._zha_device.device_info ieee = zha_device_info["ieee"] From 91e41a0cc216cb141e57efeea9cb370f225b9fe7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 1 May 2021 17:56:50 -0600 Subject: [PATCH 095/852] Fix KeyError in IQVIA (#49968) --- homeassistant/components/iqvia/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 65914bb1756..f8ccf3c7e29 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -38,6 +38,7 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass, entry): """Set up IQVIA as config entry.""" + hass.data.setdefault(DOMAIN, {}) coordinators = {} if not entry.unique_id: From 796f9cad1f95af6b73caab6fb709b12fc00dcc17 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 2 May 2021 00:04:24 +0000 Subject: [PATCH 096/852] [ci skip] Translation update --- .../components/fritz/translations/fr.json | 37 +++++++++++++++++++ .../components/goalzero/translations/it.json | 2 +- .../google_travel_time/translations/fr.json | 1 + .../google_travel_time/translations/it.json | 1 + .../components/mutesync/translations/fr.json | 16 ++++++++ .../components/omnilogic/translations/fr.json | 1 + .../components/omnilogic/translations/it.json | 1 + .../waze_travel_time/translations/fr.json | 1 + .../waze_travel_time/translations/it.json | 1 + .../components/zha/translations/ca.json | 13 +++++++ .../components/zha/translations/et.json | 13 +++++++ .../components/zha/translations/fr.json | 13 +++++++ .../components/zha/translations/it.json | 13 +++++++ .../components/zha/translations/ru.json | 13 +++++++ 14 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fritz/translations/fr.json create mode 100644 homeassistant/components/mutesync/translations/fr.json diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json new file mode 100644 index 00000000000..32e7e3694f9 --- /dev/null +++ b/homeassistant/components/fritz/translations/fr.json @@ -0,0 +1,37 @@ +{ + "config": { + "error": { + "connection_error": "Erreur de connexion", + "invalid_auth": "Authentification invalide" + }, + "flow_title": "FRITZ!Box Tools : {name}", + "step": { + "confirm": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "FRITZ!Box d\u00e9couvert : {name}\n\nConfiguration de FRITZ!Box Tools pour contr\u00f4ler votre {name}", + "title": "Configuration FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Mettre \u00e0 jour les informations d'identification FRITZ!Box Tools pour : {host}.", + "title": "Mise \u00e0 jour de FRITZ!Box Tools - informations d'identification" + }, + "start_config": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "description": "Configuration de FRITZ!Box Tools pour contr\u00f4ler votre FRITZ!Box.\nMinimum requis: nom d'utilisateur, mot de passe.", + "title": "Configuration FRITZ!Box Tools - obligatoire" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/it.json b/homeassistant/components/goalzero/translations/it.json index 24f04a0bafe..98b682dd76b 100644 --- a/homeassistant/components/goalzero/translations/it.json +++ b/homeassistant/components/goalzero/translations/it.json @@ -14,7 +14,7 @@ "host": "Host", "name": "Nome" }, - "description": "Innanzitutto, devi scaricare l'app Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSegui le istruzioni per connettere il tuo Yeti alla tua rete Wifi. Quindi ottieni l'ip host dal tuo router. Il DHCP deve essere configurato nelle impostazioni del router affinch\u00e9 assicuri che l'ip host non cambi. Fare riferimento al manuale utente del router.", + "description": "Innanzitutto, devi scaricare l'app Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegui le istruzioni per connettere il tuo Yeti alla tua rete Wifi. La prenotazione DHCP per il dispositivo deve essere configurata nelle impostazioni del router per assicurarsi che l'IP host non cambi. Fare riferimento al manuale utente del router.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/google_travel_time/translations/fr.json b/homeassistant/components/google_travel_time/translations/fr.json index 8a4ecc6ac83..d9c19d0d793 100644 --- a/homeassistant/components/google_travel_time/translations/fr.json +++ b/homeassistant/components/google_travel_time/translations/fr.json @@ -11,6 +11,7 @@ "data": { "api_key": "common::config_flow::data::api_key", "destination": "Destination", + "name": "Nom", "origin": "Origine" }, "description": "Lorsque vous sp\u00e9cifiez l'origine et la destination, vous pouvez fournir un ou plusieurs emplacements s\u00e9par\u00e9s par le caract\u00e8re de tuyau, sous la forme d'une adresse, de coordonn\u00e9es de latitude / longitude ou d'un identifiant de lieu Google. Lorsque vous sp\u00e9cifiez l'emplacement \u00e0 l'aide d'un identifiant de lieu Google, l'identifiant doit \u00eatre pr\u00e9c\u00e9d\u00e9 de `place_id:`." diff --git a/homeassistant/components/google_travel_time/translations/it.json b/homeassistant/components/google_travel_time/translations/it.json index 426e7f96c3c..aa109708738 100644 --- a/homeassistant/components/google_travel_time/translations/it.json +++ b/homeassistant/components/google_travel_time/translations/it.json @@ -11,6 +11,7 @@ "data": { "api_key": "Chiave API", "destination": "Destinazione", + "name": "Nome", "origin": "Origine" }, "description": "Quando specifichi l'origine e la destinazione, puoi fornire una o pi\u00f9 posizioni separate dal carattere barra verticale, sotto forma di un indirizzo, coordinate di latitudine/longitudine o un ID luogo di Google. Quando si specifica la posizione utilizzando un ID luogo di Google, l'ID deve essere preceduto da \"place_id:\"." diff --git a/homeassistant/components/mutesync/translations/fr.json b/homeassistant/components/mutesync/translations/fr.json new file mode 100644 index 00000000000..7a292eeeeae --- /dev/null +++ b/homeassistant/components/mutesync/translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Erreur de connexion", + "invalid_auth": "Activer l'authentification dans Pr\u00e9f\u00e9rences > Authentification de m\u00fctesync", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/fr.json b/homeassistant/components/omnilogic/translations/fr.json index ab73af0d27a..4a8b293aebf 100644 --- a/homeassistant/components/omnilogic/translations/fr.json +++ b/homeassistant/components/omnilogic/translations/fr.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "D\u00e9calage pH (positif ou n\u00e9gatif)", "polling_interval": "Intervalle d'interrogation (en secondes)" } } diff --git a/homeassistant/components/omnilogic/translations/it.json b/homeassistant/components/omnilogic/translations/it.json index ceff0e81258..28a9a1a0425 100644 --- a/homeassistant/components/omnilogic/translations/it.json +++ b/homeassistant/components/omnilogic/translations/it.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "Scostamento del pH (positivo o negativo)", "polling_interval": "Intervallo di scansione (in secondi)" } } diff --git a/homeassistant/components/waze_travel_time/translations/fr.json b/homeassistant/components/waze_travel_time/translations/fr.json index 8b977e76d08..e0039ef4b14 100644 --- a/homeassistant/components/waze_travel_time/translations/fr.json +++ b/homeassistant/components/waze_travel_time/translations/fr.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Destination", + "name": "Nom", "origin": "Point de d\u00e9part", "region": "R\u00e9gion" }, diff --git a/homeassistant/components/waze_travel_time/translations/it.json b/homeassistant/components/waze_travel_time/translations/it.json index ce109b3751c..bfbe94c2a23 100644 --- a/homeassistant/components/waze_travel_time/translations/it.json +++ b/homeassistant/components/waze_travel_time/translations/it.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Destinazione", + "name": "Nome", "origin": "Origine", "region": "Area geografica" }, diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 20eee443960..ce0d90564da 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Codi necessari per activar accions", + "alarm_failed_tries": "Nombre d'intents de codi erronis consecutius per disparar una alarma", + "alarm_master_code": "Codi mestre per als panells de control d'alarma", + "title": "Opcions del panell de control d'alarma" + }, + "zha_options": { + "default_light_transition": "Temps de transici\u00f3 predeterminat (segons)", + "enable_identify_on_join": "Activa l'efecte d'identificaci\u00f3 quan els dispositius s'uneixin a la xarxa", + "title": "Opcions globals" + } + }, "device_automation": { "action_type": { "squawk": "Squawk", diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index d03300f9971..3a0ac9dbb29 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Valvestamise kood", + "alarm_failed_tries": "Mitu j\u00e4rjestikust eba\u00f5nnestunud koodi sisestamist h\u00e4ire k\u00e4ivitamiseks", + "alarm_master_code": "Valvekeskuse juhtpaneel(ide) \u00fclemkood", + "title": "Valvekeskuse juhtpaneeli s\u00e4tted" + }, + "zha_options": { + "default_light_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)", + "enable_identify_on_join": "Luba tuvastamine kui seadmed liituvad v\u00f5rguga", + "title": "\u00dcldised valikud" + } + }, "device_automation": { "action_type": { "squawk": "Pr\u00e4\u00e4ksata", diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 9e35ef9a541..75ba26ca809 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Code requis pour les actions d'armement", + "alarm_failed_tries": "Le nombre de codes erron\u00e9s cons\u00e9cutifs pour d\u00e9clencher l'alarme", + "alarm_master_code": "Code principal pour le(s) panneau(x) de contr\u00f4le d'alarme", + "title": "Options du panneau de contr\u00f4le d'alarme" + }, + "zha_options": { + "default_light_transition": "Temps de transition de la lumi\u00e8re par d\u00e9faut (en secondes)", + "enable_identify_on_join": "Activer l'effet d'identification quand les appareils rejoignent le r\u00e9seau", + "title": "Options g\u00e9n\u00e9rales" + } + }, "device_automation": { "action_type": { "squawk": "Hurlement", diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index e97828d8e2a..4a63866e7a3 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Codice necessario per le azioni di armamento", + "alarm_failed_tries": "Il numero di inserimenti consecutivi di codici falliti per attivare un allarme", + "alarm_master_code": "Codice principale per i pannelli di controllo degli allarmi", + "title": "Opzioni del pannello di controllo degli allarmi" + }, + "zha_options": { + "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", + "enable_identify_on_join": "Abilita l'effetto di identificazione quando i dispositivi si uniscono alla rete", + "title": "Opzioni globali" + } + }, "device_automation": { "action_type": { "squawk": "Strillare", diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index e4084d0b2f6..3b66c9da996 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", + "alarm_failed_tries": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043d\u0435\u0443\u0434\u0430\u0447\u043d\u044b\u0445 \u0432\u0432\u043e\u0434\u043e\u0432 \u043a\u043e\u0434\u0430, \u0434\u043b\u044f \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043d\u0438\u044f \u0442\u0440\u0435\u0432\u043e\u0433\u0438", + "alarm_master_code": "\u041c\u0430\u0441\u0442\u0435\u0440-\u043a\u043e\u0434 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u043d\u0435\u043b\u0435\u0439", + "title": "\u041e\u043f\u0446\u0438\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" + }, + "zha_options": { + "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", + "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + }, "device_automation": { "action_type": { "squawk": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u043d\u0434\u0435\u0440", From 3546ff2da2c52ff10e41c4bd3985f54c6c9a62bd Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 1 May 2021 17:04:37 -0700 Subject: [PATCH 097/852] Bump Tesla dependency teslajsonpy to 0.18.3 (#49939) Co-authored-by: J. Nick Koston --- homeassistant/components/tesla/__init__.py | 36 ++++++-- homeassistant/components/tesla/config_flow.py | 29 ++++--- homeassistant/components/tesla/const.py | 1 + homeassistant/components/tesla/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tesla/test_config_flow.py | 84 ++++++++++++++----- 7 files changed, 118 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 80cefaa9c56..2b0373dba33 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -1,9 +1,11 @@ """Support for Tesla cars.""" +import asyncio from collections import defaultdict from datetime import timedelta import logging import async_timeout +import httpx from teslajsonpy import Controller as TeslaAPI from teslajsonpy.exceptions import IncompleteCredentials, TeslaException import voluptuous as vol @@ -17,11 +19,13 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME, + EVENT_HOMEASSISTANT_CLOSE, HTTP_UNAUTHORIZED, ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.httpx_client import SERVER_SOFTWARE, USER_AGENT from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -31,6 +35,7 @@ from homeassistant.util import slugify from .config_flow import CannotConnect, InvalidAuth, validate_input from .const import ( + CONF_EXPIRATION, CONF_WAKE_ON_START, DATA_LISTENER, DEFAULT_SCAN_INTERVAL, @@ -113,6 +118,7 @@ async def async_setup(hass, base_config): CONF_PASSWORD: password, CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN], CONF_TOKEN: info[CONF_TOKEN], + CONF_EXPIRATION: info[CONF_EXPIRATION], }, options={CONF_SCAN_INTERVAL: scan_interval}, ) @@ -134,7 +140,7 @@ async def async_setup_entry(hass, config_entry): hass.data.setdefault(DOMAIN, {}) config = config_entry.data # Because users can have multiple accounts, we always create a new session so they have separate cookies - websession = aiohttp_client.async_create_clientsession(hass) + async_client = httpx.AsyncClient(headers={USER_AGENT: SERVER_SOFTWARE}) email = config_entry.title if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]: scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL] @@ -144,27 +150,45 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN].pop(email) try: controller = TeslaAPI( - websession, + async_client, email=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), refresh_token=config[CONF_TOKEN], access_token=config[CONF_ACCESS_TOKEN], + expiration=config.get(CONF_EXPIRATION, 0), update_interval=config_entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), ) - (refresh_token, access_token) = await controller.connect( + result = await controller.connect( wake_if_asleep=config_entry.options.get( CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START ) ) + refresh_token = result["refresh_token"] + access_token = result["access_token"] except IncompleteCredentials as ex: + await async_client.aclose() raise ConfigEntryAuthFailed from ex except TeslaException as ex: + await async_client.aclose() if ex.code == HTTP_UNAUTHORIZED: raise ConfigEntryAuthFailed from ex _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False + + async def _async_close_client(*_): + await async_client.aclose() + + @callback + def _async_create_close_task(): + asyncio.create_task(_async_close_client()) + + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_client) + ) + config_entry.async_on_unload(_async_create_close_task) + _async_save_tokens(hass, config_entry, access_token, refresh_token) coordinator = TeslaDataUpdateCoordinator( hass, config_entry=config_entry, controller=controller @@ -240,7 +264,9 @@ class TeslaDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Fetch data from API endpoint.""" if self.controller.is_token_refreshed(): - (refresh_token, access_token) = self.controller.get_tokens() + result = self.controller.get_tokens() + refresh_token = result["refresh_token"] + access_token = result["access_token"] _async_save_tokens( self.hass, self.config_entry, access_token, refresh_token ) diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index b6f31e9de98..706c91ae59b 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -1,7 +1,9 @@ """Tesla Config Flow.""" import logging +import httpx from teslajsonpy import Controller as TeslaAPI, TeslaException +from teslajsonpy.exceptions import IncompleteCredentials import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -14,9 +16,11 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.httpx_client import SERVER_SOFTWARE, USER_AGENT from .const import ( + CONF_EXPIRATION, CONF_WAKE_ON_START, DEFAULT_SCAN_INTERVAL, DEFAULT_WAKE_ON_START, @@ -35,6 +39,7 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the tesla flow.""" self.username = None + self.reauth = False async def async_step_import(self, import_config): """Import a config entry from configuration.yaml.""" @@ -46,10 +51,7 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: existing_entry = self._async_entry_for_username(user_input[CONF_USERNAME]) - if ( - existing_entry - and existing_entry.data[CONF_PASSWORD] == user_input[CONF_PASSWORD] - ): + if existing_entry and not self.reauth: return self.async_abort(reason="already_configured") try: @@ -81,6 +83,7 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth(self, data): """Handle configuration by re-auth.""" self.username = data[CONF_USERNAME] + self.reauth = True return await self.async_step_user() @staticmethod @@ -146,26 +149,32 @@ async def validate_input(hass: core.HomeAssistant, data): """ config = {} - websession = aiohttp_client.async_create_clientsession(hass) + async_client = httpx.AsyncClient(headers={USER_AGENT: SERVER_SOFTWARE}) try: controller = TeslaAPI( - websession, + async_client, email=data[CONF_USERNAME], password=data[CONF_PASSWORD], update_interval=DEFAULT_SCAN_INTERVAL, ) - (config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect( - test_login=True - ) + result = await controller.connect(test_login=True) + config[CONF_TOKEN] = result["refresh_token"] + config[CONF_ACCESS_TOKEN] = result["access_token"] + config[CONF_EXPIRATION] = result[CONF_EXPIRATION] config[CONF_USERNAME] = data[CONF_USERNAME] config[CONF_PASSWORD] = data[CONF_PASSWORD] + except IncompleteCredentials as ex: + _LOGGER.error("Authentication error: %s %s", ex.message, ex) + raise InvalidAuth() from ex except TeslaException as ex: if ex.code == HTTP_UNAUTHORIZED: _LOGGER.error("Invalid credentials: %s", ex) raise InvalidAuth() from ex _LOGGER.error("Unable to communicate with Tesla API: %s", ex) raise CannotConnect() from ex + finally: + await async_client.aclose() _LOGGER.debug("Credentials successfully connected to the Tesla API") return config diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py index 94883e4a833..4155942c0ad 100644 --- a/homeassistant/components/tesla/const.py +++ b/homeassistant/components/tesla/const.py @@ -1,4 +1,5 @@ """Const file for Tesla cars.""" +CONF_EXPIRATION = "expiration" CONF_WAKE_ON_START = "enable_wake_on_start" DOMAIN = "tesla" DATA_LISTENER = "listener" diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 6befca8a5f2..8604436d5a4 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.11.5"], + "requirements": ["teslajsonpy==0.18.3"], "codeowners": ["@zabuldon", "@alandtse"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index ddde6b15107..9c61d470239 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2223,7 +2223,7 @@ temperusb==1.5.3 tesla-powerwall==0.3.5 # homeassistant.components.tesla -teslajsonpy==0.11.5 +teslajsonpy==0.18.3 # homeassistant.components.tensorflow # tf-models-official==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0282c879ad4..7f85f6014a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1180,7 +1180,7 @@ tellduslive==0.10.11 tesla-powerwall==0.3.5 # homeassistant.components.tesla -teslajsonpy==0.11.5 +teslajsonpy==0.18.3 # homeassistant.components.toon toonapi==0.2.0 diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py index b35ab0039d0..4a45aac5124 100644 --- a/tests/components/tesla/test_config_flow.py +++ b/tests/components/tesla/test_config_flow.py @@ -1,10 +1,12 @@ """Test the Tesla config flow.""" +import datetime from unittest.mock import patch -from teslajsonpy import TeslaException +from teslajsonpy.exceptions import IncompleteCredentials, TeslaException from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.tesla.const import ( + CONF_EXPIRATION, CONF_WAKE_ON_START, DEFAULT_SCAN_INTERVAL, DEFAULT_WAKE_ON_START, @@ -22,6 +24,12 @@ from homeassistant.const import ( from tests.common import MockConfigEntry +TEST_USERNAME = "test-username" +TEST_TOKEN = "test-token" +TEST_PASSWORD = "test-password" +TEST_ACCESS_TOKEN = "test-access-token" +TEST_VALID_EXPIRATION = datetime.datetime.now().timestamp() * 2 + async def test_form(hass): """Test we get the form.""" @@ -34,7 +42,11 @@ async def test_form(hass): with patch( "homeassistant.components.tesla.config_flow.TeslaAPI.connect", - return_value=("test-refresh-token", "test-access-token"), + return_value={ + "refresh_token": TEST_TOKEN, + CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN, + CONF_EXPIRATION: TEST_VALID_EXPIRATION, + }, ), patch( "homeassistant.components.tesla.async_setup", return_value=True ) as mock_setup, patch( @@ -50,8 +62,9 @@ async def test_form(hass): assert result2["data"] == { CONF_USERNAME: "test@email.com", CONF_PASSWORD: "test", - CONF_TOKEN: "test-refresh-token", - CONF_ACCESS_TOKEN: "test-access-token", + CONF_TOKEN: TEST_TOKEN, + CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN, + CONF_EXPIRATION: TEST_VALID_EXPIRATION, } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -69,7 +82,26 @@ async def test_form_invalid_auth(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + {CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_invalid_auth_incomplete_credentials(hass): + """Test we handle invalid auth with incomplete credentials.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tesla.config_flow.TeslaAPI.connect", + side_effect=IncompleteCredentials(401), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD}, ) assert result2["type"] == "form" @@ -88,7 +120,7 @@ async def test_form_cannot_connect(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "test-password", CONF_USERNAME: "test-username"}, + {CONF_PASSWORD: TEST_PASSWORD, CONF_USERNAME: TEST_USERNAME}, ) assert result2["type"] == "form" @@ -99,8 +131,8 @@ async def test_form_repeat_identifier(hass): """Test we handle repeat identifiers.""" entry = MockConfigEntry( domain=DOMAIN, - title="test-username", - data={"username": "test-username", "password": "test-password"}, + title=TEST_USERNAME, + data={"username": TEST_USERNAME, "password": TEST_PASSWORD}, options=None, ) entry.add_to_hass(hass) @@ -110,11 +142,15 @@ async def test_form_repeat_identifier(hass): ) with patch( "homeassistant.components.tesla.config_flow.TeslaAPI.connect", - return_value=("test-refresh-token", "test-access-token"), + return_value={ + "refresh_token": TEST_TOKEN, + CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN, + CONF_EXPIRATION: TEST_VALID_EXPIRATION, + }, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + {CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD}, ) assert result2["type"] == "abort" @@ -125,8 +161,8 @@ async def test_form_reauth(hass): """Test we handle reauth.""" entry = MockConfigEntry( domain=DOMAIN, - title="test-username", - data={"username": "test-username", "password": "same"}, + title=TEST_USERNAME, + data={"username": TEST_USERNAME, "password": "same"}, options=None, ) entry.add_to_hass(hass) @@ -134,15 +170,19 @@ async def test_form_reauth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, - data={"username": "test-username"}, + data={"username": TEST_USERNAME}, ) with patch( "homeassistant.components.tesla.config_flow.TeslaAPI.connect", - return_value=("test-refresh-token", "test-access-token"), + return_value={ + "refresh_token": TEST_TOKEN, + CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN, + CONF_EXPIRATION: TEST_VALID_EXPIRATION, + }, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + {CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: "new-password"}, ) assert result2["type"] == "abort" @@ -154,17 +194,21 @@ async def test_import(hass): with patch( "homeassistant.components.tesla.config_flow.TeslaAPI.connect", - return_value=("test-refresh-token", "test-access-token"), + return_value={ + "refresh_token": TEST_TOKEN, + CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN, + CONF_EXPIRATION: TEST_VALID_EXPIRATION, + }, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_PASSWORD: "test-password", CONF_USERNAME: "test-username"}, + data={CONF_PASSWORD: TEST_PASSWORD, CONF_USERNAME: TEST_USERNAME}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test-username" - assert result["data"][CONF_ACCESS_TOKEN] == "test-access-token" - assert result["data"][CONF_TOKEN] == "test-refresh-token" + assert result["title"] == TEST_USERNAME + assert result["data"][CONF_ACCESS_TOKEN] == TEST_ACCESS_TOKEN + assert result["data"][CONF_TOKEN] == TEST_TOKEN assert result["description_placeholders"] is None From 1bd982668449815fee2105478569f8e4b5670add Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 May 2021 20:30:28 -0700 Subject: [PATCH 098/852] Handle different entity_id formats (#49969) --- homeassistant/components/recorder/__init__.py | 18 ++++++++-- tests/components/recorder/test_init.py | 35 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index b4be8852f55..a783dabdbed 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -358,13 +358,27 @@ class Recorder(threading.Thread): self._event_listener = None @callback - def _async_event_filter(self, event): + def _async_event_filter(self, event) -> bool: """Filter events.""" if event.event_type in self.exclude_t: return False entity_id = event.data.get(ATTR_ENTITY_ID) - return bool(entity_id is None or self.entity_filter(entity_id)) + + if entity_id is None: + return True + + if isinstance(entity_id, str): + return self.entity_filter(entity_id) + + if isinstance(entity_id, list): + for eid in entity_id: + if self.entity_filter(eid): + return True + return False + + # Unknown what it is. + return True def do_adhoc_purge(self, **kwargs): """Trigger an adhoc purge retaining keep_days worth of data.""" diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 70271634ff5..a34df0a4ac2 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -931,3 +931,38 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() hass.stop() + + +def test_entity_id_filter(hass_recorder): + """Test that entity ID filtering filters string and list.""" + hass = hass_recorder( + {"include": {"domains": "hello"}, "exclude": {"domains": "hidden_domain"}} + ) + + for idx, data in enumerate( + ( + {}, + {"entity_id": "hello.world"}, + {"entity_id": ["hello.world"]}, + {"entity_id": ["hello.world", "hidden_domain.person"]}, + {"entity_id": {"unexpected": "data"}}, + ) + ): + hass.bus.fire("hello", data) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type="hello")) + assert len(db_events) == idx + 1, data + + for data in ( + {"entity_id": "hidden_domain.person"}, + {"entity_id": ["hidden_domain.person"]}, + ): + hass.bus.fire("hello", data) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type="hello")) + # Keep referring idx + 1, as no new events are being added + assert len(db_events) == idx + 1, data From 8adbc62a6ee6849e8d7830011a82f179ccafeed2 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 2 May 2021 10:41:36 +0200 Subject: [PATCH 099/852] Enable homeassistant.update_entity service for all modbus platforms (#49918) * Rename _update() to update() A platform neeed a function update(), even though polling is false, this is due to the service: homeassistant.update_entity, which calls update() * Update test harnesss to script testing. Test homeassistant.update_entity in all platforms. This call calls update() in the platform to get a new reading. * Add reuse parameter. * Move service call from helper to tests. * Change run_service_update --> prepare_service_update. * Remove entity_id parameter. --- .../components/modbus/binary_sensor.py | 4 +- homeassistant/components/modbus/climate.py | 6 +-- homeassistant/components/modbus/cover.py | 8 ++-- homeassistant/components/modbus/sensor.py | 4 +- homeassistant/components/modbus/switch.py | 8 ++-- tests/components/modbus/conftest.py | 18 +++++++++ .../modbus/test_modbus_binary_sensor.py | 33 +++++++++++++++- .../components/modbus/test_modbus_climate.py | 27 ++++++++++++- tests/components/modbus/test_modbus_cover.py | 38 ++++++++++++++++++- tests/components/modbus/test_modbus_sensor.py | 31 ++++++++++++++- tests/components/modbus/test_modbus_switch.py | 32 +++++++++++++++- 11 files changed, 188 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index cd336ba4f73..979888d0a19 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -129,7 +129,7 @@ class ModbusBinarySensor(BinarySensorEntity): async def async_added_to_hass(self): """Handle entity which will be added.""" async_track_time_interval( - self.hass, lambda arg: self._update(), self._scan_interval + self.hass, lambda arg: self.update(), self._scan_interval ) @property @@ -162,7 +162,7 @@ class ModbusBinarySensor(BinarySensorEntity): """Return True if entity is available.""" return self._available - def _update(self): + def update(self): """Update the state of the sensor.""" if self._input_type == CALL_TYPE_COIL: result = self._hub.read_coils(self._slave, self._address, 1) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 7d326407c3b..d98cda3ed43 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -133,7 +133,7 @@ class ModbusThermostat(ClimateEntity): async def async_added_to_hass(self): """Handle entity which will be added.""" async_track_time_interval( - self.hass, lambda arg: self._update(), self._scan_interval + self.hass, lambda arg: self.update(), self._scan_interval ) @property @@ -214,14 +214,14 @@ class ModbusThermostat(ClimateEntity): self._target_temperature_register, register_value, ) - self._update() + self.update() @property def available(self) -> bool: """Return True if entity is available.""" return self._available - def _update(self): + def update(self): """Update Target & Current Temperature.""" self._target_temperature = self._read_register( CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index a0deba9d732..7d9bf9e2e45 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -107,7 +107,7 @@ class ModbusCover(CoverEntity, RestoreEntity): self._value = state.state async_track_time_interval( - self.hass, lambda arg: self._update(), self._scan_interval + self.hass, lambda arg: self.update(), self._scan_interval ) @property @@ -161,7 +161,7 @@ class ModbusCover(CoverEntity, RestoreEntity): else: self._write_register(self._state_open) - self._update() + self.update() def close_cover(self, **kwargs: Any) -> None: """Close cover.""" @@ -170,9 +170,9 @@ class ModbusCover(CoverEntity, RestoreEntity): else: self._write_register(self._state_closed) - self._update() + self.update() - def _update(self): + def update(self): """Update the state of the cover.""" if self._coil is not None and self._status_register is None: self._value = self._read_coil() diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 81b54cb62e1..c1a33c41f6d 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -229,7 +229,7 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): self._value = state.state async_track_time_interval( - self.hass, lambda arg: self._update(), self._scan_interval + self.hass, lambda arg: self.update(), self._scan_interval ) @property @@ -282,7 +282,7 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): registers.reverse() return registers - def _update(self): + def update(self): """Update the state of the sensor.""" if self._register_type == CALL_TYPE_REGISTER_INPUT: result = self._hub.read_input_registers( diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 3a74b5d14e1..6fff8c1d373 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -99,7 +99,7 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): if self._verify_active: async_track_time_interval( - self.hass, lambda arg: self._update(), self._scan_interval + self.hass, lambda arg: self.update(), self._scan_interval ) @property @@ -132,7 +132,7 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): else: self._available = True if self._verify_active: - self._update() + self.update() else: self._is_on = True self.schedule_update_ha_state() @@ -146,12 +146,12 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): else: self._available = True if self._verify_active: - self._update() + self.update() else: self._is_on = False self.schedule_update_ha_state() - def _update(self): + def update(self): """Update the entity state.""" if not self._verify_active: return diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index de30e690bba..d3ae1286ef1 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -201,3 +201,21 @@ async def base_config_test( config_modbus=config_modbus, expect_init_to_fail=expect_init_to_fail, ) + + +async def prepare_service_update(hass, config): + """Run test for service write_coil.""" + + config_modbus = { + DOMAIN: { + CONF_NAME: DEFAULT_HUB, + CONF_TYPE: "tcp", + CONF_HOST: "modbusTest", + CONF_PORT: 5001, + **config, + }, + } + assert await async_setup_component(hass, DOMAIN, config_modbus) + await hass.async_block_till_done() + assert await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_modbus_binary_sensor.py index 4ce423b2f16..27821c170e1 100644 --- a/tests/components/modbus/test_modbus_binary_sensor.py +++ b/tests/components/modbus/test_modbus_binary_sensor.py @@ -19,7 +19,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) -from .conftest import base_config_test, base_test +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update @pytest.mark.parametrize("do_discovery", [False, True]) @@ -99,3 +99,34 @@ async def test_all_binary_sensor(hass, do_type, regs, expected): scan_interval=5, ) assert state == expected + + +async def test_service_binary_sensor_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "binary_sensor.test" + config = { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: "test", + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + } + ] + } + mock_pymodbus.read_coils.return_value = ReadResult([0x00]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + mock_pymodbus.read_coils.return_value = ReadResult([0x01]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON diff --git a/tests/components/modbus/test_modbus_climate.py b/tests/components/modbus/test_modbus_climate.py index f932817b12e..1e14b255ba5 100644 --- a/tests/components/modbus/test_modbus_climate.py +++ b/tests/components/modbus/test_modbus_climate.py @@ -10,7 +10,7 @@ from homeassistant.components.modbus.const import ( ) from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE -from .conftest import base_config_test, base_test +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update @pytest.mark.parametrize( @@ -76,3 +76,28 @@ async def test_temperature_climate(hass, regs, expected): scan_interval=5, ) assert state == expected + + +async def test_service_climate_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "climate.test" + config = { + CONF_CLIMATES: [ + { + CONF_NAME: "test", + CONF_TARGET_TEMP: 117, + CONF_CURRENT_TEMP: 117, + CONF_SLAVE: 10, + } + ] + } + mock_pymodbus.read_input_registers.return_value = ReadResult([0x00]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == "auto" diff --git a/tests/components/modbus/test_modbus_cover.py b/tests/components/modbus/test_modbus_cover.py index 957f4b28c75..09d23ebf8bd 100644 --- a/tests/components/modbus/test_modbus_cover.py +++ b/tests/components/modbus/test_modbus_cover.py @@ -4,7 +4,12 @@ import logging import pytest from homeassistant.components.cover import DOMAIN as COVER_DOMAIN -from homeassistant.components.modbus.const import CALL_TYPE_COIL, CONF_REGISTER +from homeassistant.components.modbus.const import ( + CALL_TYPE_COIL, + CALL_TYPE_REGISTER_HOLDING, + CONF_REGISTER, + CONF_STATUS_REGISTER_TYPE, +) from homeassistant.const import ( CONF_COVERS, CONF_NAME, @@ -14,7 +19,7 @@ from homeassistant.const import ( STATE_OPEN, ) -from .conftest import base_config_test, base_test +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update @pytest.mark.parametrize( @@ -168,3 +173,32 @@ async def test_unsupported_config_cover(hass, read_type, caplog): assert len(caplog.records) == 1 assert caplog.records[0].levelname == "WARNING" + + +async def test_service_cover_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "cover.test" + config = { + CONF_COVERS: [ + { + CONF_NAME: "test", + CONF_REGISTER: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + } + ] + } + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_CLOSED + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OPEN diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index b8ab10953c8..c83eab0b3d6 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -40,7 +40,7 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import base_config_test, base_test +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update @pytest.mark.parametrize( @@ -621,3 +621,32 @@ async def test_swap_sensor_wrong_config(hass, caplog, swap_type): expect_init_to_fail=True, ) assert caplog.messages[-1].startswith("Error in sensor " + sensor_name + " swap") + + +async def test_service_sensor_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "sensor.test" + config = { + CONF_SENSORS: [ + { + CONF_NAME: "test", + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + } + ] + } + mock_pymodbus.read_input_registers.return_value = ReadResult([27]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == "27" + mock_pymodbus.read_input_registers.return_value = ReadResult([32]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == "32" diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py index bc96127cbd6..f247bb4b148 100644 --- a/tests/components/modbus/test_modbus_switch.py +++ b/tests/components/modbus/test_modbus_switch.py @@ -25,7 +25,7 @@ from homeassistant.const import ( STATE_ON, ) -from .conftest import base_config_test, base_test +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update @pytest.mark.parametrize( @@ -220,3 +220,33 @@ async def test_register_state_switch(hass, regs, expected): scan_interval=5, ) assert state == expected + + +async def test_service_switch_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "switch.test" + config = { + CONF_SWITCHES: [ + { + CONF_NAME: "test", + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + } + mock_pymodbus.read_coils.return_value = ReadResult([0x01]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + mock_pymodbus.read_coils.return_value = ReadResult([0x00]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF From 24b9d7339238a7434c0d89356a83a71f1ddaef1d Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 2 May 2021 09:53:35 +0100 Subject: [PATCH 100/852] Improves UX of Utility Meter services (#48556) --- .../components/utility_meter/services.yaml | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index d33229f4e56..fac9dadfa29 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -1,35 +1,40 @@ # Describes the format for available switch services reset: + name: Reset description: Resets the counter of an utility meter. - fields: - entity_id: - description: Name(s) of the utility meter to reset - example: "utility_meter.energy" + target: next_tariff: + name: Next Tariff description: Changes the tariff to the next one. - fields: - entity_id: - description: Name(s) of entities to reset - example: "utility_meter.energy" + target: select_tariff: - description: selects the current tariff of an utility meter. + name: Select Tariff + description: Selects the current tariff of an utility meter. + target: fields: - entity_id: - description: Name of the entity to set the tariff for - example: "utility_meter.energy" tariff: + name: Tariff description: Name of the tariff to switch to example: "offpeak" + required: true + selector: + text: calibrate: - description: calibrates an utility meter. + name: Calibrate + description: Calibrates a utility meter sensor. + target: + entity: + domain: sensor fields: - entity_id: - description: Name of the entity to calibrate - example: "utility_meter.energy" value: + name: Value description: Value to which set the meter example: "100" + required: true + selector: + text: + From f7fafa6b8b48575c21c7e57ef30517a9b30f5f3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 May 2021 05:09:12 -1000 Subject: [PATCH 101/852] Add zeroconf discovery to rachio (#49973) --- homeassistant/components/rachio/manifest.json | 6 ++++++ homeassistant/generated/zeroconf.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index 67cdf2496ee..735e2f35bf4 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -24,5 +24,11 @@ "homekit": { "models": ["Rachio"] }, + "zeroconf": [ + { + "type": "_http._tcp.local.", + "name": "rachio*" + } + ], "iot_class": "cloud_push" } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 4c017b07628..6ea8b7d2ebb 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -94,6 +94,10 @@ ZEROCONF = { } ], "_http._tcp.local.": [ + { + "domain": "rachio", + "name": "rachio*" + }, { "domain": "shelly", "name": "shelly*" From 6c4448a272a2979dc3f90b93aab43f60b3427a79 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 2 May 2021 17:18:36 +0200 Subject: [PATCH 102/852] Upgrade elgato to 2.1.0 (#49975) --- homeassistant/components/elgato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index f2493befcbd..ebc337c2925 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -3,7 +3,7 @@ "name": "Elgato Key Light", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/elgato", - "requirements": ["elgato==2.0.1"], + "requirements": ["elgato==2.1.0"], "zeroconf": ["_elg._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 9c61d470239..b1fb32532b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -530,7 +530,7 @@ ecoaliface==0.4.0 eebrightbox==0.0.4 # homeassistant.components.elgato -elgato==2.0.1 +elgato==2.1.0 # homeassistant.components.eliqonline eliqonline==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f85f6014a4..157c3d2abc1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -288,7 +288,7 @@ dynalite_devices==0.1.46 eebrightbox==0.0.4 # homeassistant.components.elgato -elgato==2.0.1 +elgato==2.1.0 # homeassistant.components.elkm1 elkm1-lib==0.8.10 From 93b668a6f9e603e50061e2d6bf2223d22bb29162 Mon Sep 17 00:00:00 2001 From: Florian Gareis Date: Mon, 3 May 2021 00:05:40 +0200 Subject: [PATCH 103/852] Upgrade yeelight to 0.6.2 (#49995) --- homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index bfb195b91fc..8e5288efb81 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.6.1"], + "requirements": ["yeelight==0.6.2"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index b1fb32532b5..3c0eba02725 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2376,7 +2376,7 @@ yalesmartalarmclient==0.1.6 yalexs==1.1.11 # homeassistant.components.yeelight -yeelight==0.6.1 +yeelight==0.6.2 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 157c3d2abc1..2d4753110a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1261,7 +1261,7 @@ xmltodict==0.12.0 yalexs==1.1.11 # homeassistant.components.yeelight -yeelight==0.6.1 +yeelight==0.6.2 # homeassistant.components.onvif zeep[async]==4.0.0 From 26fd7fc15b44a17e539db0998fcf7f99b64cfd2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 May 2021 12:43:59 -1000 Subject: [PATCH 104/852] Add dhcp discovery to tado (#49992) --- homeassistant/components/tado/manifest.json | 5 +++++ homeassistant/generated/dhcp.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 7b488487afe..758756e8127 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -8,5 +8,10 @@ "homekit": { "models": ["tado", "AC02"] }, + "dhcp": [ + { + "hostname": "tado*" + } + ], "iot_class": "cloud_polling" } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 0fa1777d2a4..aa70d978e6c 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -179,6 +179,10 @@ DHCP = [ "hostname": "squeezebox*", "macaddress": "000420*" }, + { + "domain": "tado", + "hostname": "tado*" + }, { "domain": "tesla", "hostname": "tesla_*", From 6967fd184bd672892f51734633d0218fdbe3ddbf Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Mon, 3 May 2021 01:18:29 +0200 Subject: [PATCH 105/852] Abstract Rituals API data processing to PyPI (#49872) --- .../rituals_perfume_genie/__init__.py | 9 +++--- .../rituals_perfume_genie/binary_sensor.py | 10 ++++--- .../components/rituals_perfume_genie/const.py | 2 -- .../rituals_perfume_genie/entity.py | 14 ++++----- .../rituals_perfume_genie/manifest.json | 2 +- .../rituals_perfume_genie/sensor.py | 30 +++++-------------- .../rituals_perfume_genie/switch.py | 10 +++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 32 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 7b1a4ae7d1c..a06c9acdd7d 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUB, HUBLOT +from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUBLOT PLATFORMS = ["binary_sensor", "sensor", "switch"] @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): } for device in account_devices: - hublot = device.data[HUB][HUBLOT] + hublot = device.hub_data[HUBLOT] coordinator = RitualsPerufmeGenieDataUpdateCoordinator(hass, device) await coordinator.async_refresh() @@ -70,11 +70,10 @@ class RitualsPerufmeGenieDataUpdateCoordinator(DataUpdateCoordinator): super().__init__( hass, _LOGGER, - name=f"{DOMAIN}-{device.data[HUB][HUBLOT]}", + name=f"{DOMAIN}-{device.hub_data[HUBLOT]}", update_interval=UPDATE_INTERVAL, ) - async def _async_update_data(self) -> dict: + async def _async_update_data(self) -> None: """Fetch data from Rituals.""" await self._device.update_data() - return self._device.data diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index a7c6732cb13..a73595dfdcf 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -1,4 +1,6 @@ """Support for Rituals Perfume Genie binary sensors.""" +from __future__ import annotations + from typing import Callable from pyrituals import Diffuser @@ -11,8 +13,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID -from .entity import SENSORS, DiffuserEntity +from .const import COORDINATORS, DEVICES, DOMAIN +from .entity import DiffuserEntity CHARGING_SUFFIX = " Battery Charging" BATTERY_CHARGING_ID = 21 @@ -26,7 +28,7 @@ async def async_setup_entry( coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] entities = [] for hublot, diffuser in diffusers.items(): - if BATTERY in diffuser.data[HUB][SENSORS]: + if diffuser.has_battery: coordinator = coordinators[hublot] entities.append(DiffuserBatteryChargingBinarySensor(diffuser, coordinator)) @@ -43,7 +45,7 @@ class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return the state of the battery charging binary sensor.""" - return self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID + return self._diffuser.charging @property def device_class(self) -> str: diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py index fef16b7f6f6..c0bf72fb90e 100644 --- a/homeassistant/components/rituals_perfume_genie/const.py +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -6,8 +6,6 @@ DEVICES = "devices" ACCOUNT_HASH = "account_hash" ATTRIBUTES = "attributes" -BATTERY = "battc" -HUB = "hub" HUBLOT = "hublot" ID = "id" SENSORS = "sensors" diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index a3b4f568bc5..1253a38e962 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -7,7 +7,7 @@ from pyrituals import Diffuser from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTRIBUTES, BATTERY, DOMAIN, HUB, HUBLOT, SENSORS +from .const import ATTRIBUTES, DOMAIN, HUBLOT, SENSORS MANUFACTURER = "Rituals Cosmetics" MODEL = "The Perfume Genie" @@ -30,8 +30,8 @@ class DiffuserEntity(CoordinatorEntity): super().__init__(coordinator) self._diffuser = diffuser self._entity_suffix = entity_suffix - self._hublot = self.coordinator.data[HUB][HUBLOT] - self._hubname = self.coordinator.data[HUB][ATTRIBUTES][ROOMNAME] + self._hublot = self._diffuser.hub_data[HUBLOT] + self._hubname = self._diffuser.hub_data[ATTRIBUTES][ROOMNAME] @property def unique_id(self) -> str: @@ -46,9 +46,7 @@ class DiffuserEntity(CoordinatorEntity): @property def available(self) -> bool: """Return if the entity is available.""" - return ( - super().available and self.coordinator.data[HUB][STATUS] == AVAILABLE_STATE - ) + return super().available and self._diffuser.hub_data[STATUS] == AVAILABLE_STATE @property def device_info(self) -> dict[str, Any]: @@ -57,6 +55,6 @@ class DiffuserEntity(CoordinatorEntity): "name": self._hubname, "identifiers": {(DOMAIN, self._hublot)}, "manufacturer": MANUFACTURER, - "model": MODEL if BATTERY in self._diffuser.data[HUB][SENSORS] else MODEL2, - "sw_version": self.coordinator.data[HUB][SENSORS][VERSION], + "model": MODEL if self._diffuser.has_battery else MODEL2, + "sw_version": self._diffuser.hub_data[SENSORS][VERSION], } diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json index 8ec7b0c8df3..756af10f33b 100644 --- a/homeassistant/components/rituals_perfume_genie/manifest.json +++ b/homeassistant/components/rituals_perfume_genie/manifest.json @@ -3,7 +3,7 @@ "name": "Rituals Perfume Genie", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie", - "requirements": ["pyrituals==0.0.2"], + "requirements": ["pyrituals==0.0.3"], "codeowners": ["@milanmeu"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 388932be74c..1c12b305d48 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID, SENSORS +from .const import COORDINATORS, DEVICES, DOMAIN, ID, SENSORS from .entity import DiffuserEntity TITLE = "title" @@ -44,7 +44,7 @@ async def async_setup_entry( entities.append(DiffuserPerfumeSensor(diffuser, coordinator)) entities.append(DiffuserFillSensor(diffuser, coordinator)) entities.append(DiffuserWifiSensor(diffuser, coordinator)) - if BATTERY in diffuser.data[HUB][SENSORS]: + if diffuser.has_battery: entities.append(DiffuserBatterySensor(diffuser, coordinator)) async_add_entities(entities) @@ -60,14 +60,14 @@ class DiffuserPerfumeSensor(DiffuserEntity): @property def icon(self) -> str: """Return the perfume sensor icon.""" - if self.coordinator.data[HUB][SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: + if self._diffuser.hub_data[SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: return "mdi:tag-remove" return "mdi:tag-text" @property def state(self) -> str: """Return the state of the perfume sensor.""" - return self.coordinator.data[HUB][SENSORS][PERFUME][TITLE] + return self._diffuser.hub_data[SENSORS][PERFUME][TITLE] class DiffuserFillSensor(DiffuserEntity): @@ -80,14 +80,14 @@ class DiffuserFillSensor(DiffuserEntity): @property def icon(self) -> str: """Return the fill sensor icon.""" - if self.coordinator.data[HUB][SENSORS][FILL][ID] == FILL_NO_CARTRIDGE_ID: + if self._diffuser.hub_data[SENSORS][FILL][ID] == FILL_NO_CARTRIDGE_ID: return "mdi:beaker-question" return "mdi:beaker" @property def state(self) -> str: """Return the state of the fill sensor.""" - return self.coordinator.data[HUB][SENSORS][FILL][TITLE] + return self._diffuser.hub_data[SENSORS][FILL][TITLE] class DiffuserBatterySensor(DiffuserEntity): @@ -100,15 +100,7 @@ class DiffuserBatterySensor(DiffuserEntity): @property def state(self) -> int: """Return the state of the battery sensor.""" - # Use ICON because TITLE may change in the future. - # ICON filename does not match the image. - return { - "battery-charge.png": 100, - "battery-full.png": 100, - "Battery-75.png": 50, - "battery-50.png": 25, - "battery-low.png": 10, - }[self.coordinator.data[HUB][SENSORS][BATTERY][ICON]] + return self._diffuser.battery_percentage @property def device_class(self) -> str: @@ -131,13 +123,7 @@ class DiffuserWifiSensor(DiffuserEntity): @property def state(self) -> int: """Return the state of the wifi sensor.""" - # Use ICON because TITLE may change in the future. - return { - "icon-signal.png": 100, - "icon-signal-75.png": 70, - "icon-signal-low.png": 25, - "icon-signal-0.png": 0, - }[self.coordinator.data[HUB][SENSORS][WIFI][ICON]] + return self._diffuser.wifi_percentage @property def device_class(self) -> str: diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index 1328a18d766..a564a04c698 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, HUB +from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN from .entity import DiffuserEntity FAN = "fanc" @@ -40,7 +40,7 @@ class DiffuserSwitch(SwitchEntity, DiffuserEntity): def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the diffuser switch.""" super().__init__(diffuser, coordinator, "") - self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE + self._is_on = self._diffuser.is_on @property def icon(self) -> str: @@ -51,8 +51,8 @@ class DiffuserSwitch(SwitchEntity, DiffuserEntity): def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = { - "fan_speed": self.coordinator.data[HUB][ATTRIBUTES][SPEED], - "room_size": self.coordinator.data[HUB][ATTRIBUTES][ROOM], + "fan_speed": self._diffuser.hub_data[ATTRIBUTES][SPEED], + "room_size": self._diffuser.hub_data[ATTRIBUTES][ROOM], } return attributes @@ -76,5 +76,5 @@ class DiffuserSwitch(SwitchEntity, DiffuserEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE + self._is_on = self._diffuser.is_on self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 3c0eba02725..159252d6da8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1676,7 +1676,7 @@ pyrepetier==3.0.5 pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie -pyrituals==0.0.2 +pyrituals==0.0.3 # homeassistant.components.zeroconf pyroute2==0.5.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d4753110a2..4e22c4a41ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -924,7 +924,7 @@ pyqwikswitch==0.93 pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie -pyrituals==0.0.2 +pyrituals==0.0.3 # homeassistant.components.zeroconf pyroute2==0.5.18 From 04266301e92c556eb27e53fe59ff9387beb7517b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 3 May 2021 00:05:16 +0000 Subject: [PATCH 106/852] [ci skip] Translation update --- .../components/adguard/translations/de.json | 1 + .../advantage_air/translations/de.json | 1 + .../alarmdecoder/translations/de.json | 4 ++ .../components/almond/translations/ru.json | 2 +- .../ambiclimate/translations/ru.json | 2 +- .../components/cast/translations/de.json | 4 +- .../components/cloud/translations/de.json | 1 + .../coronavirus/translations/de.json | 3 +- .../components/deconz/translations/de.json | 4 ++ .../components/denonavr/translations/de.json | 1 + .../devolo_home_control/translations/de.json | 7 +++ .../dialogflow/translations/ru.json | 2 +- .../components/dsmr/translations/de.json | 10 ++++ .../components/eafm/translations/de.json | 4 +- .../enphase_envoy/translations/de.json | 3 +- .../flunearyou/translations/de.json | 1 + .../forked_daapd/translations/de.json | 2 + .../components/fritz/translations/de.json | 36 +++++++++++++++ .../components/geofency/translations/ru.json | 2 +- .../components/gios/translations/de.json | 5 ++ .../google_travel_time/translations/de.json | 19 ++++++-- .../google_travel_time/translations/es.json | 1 + .../components/gpslogger/translations/ru.json | 2 +- .../components/hassio/translations/de.json | 1 + .../home_connect/translations/ru.json | 2 +- .../home_plus_control/translations/ru.json | 2 +- .../components/homekit/translations/de.json | 2 + .../homekit_controller/translations/de.json | 3 ++ .../huawei_lte/translations/de.json | 3 +- .../components/hyperion/translations/de.json | 1 + .../components/ifttt/translations/ru.json | 2 +- .../components/insteon/translations/de.json | 46 +++++++++++++++++-- .../components/kmtronic/translations/de.json | 9 ++++ .../components/life360/translations/ru.json | 4 +- .../components/locative/translations/ru.json | 2 +- .../logi_circle/translations/ru.json | 2 +- .../lutron_caseta/translations/de.json | 21 +++++++++ .../components/lyric/translations/de.json | 7 ++- .../components/lyric/translations/ru.json | 2 +- .../components/mailgun/translations/ru.json | 2 +- .../components/mikrotik/translations/de.json | 1 + .../components/motioneye/translations/de.json | 13 ++++++ .../components/mqtt/translations/de.json | 3 ++ .../components/mutesync/translations/de.json | 15 ++++++ .../components/mysensors/translations/de.json | 7 +++ .../components/neato/translations/ru.json | 4 +- .../components/nest/translations/ru.json | 2 +- .../components/netatmo/translations/ru.json | 2 +- .../components/omnilogic/translations/de.json | 1 + .../components/omnilogic/translations/es.json | 1 + .../ondilo_ico/translations/ru.json | 2 +- .../opentherm_gw/translations/de.json | 5 +- .../openweathermap/translations/de.json | 1 + .../components/owntracks/translations/ru.json | 2 +- .../philips_js/translations/de.json | 3 +- .../components/picnic/translations/de.json | 22 +++++++++ .../components/plaato/translations/de.json | 5 +- .../components/plaato/translations/ru.json | 2 +- .../components/plugwise/translations/de.json | 9 ++-- .../components/point/translations/ru.json | 2 +- .../progettihwsw/translations/de.json | 6 ++- .../components/ps4/translations/ru.json | 6 +-- .../rainmachine/translations/de.json | 3 ++ .../recollect_waste/translations/de.json | 6 +++ .../components/risco/translations/de.json | 18 +++++++- .../components/roomba/translations/de.json | 3 +- .../components/roomba/translations/ru.json | 2 +- .../components/roon/translations/de.json | 7 ++- .../components/sma/translations/de.json | 5 +- .../components/smappee/translations/ru.json | 2 +- .../components/smarttub/translations/de.json | 4 ++ .../components/soma/translations/ru.json | 2 +- .../components/somfy/translations/ru.json | 2 +- .../components/spotify/translations/de.json | 1 + .../components/spotify/translations/ru.json | 2 +- .../srp_energy/translations/de.json | 3 ++ .../components/subaru/translations/de.json | 14 +++++- .../components/tasmota/translations/de.json | 3 ++ .../components/toon/translations/ru.json | 2 +- .../totalconnect/translations/de.json | 7 ++- .../components/traccar/translations/ru.json | 2 +- .../components/tuya/translations/de.json | 17 ++++++- .../components/twilio/translations/ru.json | 2 +- .../components/twinkly/translations/de.json | 4 ++ .../components/unifi/translations/de.json | 2 + .../components/vera/translations/ru.json | 2 +- .../components/vizio/translations/de.json | 3 +- .../waze_travel_time/translations/de.json | 1 + .../waze_travel_time/translations/es.json | 1 + .../components/wilight/translations/de.json | 1 + .../components/withings/translations/ru.json | 2 +- .../components/xbox/translations/ru.json | 2 +- .../components/yeelight/translations/de.json | 5 +- .../components/zha/translations/de.json | 13 ++++++ .../components/zha/translations/es.json | 13 ++++++ .../components/zha/translations/zh-Hant.json | 13 ++++++ .../components/zwave_js/translations/de.json | 2 + 97 files changed, 444 insertions(+), 69 deletions(-) create mode 100644 homeassistant/components/fritz/translations/de.json create mode 100644 homeassistant/components/motioneye/translations/de.json create mode 100644 homeassistant/components/mutesync/translations/de.json create mode 100644 homeassistant/components/picnic/translations/de.json diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json index 2731f3f7eba..0819dc1c0a5 100644 --- a/homeassistant/components/adguard/translations/de.json +++ b/homeassistant/components/adguard/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, diff --git a/homeassistant/components/advantage_air/translations/de.json b/homeassistant/components/advantage_air/translations/de.json index 3b4066996eb..c761ac5c6be 100644 --- a/homeassistant/components/advantage_air/translations/de.json +++ b/homeassistant/components/advantage_air/translations/de.json @@ -12,6 +12,7 @@ "ip_address": "IP Adresse", "port": "Port" }, + "description": "Anschluss an die API Ihres Advantage Air Wandtabletts.", "title": "Verbinden" } } diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json index c37fb7b4390..aea85f49a59 100644 --- a/homeassistant/components/alarmdecoder/translations/de.json +++ b/homeassistant/components/alarmdecoder/translations/de.json @@ -3,12 +3,16 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, + "create_entry": { + "default": "Erfolgreich mit AlarmDecoder verbunden." + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "protocol": { "data": { + "device_baudrate": "Ger\u00e4te-Baudrate", "device_path": "Ger\u00e4tepfad", "host": "Host", "port": "Port" diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json index 62b5df122a1..b77e1cfca2c 100644 --- a/homeassistant/components/almond/translations/ru.json +++ b/homeassistant/components/almond/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, diff --git a/homeassistant/components/ambiclimate/translations/ru.json b/homeassistant/components/ambiclimate/translations/ru.json index a1948c45d0f..66ed1f5e43c 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_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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." + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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." diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index a59b3421c10..e9821bbe937 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -27,7 +27,9 @@ "step": { "options": { "data": { - "known_hosts": "Optionale Liste bekannter Hosts, wenn die mDNS-Erkennung nicht funktioniert." + "ignore_cec": "Optionale Liste, die an pychromecast.IGNORE_CEC \u00fcbergeben wird.", + "known_hosts": "Optionale Liste bekannter Hosts, wenn die mDNS-Erkennung nicht funktioniert.", + "uuid": "Optionale Liste der UUIDs. Casts, die nicht aufgef\u00fchrt sind, werden nicht hinzugef\u00fcgt." }, "description": "Bitte die Google Cast-Konfiguration eingeben." } diff --git a/homeassistant/components/cloud/translations/de.json b/homeassistant/components/cloud/translations/de.json index 443a5e3aa72..fd5598fa026 100644 --- a/homeassistant/components/cloud/translations/de.json +++ b/homeassistant/components/cloud/translations/de.json @@ -7,6 +7,7 @@ "can_reach_cloud_auth": "Authentifizierungsserver erreichbar", "google_enabled": "Google aktiviert", "logged_in": "Angemeldet", + "relayer_connected": "Relay Verbunden", "remote_connected": "Remote verbunden", "remote_enabled": "Remote aktiviert", "subscription_expiration": "Ablauf des Abonnements" diff --git a/homeassistant/components/coronavirus/translations/de.json b/homeassistant/components/coronavirus/translations/de.json index f2aee659bc0..caa74a64ab9 100644 --- a/homeassistant/components/coronavirus/translations/de.json +++ b/homeassistant/components/coronavirus/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Dieses Land ist bereits konfiguriert." + "already_configured": "Dieses Land ist bereits konfiguriert.", + "cannot_connect": "Verbinden fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index a8575d212d6..99d9e8d1e92 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -42,6 +42,10 @@ "button_2": "Zweite Taste", "button_3": "Dritte Taste", "button_4": "Vierte Taste", + "button_5": "Taste 5", + "button_6": "Taste 6", + "button_7": "Taste 7", + "button_8": "Taste 8", "close": "Schlie\u00dfen", "dim_down": "Dimmer runter", "dim_up": "Dimmer hoch", diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index 0cf669a13b4..f8896e5a08f 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Alle Quellen anzeigen", + "update_audyssey": "Audyssey-Einstellungen aktualisieren", "zone2": "Zone 2 einrichten", "zone3": "Zone 3 einrichten" }, diff --git a/homeassistant/components/devolo_home_control/translations/de.json b/homeassistant/components/devolo_home_control/translations/de.json index 6cf7ed3c821..251d058b42e 100644 --- a/homeassistant/components/devolo_home_control/translations/de.json +++ b/homeassistant/components/devolo_home_control/translations/de.json @@ -14,6 +14,13 @@ "password": "Passwort", "username": "E-Mail / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Passwort", + "username": "E-Mail / devolo ID" + } } } } diff --git a/homeassistant/components/dialogflow/translations/ru.json b/homeassistant/components/dialogflow/translations/ru.json index de1e10b7e49..55768c4f567 100644 --- a/homeassistant/components/dialogflow/translations/ru.json +++ b/homeassistant/components/dialogflow/translations/ru.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Dialogflow]({dialogflow_url}).\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\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 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Dialogflow]({dialogflow_url}).\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/dsmr/translations/de.json b/homeassistant/components/dsmr/translations/de.json index da1d200c2a2..97d6739b787 100644 --- a/homeassistant/components/dsmr/translations/de.json +++ b/homeassistant/components/dsmr/translations/de.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Mindestzeit zwischen Entit\u00e4tsaktualisierungen [s]" + }, + "title": "DSMR-Optionen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/de.json b/homeassistant/components/eafm/translations/de.json index 874e5ff9dad..9bb9fda51bf 100644 --- a/homeassistant/components/eafm/translations/de.json +++ b/homeassistant/components/eafm/translations/de.json @@ -7,7 +7,9 @@ "user": { "data": { "station": "Station" - } + }, + "description": "W\u00e4hlen Sie die Station aus, die Sie \u00fcberwachen m\u00f6chten", + "title": "Verfolgen Sie eine Hochwasser\u00fcberwachungsstation" } } } diff --git a/homeassistant/components/enphase_envoy/translations/de.json b/homeassistant/components/enphase_envoy/translations/de.json index c3c916f31f7..5aee8a03e6b 100644 --- a/homeassistant/components/enphase_envoy/translations/de.json +++ b/homeassistant/components/enphase_envoy/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json index 1c94931f405..585923fb2bf 100644 --- a/homeassistant/components/flunearyou/translations/de.json +++ b/homeassistant/components/flunearyou/translations/de.json @@ -12,6 +12,7 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" }, + "description": "\u00dcberwachen Sie benutzerbasierte und CDC-Berichte f\u00fcr ein Koordinatenpaar.", "title": "Konfigurieren Sie die Grippe in Ihrer N\u00e4he" } } diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index 559db72d42c..0001157ce41 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -17,6 +17,7 @@ "user": { "data": { "host": "Host", + "name": "Freundlicher Name", "password": "API-Passwort (leer lassen, wenn kein Passwort vorhanden ist)", "port": "API Port" }, @@ -28,6 +29,7 @@ "step": { "init": { "data": { + "librespot_java_port": "Port f\u00fcr librespot-java pipe control (falls verwendet)", "max_playlists": "Maximale Anzahl der als Quellen verwendeten Wiedergabelisten", "tts_pause_time": "Sekunden bis zur Pause vor und nach der TTS", "tts_volume": "TTS-Lautst\u00e4rke (Float im Bereich [0,1])" diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json new file mode 100644 index 00000000000..19005c7fd23 --- /dev/null +++ b/homeassistant/components/fritz/translations/de.json @@ -0,0 +1,36 @@ +{ + "config": { + "error": { + "invalid_auth": "Authentifizierung fehlgeschlagen" + }, + "flow_title": "FRITZ! Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Entdeckte FRITZ! Box: {name} \n\n Richten Sie FRITZ! Box Tools ein, um {name} zu kontrollieren", + "title": "FRITZ! Box Tools einrichten" + }, + "reauth_confirm": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Aktualisieren Sie die Anmeldeinformationen von FRITZ! Box Tools f\u00fcr: {host} . \n\n FRITZ! Box Tools kann sich nicht bei Ihrer FRITZ! Box anmelden.", + "title": "Aktualisieren der FRITZ! Box Tools - Anmeldeinformationen" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + }, + "description": "Einrichten der FRITZ! Box Tools zur Steuerung Ihrer FRITZ! Box.\n Ben\u00f6tigt: Benutzername, Passwort.", + "title": "Setup FRITZ! Box Tools - obligatorisch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/ru.json b/homeassistant/components/geofency/translations/ru.json index 46b5c0ba93f..c4ac5e79282 100644 --- a/homeassistant/components/geofency/translations/ru.json +++ b/homeassistant/components/geofency/translations/ru.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Geofency.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\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 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Geofency.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/gios/translations/de.json b/homeassistant/components/gios/translations/de.json index 7bbb01cf18d..e1351278f38 100644 --- a/homeassistant/components/gios/translations/de.json +++ b/homeassistant/components/gios/translations/de.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Polnische Hauptinspektion f\u00fcr Umweltschutz)" } } + }, + "system_health": { + "info": { + "can_reach_server": "GIO\u015a-Server erreichbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/de.json b/homeassistant/components/google_travel_time/translations/de.json index c2a95e49afb..4e89ad7da1a 100644 --- a/homeassistant/components/google_travel_time/translations/de.json +++ b/homeassistant/components/google_travel_time/translations/de.json @@ -11,8 +11,10 @@ "data": { "api_key": "API-Schl\u00fcssel", "destination": "Zielort", + "name": "Name", "origin": "Startort" - } + }, + "description": "Bei der Angabe von Ursprung und Ziel k\u00f6nnen Sie einen oder mehrere durch das Pipe-Zeichen getrennte Orte in Form einer Adresse, L\u00e4ngen- / Breitengradkoordinaten oder einer Google-Orts-ID angeben. Wenn Sie den Standort mithilfe einer Google-Orts-ID angeben, muss der ID \"place_id:\" vorangestellt werden." } } }, @@ -20,9 +22,18 @@ "step": { "init": { "data": { - "language": "Sprache" - } + "avoid": "Vermeiden", + "language": "Sprache", + "mode": "Reisemodus", + "time": "Uhrzeit", + "time_type": "Zeittyp", + "transit_mode": "Transit-Modus", + "transit_routing_preference": "Transit-Routing-Einstellungen", + "units": "Einheiten" + }, + "description": "Sie k\u00f6nnen optional entweder eine Abfahrtszeit oder eine Ankunftszeit angeben. Wenn Sie eine Abfahrtszeit angeben, k\u00f6nnen Sie \"Now\", einen Unix-Zeitstempel oder eine 24-Stunden-Zeichenkette wie \"08:00:00\" eingeben. Wenn Sie eine Ankunftszeit angeben, k\u00f6nnen Sie einen Unix-Zeitstempel oder eine 24-Stunden-Zeichenkette wie \"08:00:00\" verwenden." } } - } + }, + "title": "Google Maps Reisezeit" } \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/es.json b/homeassistant/components/google_travel_time/translations/es.json index 1c59ce07997..cecfc620e47 100644 --- a/homeassistant/components/google_travel_time/translations/es.json +++ b/homeassistant/components/google_travel_time/translations/es.json @@ -11,6 +11,7 @@ "data": { "api_key": "Clave API", "destination": "Destino", + "name": "Nombre", "origin": "Origen" }, "description": "Al especificar el origen y el destino, puedes proporcionar una o m\u00e1s ubicaciones separadas por el car\u00e1cter de barra vertical, en forma de una direcci\u00f3n, coordenadas de latitud/longitud o un ID de lugar de Google. Al especificar la ubicaci\u00f3n utilizando un ID de lugar de Google, el ID debe tener el prefijo `place_id:`." diff --git a/homeassistant/components/gpslogger/translations/ru.json b/homeassistant/components/gpslogger/translations/ru.json index 31b7759d2cb..999949d5dd0 100644 --- a/homeassistant/components/gpslogger/translations/ru.json +++ b/homeassistant/components/gpslogger/translations/ru.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\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 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/hassio/translations/de.json b/homeassistant/components/hassio/translations/de.json index a0d02f4a7b9..19538e0b3e1 100644 --- a/homeassistant/components/hassio/translations/de.json +++ b/homeassistant/components/hassio/translations/de.json @@ -5,6 +5,7 @@ "disk_total": "Speicherplatz gesamt", "disk_used": "Speicherplatz genutzt", "docker_version": "Docker-Version", + "healthy": "Gesund", "host_os": "Host-Betriebssystem", "installed_addons": "Installierte Add-ons", "supervisor_api": "Supervisor-API", diff --git a/homeassistant/components/home_connect/translations/ru.json b/homeassistant/components/home_connect/translations/ru.json index b354d91c20c..4803f4bd066 100644 --- a/homeassistant/components/home_connect/translations/ru.json +++ b/homeassistant/components/home_connect/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, "create_entry": { diff --git a/homeassistant/components/home_plus_control/translations/ru.json b/homeassistant/components/home_plus_control/translations/ru.json index fd3da6929d8..c968cc261e2 100644 --- a/homeassistant/components/home_plus_control/translations/ru.json +++ b/homeassistant/components/home_plus_control/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index 55df691b58b..b1f5b23c264 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -19,6 +19,7 @@ "title": "W\u00e4hle die Domains aus, die aufgenommen werden sollen" }, "pairing": { + "description": "Um die Kopplung abzuschlie\u00dfen, folgen Sie den Anweisungen in \"Benachrichtigungen\" unter \"HomeKit-Kopplung\".", "title": "HomeKit verbinden" }, "user": { @@ -54,6 +55,7 @@ "entities": "Entit\u00e4ten", "mode": "Modus" }, + "description": "W\u00e4hlen Sie die einzubeziehenden Entit\u00e4ten aus. Im Zubeh\u00f6rmodus wird nur eine einzelne Entit\u00e4t eingeschlossen. Im Bridge-Include-Modus werden alle Entit\u00e4ten in der Dom\u00e4ne eingeschlossen, sofern nicht bestimmte Entit\u00e4ten ausgew\u00e4hlt sind. Im Bridge-Exclude-Modus werden alle Entit\u00e4ten in der Dom\u00e4ne eingeschlossen, au\u00dfer den ausgeschlossenen Entit\u00e4ten. F\u00fcr eine optimale Leistung wird f\u00fcr jeden TV-Media-Player, jede aktivit\u00e4tsbasierte Fernbedienung, jedes Schloss und jede Kamera ein separates HomeKit-Zubeh\u00f6r erstellt.", "title": "W\u00e4hle die Entit\u00e4ten aus, die aufgenommen werden sollen" }, "init": { diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 49586a23634..120e9a63e66 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -7,6 +7,7 @@ "already_paired": "Dieses Zubeh\u00f6r ist bereits mit einem anderen Ger\u00e4t gekoppelt. Setze das Zubeh\u00f6r zur\u00fcck und versuche es erneut.", "ignored_model": "Die Unterst\u00fctzung von HomeKit f\u00fcr dieses Modell ist blockiert, da eine vollst\u00e4ndige native Integration verf\u00fcgbar ist.", "invalid_config_entry": "Dieses Ger\u00e4t wird als bereit zum Koppeln angezeigt, es gibt jedoch bereits einen widerspr\u00fcchlichen Konfigurationseintrag in Home Assistant, der zuerst entfernt werden muss.", + "invalid_properties": "Ung\u00fcltige Eigenschaften vom Ger\u00e4t gemeldet.", "no_devices": "Keine ungekoppelten Ger\u00e4te gefunden" }, "error": { @@ -19,9 +20,11 @@ "flow_title": "HomeKit-Zubeh\u00f6r: {name}", "step": { "busy_error": { + "description": "Brechen Sie das Pairing auf allen Controllern ab oder versuchen Sie, das Ger\u00e4t neu zu starten, und fahren Sie dann fort, das Pairing fortzusetzen.", "title": "Das Ger\u00e4t wird bereits mit einem anderen Controller gekoppelt" }, "max_tries_error": { + "description": "Das Ger\u00e4t hat mehr als 100 erfolglose Authentifizierungsversuche erhalten. Versuchen Sie, das Ger\u00e4t neu zu starten, und fahren Sie dann fort, die Kopplung fortzusetzen.", "title": "Maximale Authentifizierungsversuche \u00fcberschritten" }, "pair": { diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 43361e46929..10a7af41a3c 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -34,7 +34,8 @@ "data": { "name": "Name des Benachrichtigungsdienstes (\u00c4nderung erfordert Neustart)", "recipient": "SMS-Benachrichtigungsempf\u00e4nger", - "track_new_devices": "Neue Ger\u00e4te verfolgen" + "track_new_devices": "Neue Ger\u00e4te verfolgen", + "track_wired_clients": "Kabelgebundene Netzwerk-Clients verfolgen" } } } diff --git a/homeassistant/components/hyperion/translations/de.json b/homeassistant/components/hyperion/translations/de.json index 95c0f1734cc..a27abb7ff5f 100644 --- a/homeassistant/components/hyperion/translations/de.json +++ b/homeassistant/components/hyperion/translations/de.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Hyperion-Effekte zum Anzeigen", "priority": "Hyperion-Priorit\u00e4t f\u00fcr Farben und Effekte" } } diff --git a/homeassistant/components/ifttt/translations/ru.json b/homeassistant/components/ifttt/translations/ru.json index 2e7f83c6f61..8cf34ace24e 100644 --- a/homeassistant/components/ifttt/translations/ru.json +++ b/homeassistant/components/ifttt/translations/ru.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\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 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\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]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json index 6bbc4d5474f..84b53c4aa15 100644 --- a/homeassistant/components/insteon/translations/de.json +++ b/homeassistant/components/insteon/translations/de.json @@ -5,14 +5,17 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "select_single": "W\u00e4hlen Sie eine Option aus." }, "step": { "hubv1": { "data": { "host": "IP-Adresse", "port": "Port" - } + }, + "description": "Konfigurieren Sie den Insteon Hub Version 1 (vor 2014).", + "title": "Insteon Hub Version 1" }, "hubv2": { "data": { @@ -20,15 +23,22 @@ "password": "Passwort", "port": "Port", "username": "Benutzername" - } + }, + "description": "Konfigurieren Sie den Insteon Hub Version 2.", + "title": "Insteon Hub Version 2" }, "plm": { "data": { "device": "USB-Ger\u00e4te-Pfad" }, + "description": "Konfigurieren Sie das Insteon PowerLink Modem (PLM).", "title": "Insteon PLM" }, "user": { + "data": { + "modem_type": "Modemtyp." + }, + "description": "W\u00e4hlen Sie den Insteon-Modemtyp aus.", "title": "Insteon" } } @@ -36,16 +46,27 @@ "options": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "input_error": "Ung\u00fcltige Eingaben, bitte \u00fcberpr\u00fcfen Sie Ihre Werte.", "select_single": "W\u00e4hle eine Option aus." }, "step": { "add_override": { + "data": { + "address": "Ger\u00e4teadresse (z. B. 1a2b3c)", + "cat": "Ger\u00e4tekategorie (z. B. 0x10)", + "subcat": "Ger\u00e4teunterkategorie (z. B. 0x0a)" + }, + "description": "F\u00fcgen Sie eine Ger\u00e4te\u00fcberschreibung hinzu.", "title": "Insteon" }, "add_x10": { "data": { - "platform": "Plattform" + "housecode": "Hauscode (a - p)", + "platform": "Plattform", + "steps": "Dimmerstufen (nur f\u00fcr Lichtger\u00e4te, Voreinstellung 22)", + "unitcode": "Unitcode (1 - 16)" }, + "description": "\u00c4ndern Sie das Insteon Hub-Passwort.", "title": "Insteon" }, "change_hub_config": { @@ -55,15 +76,32 @@ "port": "Port", "username": "Benutzername" }, + "description": "\u00c4ndern Sie die Verbindungsinformationen des Insteon-Hubs. Sie m\u00fcssen Home Assistant neu starten, nachdem Sie diese \u00c4nderung vorgenommen haben. Dies \u00e4ndert nicht die Konfiguration des Hubs selbst. Um die Konfiguration im Hub zu \u00e4ndern, verwenden Sie die Hub-App.", "title": "Insteon" }, "init": { + "data": { + "add_override": "F\u00fcgen Sie eine Ger\u00e4te\u00fcberschreibung hinzu.", + "add_x10": "F\u00fcgen Sie ein X10-Ger\u00e4t hinzu.", + "change_hub_config": "\u00c4ndern Sie die Konfiguration des Hubs.", + "remove_override": "Entfernen Sie eine Ger\u00e4te\u00fcbersteuerung.", + "remove_x10": "Entfernen Sie ein X10-Ger\u00e4t." + }, + "description": "W\u00e4hlen Sie eine Option zum Konfigurieren aus.", "title": "Insteon" }, "remove_override": { + "data": { + "address": "W\u00e4hlen Sie eine Ger\u00e4teadresse zum Entfernen" + }, + "description": "Entfernen einer Ger\u00e4te\u00fcbersteuerung", "title": "Insteon" }, "remove_x10": { + "data": { + "address": "W\u00e4hlen Sie eine Ger\u00e4teadresse zum Entfernen" + }, + "description": "Ein X10-Ger\u00e4t entfernen", "title": "Insteon" } } diff --git a/homeassistant/components/kmtronic/translations/de.json b/homeassistant/components/kmtronic/translations/de.json index 625c7372347..336348274ac 100644 --- a/homeassistant/components/kmtronic/translations/de.json +++ b/homeassistant/components/kmtronic/translations/de.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Umgekehrte Schalterlogik (NC verwenden)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json index b7bc7198987..4b3fbceb5d6 100644 --- a/homeassistant/components/life360/translations/ru.json +++ b/homeassistant/components/life360/translations/ru.json @@ -5,7 +5,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\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." + "default": "\u041e\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]({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": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", @@ -19,7 +19,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "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.", + "description": "\u041e\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]({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/locative/translations/ru.json b/homeassistant/components/locative/translations/ru.json index c9fc9cfd36a..dd353c25117 100644 --- a/homeassistant/components/locative/translations/ru.json +++ b/homeassistant/components/locative/translations/ru.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\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 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/logi_circle/translations/ru.json b/homeassistant/components/logi_circle/translations/ru.json index 8da20b60c39..621ce6863bb 100644 --- a/homeassistant/components/logi_circle/translations/ru.json +++ b/homeassistant/components/logi_circle/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "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.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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." + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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." }, "error": { "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.", diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index 75cdac79482..1950edd8ff2 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -32,9 +32,30 @@ "button_2": "Zweite Taste", "button_3": "Dritte Taste", "button_4": "Vierte Taste", + "lower_all": "Alle senken", "off": "Aus", "on": "An", + "open_1": "\u00d6ffnen 1", + "open_2": "\u00d6ffnen 2", + "open_3": "\u00d6ffnen 3", + "open_4": "\u00d6ffnen 4", + "open_all": "Alle \u00f6ffnen", + "raise": "Raise", + "raise_1": "Anheben 1", + "raise_2": "Anheben 2", + "raise_3": "Anheben 3", + "raise_4": "Anheben 4", + "raise_all": "Erhebe alle", + "stop": "Stop (Favorit)", + "stop_1": "Stop 1", + "stop_2": "Stop 2", + "stop_3": "Stop 3", + "stop_4": "Stop 4", "stop_all": "Alle anhalten" + }, + "trigger_type": { + "press": "\"{subtype}\" gedr\u00fcckt", + "release": "\" {subtype} \" freigegeben" } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/de.json b/homeassistant/components/lyric/translations/de.json index 5bab6ed132b..6f1660f4dd3 100644 --- a/homeassistant/components/lyric/translations/de.json +++ b/homeassistant/components/lyric/translations/de.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "reauth_successful": "Erneute Authentifizierung war erfolgreich" }, "create_entry": { "default": "Erfolgreich authentifiziert" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "description": "Die Lyric-Integration muss Ihr Konto neu authentifizieren.", + "title": "Integration erneut Authentifizieren" } } } diff --git a/homeassistant/components/lyric/translations/ru.json b/homeassistant/components/lyric/translations/ru.json index 3092d64b03f..d38cfbf4e0f 100644 --- a/homeassistant/components/lyric/translations/ru.json +++ b/homeassistant/components/lyric/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "create_entry": { diff --git a/homeassistant/components/mailgun/translations/ru.json b/homeassistant/components/mailgun/translations/ru.json index 740f616da4c..2e79776f89a 100644 --- a/homeassistant/components/mailgun/translations/ru.json +++ b/homeassistant/components/mailgun/translations/ru.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\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 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\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]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/mikrotik/translations/de.json b/homeassistant/components/mikrotik/translations/de.json index 82ea47dc4bf..1c11717c1b2 100644 --- a/homeassistant/components/mikrotik/translations/de.json +++ b/homeassistant/components/mikrotik/translations/de.json @@ -27,6 +27,7 @@ "device_tracker": { "data": { "arp_ping": "ARP-Ping aktivieren", + "detection_time": "Heimintervall ber\u00fccksichtigen", "force_dhcp": "Erzwingen Sie das Scannen \u00fcber DHCP" } } diff --git a/homeassistant/components/motioneye/translations/de.json b/homeassistant/components/motioneye/translations/de.json new file mode 100644 index 00000000000..8bd6663d2a8 --- /dev/null +++ b/homeassistant/components/motioneye/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "Der Service ist bereits eingerichtet", + "reauth_successful": "Erneute Authentifizierung erfolgreich" + }, + "error": { + "cannot_connect": "Fehler beim Verbinden", + "invalid_auth": "Authentifizerung fehlgeschlagen", + "invalid_url": "Ung\u00fcltige URL" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 4b57249eb38..3bd07c4507b 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -50,6 +50,8 @@ }, "options": { "error": { + "bad_birth": "Ung\u00fcltiges Birth Thema.", + "bad_will": "Ung\u00fcltiges will Thema.", "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { @@ -64,6 +66,7 @@ }, "options": { "data": { + "birth_payload": "Nutzdaten der Birth Nachricht", "discovery": "Erkennung aktivieren", "will_enable": "Letzten Willen aktivieren" }, diff --git a/homeassistant/components/mutesync/translations/de.json b/homeassistant/components/mutesync/translations/de.json new file mode 100644 index 00000000000..51edf2d9226 --- /dev/null +++ b/homeassistant/components/mutesync/translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "invalid_auth": "Aktivieren Sie die Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index d05e2bdb47b..c61c4177136 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -3,10 +3,16 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", + "duplicate_persistence_file": "Persistenzdatei wird bereits verwendet", + "duplicate_topic": "Thema bereits in Verwendung", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_device": "Ung\u00fcltiges Ger\u00e4t", "invalid_ip": "Ung\u00fcltige IP-Adresse", + "invalid_persistence_file": "Ung\u00fcltige Persistenzdatei", + "invalid_port": "Ung\u00fcltige Portnummer", + "invalid_publish_topic": "Ung\u00fcltiges Ver\u00f6ffentlichungsthema", "invalid_serial": "Ung\u00fcltiger Serieller Port", + "invalid_subscribe_topic": "Ung\u00fcltiges Abonnementthema", "invalid_version": "Ung\u00fcltige MySensors Version", "not_a_number": "Bitte eine Nummer eingeben", "unknown": "Unerwarteter Fehler" @@ -19,6 +25,7 @@ "invalid_ip": "Ung\u00fcltige IP-Adresse", "invalid_serial": "Ung\u00fcltiger Serieller Port", "invalid_version": "Ung\u00fcltige MySensors Version", + "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", "not_a_number": "Bitte eine Nummer eingeben", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/neato/translations/ru.json b/homeassistant/components/neato/translations/ru.json index 25bb616a638..430300126a3 100644 --- a/homeassistant/components/neato/translations/ru.json +++ b/homeassistant/components/neato/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "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.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, @@ -28,7 +28,7 @@ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "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.", + "description": "\u041e\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]({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" } } diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 0763b68a1be..4897114d286 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -3,7 +3,7 @@ "abort": { "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.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", diff --git a/homeassistant/components/netatmo/translations/ru.json b/homeassistant/components/netatmo/translations/ru.json index b25e0843967..1fadfb90773 100644 --- a/homeassistant/components/netatmo/translations/ru.json +++ b/homeassistant/components/netatmo/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json index 85de80d3dfa..612793ca059 100644 --- a/homeassistant/components/omnilogic/translations/de.json +++ b/homeassistant/components/omnilogic/translations/de.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH-Offset (positiv oder negativ)", "polling_interval": "Abfrageintervall (in Sekunden)" } } diff --git a/homeassistant/components/omnilogic/translations/es.json b/homeassistant/components/omnilogic/translations/es.json index 7e054159bf9..54aef5f1892 100644 --- a/homeassistant/components/omnilogic/translations/es.json +++ b/homeassistant/components/omnilogic/translations/es.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "Desplazamiento del pH (positivo o negativo)", "polling_interval": "Intervalo de sondeo (en segundos)" } } diff --git a/homeassistant/components/ondilo_ico/translations/ru.json b/homeassistant/components/ondilo_ico/translations/ru.json index 56bb2d342b7..199eb034c93 100644 --- a/homeassistant/components/ondilo_ico/translations/ru.json +++ b/homeassistant/components/ondilo_ico/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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." + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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." diff --git a/homeassistant/components/opentherm_gw/translations/de.json b/homeassistant/components/opentherm_gw/translations/de.json index 36b76592945..66636a73a26 100644 --- a/homeassistant/components/opentherm_gw/translations/de.json +++ b/homeassistant/components/opentherm_gw/translations/de.json @@ -21,7 +21,10 @@ "init": { "data": { "floor_temperature": "Boden-Temperatur", - "precision": "Genauigkeit" + "precision": "Genauigkeit", + "read_precision": "Pr\u00e4zision abfragen", + "set_precision": "Pr\u00e4zision einstellen", + "temporary_override_mode": "Tempor\u00e4rer Sollwert\u00fcbersteuerungsmodus" }, "description": "Optionen f\u00fcr das OpenTherm Gateway" } diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index cac601b71d3..da0305f633d 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -17,6 +17,7 @@ "mode": "Modus", "name": "Name der Integration" }, + "description": "Richten Sie die OpenWeatherMap-Integration ein. Zum Generieren des API-Schl\u00fcssels gehen Sie auf https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/owntracks/translations/ru.json b/homeassistant/components/owntracks/translations/ru.json index bc38694d2a8..09fdba77266 100644 --- a/homeassistant/components/owntracks/translations/ru.json +++ b/homeassistant/components/owntracks/translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\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 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/philips_js/translations/de.json b/homeassistant/components/philips_js/translations/de.json index 6288e9fb5c4..552d7aca07a 100644 --- a/homeassistant/components/philips_js/translations/de.json +++ b/homeassistant/components/philips_js/translations/de.json @@ -14,7 +14,8 @@ "data": { "pin": "PIN-Code" }, - "description": "Gib die auf deinem Fernseher angezeigten PIN ein" + "description": "Gib die auf deinem Fernseher angezeigten PIN ein", + "title": "Paaren" }, "user": { "data": { diff --git a/homeassistant/components/picnic/translations/de.json b/homeassistant/components/picnic/translations/de.json new file mode 100644 index 00000000000..2369d323479 --- /dev/null +++ b/homeassistant/components/picnic/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits eingerichtet" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "country_code": "L\u00e4ndercode", + "password": "Passwort", + "username": "Benutzername" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index 9a092ef4fa6..a95359abeaf 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -16,8 +16,10 @@ "step": { "api_method": { "data": { + "token": "F\u00fcgen Sie hier das Auth Token ein", "use_webhook": "Webhook verwenden" }, + "description": "Um die API abfragen zu k\u00f6nnen, wird ein `auth_token` ben\u00f6tigt, das durch folgende [diese](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) Anweisungen erhalten werden kann\n\n Ausgew\u00e4hltes Ger\u00e4t: **{Ger\u00e4tetyp}** \n\nWenn Sie lieber die eingebaute Webhook-Methode (nur Airlock) verwenden m\u00f6chten, setzen Sie bitte einen Haken und lassen Sie das Auth Token leer", "title": "API-Methode ausw\u00e4hlen" }, "user": { @@ -29,7 +31,8 @@ "title": "Plaato Webhook einrichten" }, "webhook": { - "description": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." + "description": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url}).", + "title": "Zu verwendender Webhook" } } }, diff --git a/homeassistant/components/plaato/translations/ru.json b/homeassistant/components/plaato/translations/ru.json index 99e1bf94e0d..9ff1977dc53 100644 --- a/homeassistant/components/plaato/translations/ru.json +++ b/homeassistant/components/plaato/translations/ru.json @@ -31,7 +31,7 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Plaato" }, "webhook": { - "description": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\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 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", + "description": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "title": "Webhook" } } diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 4d01be82b6a..97587131774 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -21,9 +21,11 @@ "data": { "host": "IP-Adresse", "password": "Smile ID", - "port": "Port" + "port": "Port", + "username": "Smile-Benutzername" }, - "description": "Bitte eingeben" + "description": "Bitte eingeben", + "title": "Stellen Sie eine Verbindung zu Smile her" } } }, @@ -32,7 +34,8 @@ "init": { "data": { "scan_interval": "Scanintervall (Sekunden)" - } + }, + "description": "Plugwise-Optionen einstellen" } } } diff --git a/homeassistant/components/point/translations/ru.json b/homeassistant/components/point/translations/ru.json index abb90240871..c29345af04d 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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "no_flows": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "unknown_authorize_url_generation": "\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." }, "create_entry": { diff --git a/homeassistant/components/progettihwsw/translations/de.json b/homeassistant/components/progettihwsw/translations/de.json index 0f773e03c1d..0d9823587a2 100644 --- a/homeassistant/components/progettihwsw/translations/de.json +++ b/homeassistant/components/progettihwsw/translations/de.json @@ -26,13 +26,15 @@ "relay_7": "Relais 7", "relay_8": "Relais 8", "relay_9": "Relais 9" - } + }, + "title": "Relais einrichten" }, "user": { "data": { "host": "Host", "port": "Port" - } + }, + "title": "Board einrichten" } } } diff --git a/homeassistant/components/ps4/translations/ru.json b/homeassistant/components/ps4/translations/ru.json index d39907d0179..ea42e260ec8 100644 --- a/homeassistant/components/ps4/translations/ru.json +++ b/homeassistant/components/ps4/translations/ru.json @@ -4,8 +4,8 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "credential_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", - "port_987_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 987. \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/ps4/).", - "port_997_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 997. \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/ps4/)." + "port_987_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 987. \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](https://www.home-assistant.io/components/ps4/).", + "port_997_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 997. \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](https://www.home-assistant.io/components/ps4/)." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -25,7 +25,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, - "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**. \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](https://www.home-assistant.io/components/ps4/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", + "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**. \u041e\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](https://www.home-assistant.io/components/ps4/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/rainmachine/translations/de.json b/homeassistant/components/rainmachine/translations/de.json index 511d85b36b6..7bfd228d717 100644 --- a/homeassistant/components/rainmachine/translations/de.json +++ b/homeassistant/components/rainmachine/translations/de.json @@ -20,6 +20,9 @@ "options": { "step": { "init": { + "data": { + "zone_run_time": "Standard-Zonenlaufzeit (in Sekunden)" + }, "title": "RainMachine konfigurieren" } } diff --git a/homeassistant/components/recollect_waste/translations/de.json b/homeassistant/components/recollect_waste/translations/de.json index 7cbcea1b25e..fdeab56f54e 100644 --- a/homeassistant/components/recollect_waste/translations/de.json +++ b/homeassistant/components/recollect_waste/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, + "error": { + "invalid_place_or_service_id": "Ung\u00fcltige Orts- oder Service-ID" + }, "step": { "user": { "data": { @@ -15,6 +18,9 @@ "options": { "step": { "init": { + "data": { + "friendly_name": "Verwenden Sie freundliche Namen f\u00fcr Pickup-Typen (wenn m\u00f6glich)" + }, "title": "Recollect Waste konfigurieren" } } diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index 36d808bd6de..8e50b61f16f 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -20,6 +20,16 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "Aktiv, abwesend", + "armed_custom_bypass": "Aktiv, benutzerdefiniert", + "armed_home": "Aktiv, zu Hause", + "armed_night": "Aktiv, Nacht" + }, + "description": "W\u00e4hlen Sie aus, in welchen Zustand Ihr Risco-Alarm versetzt werden soll, wenn Sie den Alarm des Home Assistant scharf schalten", + "title": "Home Assistant Zust\u00e4nde den Risco Zust\u00e4nden zuordnen" + }, "init": { "data": { "code_arm_required": "PIN-Code zum Entsperren vorgeben", @@ -31,8 +41,12 @@ "A": "Gruppe A", "B": "Gruppe B", "C": "Gruppe C", - "D": "Gruppe D" - } + "D": "Gruppe D", + "arm": "Aktiv, abwesend", + "partial_arm": "Teilweise aktiv (STAY)" + }, + "description": "W\u00e4hlen Sie aus, welchen Zustand Ihr Home Assistant-Alarm f\u00fcr jeden von Risco gemeldeten Zustand melden soll", + "title": "Risco-Zust\u00e4nde den Home Assistant-Zust\u00e4nden zuordnen" } } } diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index 780d406bcaf..b66a6681be7 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", - "not_irobot_device": "Das erkannte Ger\u00e4t ist kein iRobot-Ger\u00e4t" + "not_irobot_device": "Das erkannte Ger\u00e4t ist kein iRobot-Ger\u00e4t", + "short_blid": "Das BLID wurde abgeschnitten" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index 26e5c61faf3..c47c9ef8fd6 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -45,7 +45,7 @@ "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\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, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c BLID \u0438 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "description": "\u041e\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, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c BLID \u0438 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" } } diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index 4416589a23e..48f8fc456cf 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -9,10 +9,15 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "link": { + "description": "Sie m\u00fcssen den Home Assistant in Roon autorisieren. Nachdem Sie auf \"Submit\" geklickt haben, gehen Sie zur Roon Core-Anwendung, \u00f6ffnen Sie die Einstellungen und aktivieren Sie HomeAssistant auf der Registerkarte \"Extensions\".", + "title": "HomeAssistant in Roon autorisieren" + }, "user": { "data": { "host": "Host" - } + }, + "description": "Roon-Server konnte nicht gefunden werden, bitte geben Sie den Hostnamen oder die IP ein." } } } diff --git a/homeassistant/components/sma/translations/de.json b/homeassistant/components/sma/translations/de.json index 807645467de..bf1d325a8b4 100644 --- a/homeassistant/components/sma/translations/de.json +++ b/homeassistant/components/sma/translations/de.json @@ -12,11 +12,14 @@ "step": { "user": { "data": { + "group": "Gruppe", "host": "Host", "password": "Passwort", "ssl": "Verwendet ein SSL-Zertifikat", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" - } + }, + "description": "Geben Sie Ihre SMA-Ger\u00e4teinformationen ein.", + "title": "Richten Sie SMA Solar ein" } } } diff --git a/homeassistant/components/smappee/translations/ru.json b/homeassistant/components/smappee/translations/ru.json index 568abf4814e..3654f536a41 100644 --- a/homeassistant/components/smappee/translations/ru.json +++ b/homeassistant/components/smappee/translations/ru.json @@ -6,7 +6,7 @@ "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.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_mdns": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, "flow_title": "Smappee: {name}", diff --git a/homeassistant/components/smarttub/translations/de.json b/homeassistant/components/smarttub/translations/de.json index 8e608193b81..15e4057a888 100644 --- a/homeassistant/components/smarttub/translations/de.json +++ b/homeassistant/components/smarttub/translations/de.json @@ -9,6 +9,10 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "description": "Die SmartTub-Integration muss Ihr Konto neu authentifizieren", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "email": "E-Mail", diff --git a/homeassistant/components/soma/translations/ru.json b/homeassistant/components/soma/translations/ru.json index 119bd828c60..49ac4e65768 100644 --- a/homeassistant/components/soma/translations/ru.json +++ b/homeassistant/components/soma/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_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.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a SOMA Connect.", - "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 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\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.", "result_error": "SOMA Connect \u043e\u0442\u0432\u0435\u0442\u0438\u043b \u0441\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c \u043e\u0448\u0438\u0431\u043a\u0438." }, "create_entry": { diff --git a/homeassistant/components/somfy/translations/ru.json b/homeassistant/components/somfy/translations/ru.json index 0451cbbaa29..38ac0dda412 100644 --- a/homeassistant/components/somfy/translations/ru.json +++ b/homeassistant/components/somfy/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index db9363ec1f7..1ed426282fe 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -14,6 +14,7 @@ "title": "W\u00e4hle die Authentifizierungsmethode" }, "reauth_confirm": { + "description": "Die Spotify-Integration muss sich bei Spotify f\u00fcr das Konto neu authentifizieren: {Konto}", "title": "Integration erneut authentifizieren" } } diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index bac888937a5..fc9362697e2 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "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": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \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.", + "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "reauth_account_mismatch": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0443\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0435\u0439 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, diff --git a/homeassistant/components/srp_energy/translations/de.json b/homeassistant/components/srp_energy/translations/de.json index 302233d2923..45f8ed451a4 100644 --- a/homeassistant/components/srp_energy/translations/de.json +++ b/homeassistant/components/srp_energy/translations/de.json @@ -5,12 +5,15 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_account": "Die Konto-ID sollte eine 9-stellige Nummer sein", "invalid_auth": "Ung\u00fcltige Anmeldung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { + "id": "Konto-ID", + "is_tou": "Ist Nutzungszeitplan", "password": "Passwort", "username": "Benutzername" } diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json index 9c4a0bdf535..135b80b64ab 100644 --- a/homeassistant/components/subaru/translations/de.json +++ b/homeassistant/components/subaru/translations/de.json @@ -24,7 +24,19 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Bitte gib deine MySubaru-Anmeldedaten ein\nHINWEIS: Die Ersteinrichtung kann bis zu 30 Sekunden dauern" + "description": "Bitte gib deine MySubaru-Anmeldedaten ein\nHINWEIS: Die Ersteinrichtung kann bis zu 30 Sekunden dauern", + "title": "Subaru Starlink Konfiguration" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Aktivieren Sie die Fahrzeugabfrage" + }, + "description": "Wenn diese Option aktiviert ist, sendet die Fahrzeugabfrage alle 2 Stunden einen Fernbefehl an Ihr Fahrzeug, um neue Sensordaten zu erhalten. Ohne Fahrzeugabfrage werden neue Sensordaten nur empfangen, wenn das Fahrzeug automatisch Daten sendet (normalerweise nach dem Abstellen des Motors).", + "title": "Subaru Starlink Optionen" } } } diff --git a/homeassistant/components/tasmota/translations/de.json b/homeassistant/components/tasmota/translations/de.json index 30874708839..86c3383f912 100644 --- a/homeassistant/components/tasmota/translations/de.json +++ b/homeassistant/components/tasmota/translations/de.json @@ -5,6 +5,9 @@ }, "step": { "config": { + "data": { + "discovery_prefix": "Themenpr\u00e4fix f\u00fcr die Erkennung" + }, "description": "Bitte die Tasmota-Konfiguration einstellen.", "title": "Tasmota" }, diff --git a/homeassistant/components/toon/translations/ru.json b/homeassistant/components/toon/translations/ru.json index 162608a0f8c..1818c51bcdd 100644 --- a/homeassistant/components/toon/translations/ru.json +++ b/homeassistant/components/toon/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0433\u043b\u0430\u0448\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e.", "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.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "unknown_authorize_url_generation": "\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." diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json index 57dbaf77364..2f484f2a8ed 100644 --- a/homeassistant/components/totalconnect/translations/de.json +++ b/homeassistant/components/totalconnect/translations/de.json @@ -5,13 +5,16 @@ "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "invalid_auth": "Ung\u00fcltige Authentifizierung" + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "usercode": "Der Benutzercode ist an dieser Stelle f\u00fcr diesen Benutzer nicht g\u00fcltig" }, "step": { "locations": { "data": { "location": "Standort" - } + }, + "description": "Geben Sie den Benutzercode f\u00fcr diesen Benutzer an dieser Stelle ein", + "title": "Standort-Benutzercodes" }, "reauth_confirm": { "description": "Total Connect muss dein Konto neu authentifizieren", diff --git a/homeassistant/components/traccar/translations/ru.json b/homeassistant/components/traccar/translations/ru.json index dc4cd2cde11..7db0d6f01cf 100644 --- a/homeassistant/components/traccar/translations/ru.json +++ b/homeassistant/components/traccar/translations/ru.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Traccar.\n\n\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438: `{webhook_url}`\n\n\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 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Traccar.\n\n\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438: `{webhook_url}`\n\n\u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index c16a945200e..8aa7c08f352 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -27,6 +27,7 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { + "dev_multi_type": "Mehrere ausgew\u00e4hlte Ger\u00e4te zur Konfiguration m\u00fcssen vom gleichen Typ sein", "dev_not_config": "Ger\u00e4tetyp nicht konfigurierbar", "dev_not_found": "Ger\u00e4t nicht gefunden" }, @@ -34,14 +35,28 @@ "device": { "data": { "brightness_range_mode": "Vom Ger\u00e4t genutzter Helligkeitsbereich", + "curr_temp_divider": "Aktueller Temperaturwert-Teiler (0 = Standard verwenden)", "max_kelvin": "Maximal unterst\u00fctzte Farbtemperatur in Kelvin", "max_temp": "Maximale Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)", "min_kelvin": "Minimale unterst\u00fctzte Farbtemperatur in Kelvin", "min_temp": "Minimal Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)", "support_color": "Farbunterst\u00fctzung erzwingen", + "temp_divider": "Teiler f\u00fcr Temperaturwerte (0 = Standard verwenden)", "tuya_max_coltemp": "Vom Ger\u00e4t gemeldete maximale Farbtemperatur", "unit_of_measurement": "Vom Ger\u00e4t verwendete Temperatureinheit" - } + }, + "description": "Optionen zur Anpassung der angezeigten Informationen f\u00fcr das Ger\u00e4t `{Ger\u00e4tename}` konfigurieren", + "title": "Tuya-Ger\u00e4t konfigurieren" + }, + "init": { + "data": { + "discovery_interval": "Abfrageintervall f\u00fcr Ger\u00e4teabruf in Sekunden", + "list_devices": "W\u00e4hlen Sie die zu konfigurierenden Ger\u00e4te aus oder lassen Sie sie leer, um die Konfiguration zu speichern", + "query_device": "W\u00e4hlen Sie ein Ger\u00e4t aus, das die Abfragemethode f\u00fcr eine schnellere Statusaktualisierung verwendet.", + "query_interval": "Ger\u00e4teabrufintervall in Sekunden" + }, + "description": "Stellen Sie das Abfrageintervall nicht zu niedrig ein, sonst schlagen die Aufrufe fehl und erzeugen eine Fehlermeldung im Protokoll", + "title": "Tuya-Optionen konfigurieren" } } } diff --git a/homeassistant/components/twilio/translations/ru.json b/homeassistant/components/twilio/translations/ru.json index 69402e55a82..8d255d492a7 100644 --- a/homeassistant/components/twilio/translations/ru.json +++ b/homeassistant/components/twilio/translations/ru.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\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 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\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]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json index c196f53262d..2e8bf218e08 100644 --- a/homeassistant/components/twinkly/translations/de.json +++ b/homeassistant/components/twinkly/translations/de.json @@ -8,6 +8,10 @@ }, "step": { "user": { + "data": { + "host": "Host (oder IP-Adresse) Ihres twinkly-Ger\u00e4ts" + }, + "description": "Einrichten Ihrer Twinkly-Led-Kette", "title": "Twinkly" } } diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index b1d3e495f94..4c34101a7ce 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -30,6 +30,7 @@ "client_control": { "data": { "block_client": "Clients mit Netzwerkzugriffskontrolle", + "dpi_restrictions": "Zulassen der Steuerung von DPI-Einschr\u00e4nkungsgruppen", "poe_clients": "POE-Kontrolle von Clients zulassen" }, "description": "Konfigurieren Sie Client-Steuerelemente \n\nErstellen Sie Switches f\u00fcr Seriennummern, f\u00fcr die Sie den Netzwerkzugriff steuern m\u00f6chten.", @@ -38,6 +39,7 @@ "device_tracker": { "data": { "detection_time": "Zeit in Sekunden vom letzten Gesehenen bis zur Entfernung", + "ignore_wired_bug": "Deaktivieren der kabelgebundenen UniFi-Fehlerlogik", "ssid_filter": "W\u00e4hlen Sie SSIDs zur Verfolgung von drahtlosen Clients aus", "track_clients": "Nachverfolgen von Netzwerkclients", "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)", diff --git a/homeassistant/components/vera/translations/ru.json b/homeassistant/components/vera/translations/ru.json index 4f050ef3880..857ffaa5bf5 100644 --- a/homeassistant/components/vera/translations/ru.json +++ b/homeassistant/components/vera/translations/ru.json @@ -22,7 +22,7 @@ "exclude": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant.", "lights": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f \u0432 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435 \u0432 Home Assistant." }, - "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 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u0445: https://www.home-assistant.io/integrations/vera/.\n\u0414\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u043b\u044e\u0431\u044b\u0445 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Home Assistant. \u0427\u0442\u043e\u0431\u044b \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u043f\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0440\u043e\u0431\u0435\u043b.", + "description": "\u041e\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 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u0445: https://www.home-assistant.io/integrations/vera/.\n\u0414\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u043b\u044e\u0431\u044b\u0445 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Home Assistant. \u0427\u0442\u043e\u0431\u044b \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u043f\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0440\u043e\u0431\u0435\u043b.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 Vera" } } diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index 913317c88d7..e5d9c5a7bb8 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -7,7 +7,8 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "complete_pairing_failed": "Das Pairing konnte nicht abgeschlossen werden. Vergewissere dich, dass der eingegebene PIN korrekt ist und dass der Fernseher noch mit Strom versorgt wird und mit dem Netzwerk verbunden ist, bevor du es erneut versuchst." + "complete_pairing_failed": "Das Pairing konnte nicht abgeschlossen werden. Vergewissere dich, dass der eingegebene PIN korrekt ist und dass der Fernseher noch mit Strom versorgt wird und mit dem Netzwerk verbunden ist, bevor du es erneut versuchst.", + "existing_config_entry_found": "Ein bestehender Richten Sie das VIZIO SmartCast-Ger\u00e4t ein Config-Eintrag mit der gleichen Seriennummer wurde bereits konfiguriert. Sie m\u00fcssen den vorhandenen Eintrag l\u00f6schen, um diesen zu konfigurieren." }, "step": { "pair_tv": { diff --git a/homeassistant/components/waze_travel_time/translations/de.json b/homeassistant/components/waze_travel_time/translations/de.json index f5586b3d80d..ac2a911d233 100644 --- a/homeassistant/components/waze_travel_time/translations/de.json +++ b/homeassistant/components/waze_travel_time/translations/de.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Zielort", + "name": "Name", "origin": "Startort", "region": "Region" } diff --git a/homeassistant/components/waze_travel_time/translations/es.json b/homeassistant/components/waze_travel_time/translations/es.json index 2ae07164fe7..62325233cab 100644 --- a/homeassistant/components/waze_travel_time/translations/es.json +++ b/homeassistant/components/waze_travel_time/translations/es.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Destino", + "name": "Nombre", "origin": "Origen", "region": "Regi\u00f3n" }, diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json index d56e782279a..59838d9ee86 100644 --- a/homeassistant/components/wilight/translations/de.json +++ b/homeassistant/components/wilight/translations/de.json @@ -6,6 +6,7 @@ "flow_title": "WiLight: {name}", "step": { "confirm": { + "description": "M\u00f6chten Sie WiLight {name} einrichten? \n\n Es unterst\u00fctzt: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index d8cfd6c0b3b..15462008965 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f.", "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, "create_entry": { diff --git a/homeassistant/components/xbox/translations/ru.json b/homeassistant/components/xbox/translations/ru.json index b47907ffb86..5719a5d9d8a 100644 --- a/homeassistant/components/xbox/translations/ru.json +++ b/homeassistant/components/xbox/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "create_entry": { diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json index 6eaff2e87a3..6df9b3d09b2 100644 --- a/homeassistant/components/yeelight/translations/de.json +++ b/homeassistant/components/yeelight/translations/de.json @@ -25,10 +25,13 @@ "step": { "init": { "data": { + "model": "Modell (optional)", + "nightlight_switch": "Nachtlichtschalter verwenden", "save_on_change": "Status bei \u00c4nderung speichern", "transition": "\u00dcbergangszeit (ms)", "use_music_mode": "Musik-Modus aktivieren" - } + }, + "description": "Wenn Sie das Modell leer lassen, wird es automatisch erkannt." } } } diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index f1dea3341d7..dafe1777a9f 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Code f\u00fcr Scharfschaltaktionen erforderlich", + "alarm_failed_tries": "Die Anzahl aufeinanderfolgender fehlgeschlagener Codeeintr\u00e4ge, um einen Alarm auszul\u00f6sen", + "alarm_master_code": "Mastercode f\u00fcr die Alarmzentrale(n)", + "title": "Optionen f\u00fcr die Alarmsteuerung" + }, + "zha_options": { + "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", + "enable_identify_on_join": "Aktivieren Sie den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", + "title": "Globale Optionen" + } + }, "device_automation": { "action_type": { "squawk": "Kreischen", diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 4fc089b26e9..5655d67fd34 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "C\u00f3digo requerido para las acciones de armado", + "alarm_failed_tries": "El n\u00famero de entradas de c\u00f3digo fallidas consecutivas para activar una alarma", + "alarm_master_code": "C\u00f3digo maestro de la(s) central(es) de alarma", + "title": "Opciones del panel de control de la alarma" + }, + "zha_options": { + "default_light_transition": "Tiempo de transici\u00f3n de la luz por defecto (segundos)", + "enable_identify_on_join": "Activar el efecto de identificaci\u00f3n cuando los dispositivos se unen a la red", + "title": "Opciones globales" + } + }, "device_automation": { "action_type": { "squawk": "Squawk", diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index c1ad9b82262..ee52575dacd 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "\u8b66\u6212\u52d5\u4f5c\u9700\u8981\u4ee3\u78bc", + "alarm_failed_tries": "\u9023\u7e8c\u8f38\u5165\u4ee3\u78bc\u89f8\u767c\u8b66\u5831\u932f\u8aa4\u6b21\u6578", + "alarm_master_code": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u63a7\u5236\u78bc", + "title": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u9078\u9805" + }, + "zha_options": { + "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", + "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", + "title": "Global \u9078\u9805" + } + }, "device_automation": { "action_type": { "squawk": "\u61c9\u7b54", diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index ae9293cf926..b0af15e6637 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -1,8 +1,10 @@ { "config": { "abort": { + "addon_get_discovery_info_failed": "Z-Wave-JS-Add-on-Discovery-Informationen konnten nicht abgerufen werden.", "addon_info_failed": "Fehler beim Abrufen von Z-Wave JS Add-on Informationen.", "addon_install_failed": "Installation des Z-Wave JS Add-Ons fehlgeschlagen.", + "addon_missing_discovery_info": "Fehlende Informationen zur Erkennung des Z-Wave JS-Add-Ons.", "addon_set_config_failed": "Setzen der Z-Wave JS Konfiguration fehlgeschlagen", "addon_start_failed": "Starten des Z-Wave JS Add-ons fehlgeschlagen.", "already_configured": "Ger\u00e4t ist bereits konfiguriert", From 1c8d9ca68ba3567a95fd2f941d24d51b21c45846 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 May 2021 17:57:42 -1000 Subject: [PATCH 107/852] Check exception causes for matching strings during recorder migration (#49999) --- .../components/recorder/migration.py | 21 +++++++----- tests/components/recorder/test_migrate.py | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 6c84e110f47..17b6e277614 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -17,6 +17,17 @@ from .util import session_scope _LOGGER = logging.getLogger(__name__) +def raise_if_exception_missing_str(ex, match_substrs): + """Raise an exception if the exception and cause do not contain the match substrs.""" + lower_ex_strs = [str(ex).lower(), str(ex.__cause__).lower()] + for str_sub in match_substrs: + for exc_str in lower_ex_strs: + if exc_str and str_sub in exc_str: + return + + raise ex + + def get_schema_version(instance): """Get the schema version.""" with session_scope(session=instance.get_session()) as session: @@ -80,11 +91,7 @@ def _create_index(connection, table_name, index_name): try: index.create(connection) except (InternalError, ProgrammingError, OperationalError) as err: - lower_err_str = str(err).lower() - - if "already exists" not in lower_err_str and "duplicate" not in lower_err_str: - raise - + raise_if_exception_missing_str(err, ["already exists", "duplicate"]) _LOGGER.warning( "Index %s already exists on %s, continuing", index_name, table_name ) @@ -199,9 +206,7 @@ def _add_columns(connection, table_name, columns_def): ) ) except (InternalError, OperationalError) as err: - if "duplicate" not in str(err).lower(): - raise - + raise_if_exception_missing_str(err, ["duplicate"]) _LOGGER.warning( "Column %s already exists on %s, continuing", column_def.split(" ")[1], diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 59695b631e1..ae7510ee979 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -326,3 +326,36 @@ def test_forgiving_add_index_with_other_db_types(caplog, exception_type): assert "already exists on states" in caplog.text assert "continuing" in caplog.text + + +class MockPyODBCProgrammingError(Exception): + """A mock pyodbc error.""" + + +def test_raise_if_exception_missing_str(): + """Test we raise an exception if strings are not present.""" + programming_exc = ProgrammingError("select * from;", Mock(), Mock()) + programming_exc.__cause__ = MockPyODBCProgrammingError( + "[42S11] [FreeTDS][SQL Server]The operation failed because an index or statistics with name 'ix_states_old_state_id' already exists on table 'states'. (1913) (SQLExecDirectW)" + ) + + migration.raise_if_exception_missing_str( + programming_exc, ["already exists", "duplicate"] + ) + + with pytest.raises(ProgrammingError): + migration.raise_if_exception_missing_str(programming_exc, ["not present"]) + + +def test_raise_if_exception_missing_empty_cause_str(): + """Test we raise an exception if strings are not present with an empty cause.""" + programming_exc = ProgrammingError("select * from;", Mock(), Mock()) + programming_exc.__cause__ = MockPyODBCProgrammingError() + + with pytest.raises(ProgrammingError): + migration.raise_if_exception_missing_str( + programming_exc, ["already exists", "duplicate"] + ) + + with pytest.raises(ProgrammingError): + migration.raise_if_exception_missing_str(programming_exc, ["not present"]) From 7c7a56f704d918730aa8653acd486e35753b8d87 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 3 May 2021 06:58:14 +0300 Subject: [PATCH 108/852] Fix Shelly external sensors invalid 999 value (#49994) --- homeassistant/components/shelly/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index a7d2e1e72ce..9337011ba16 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -135,12 +135,14 @@ SENSORS = { unit=temperature_unit, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_TEMPERATURE, + available=lambda block: block.extTemp != 999, ), ("sensor", "humidity"): BlockAttributeDescription( name="Humidity", unit=PERCENTAGE, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_HUMIDITY, + available=lambda block: block.extTemp != 999, ), ("sensor", "luminosity"): BlockAttributeDescription( name="Luminosity", From 8ca6b8394cd0c1f06c36426c1357f3770b8a19f0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 May 2021 06:07:26 +0200 Subject: [PATCH 109/852] Correct the selector for frontend.set_theme service (#49952) --- homeassistant/components/frontend/services.yaml | 7 ++++--- script/hassfest/services.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml index 85d3cf2a821..0f4948f4bf9 100644 --- a/homeassistant/components/frontend/services.yaml +++ b/homeassistant/components/frontend/services.yaml @@ -17,9 +17,10 @@ set_theme: default: "light" example: "dark" selector: - options: - - "dark" - - "light" + select: + options: + - "dark" + - "light" reload_themes: name: Reload themes diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 9577d134ccc..9c28962fbad 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -69,7 +69,7 @@ def validate_services(integration: Integration): has_services = grep_dir( integration.path, "**/*.py", - r"(hass\.services\.(register|async_register))|async_register_entity_service", + r"(hass\.services\.(register|async_register))|async_register_entity_service|async_register_admin_service", ) if not has_services: From 8e0e1405e8e3564fb93d8c87254be5d1aba0a3a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 May 2021 21:49:51 -0700 Subject: [PATCH 110/852] Make hassfest service validation faster (#50003) --- script/hassfest/services.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 9c28962fbad..a5d10f8dda5 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -65,25 +65,23 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> bool def validate_services(integration: Integration): """Validate services.""" - # Find if integration uses services - has_services = grep_dir( - integration.path, - "**/*.py", - r"(hass\.services\.(register|async_register))|async_register_entity_service|async_register_admin_service", - ) - - if not has_services: - return - try: data = load_yaml(str(integration.path / "services.yaml")) except FileNotFoundError: - integration.add_error("services", "Registers services but has no services.yaml") + # Find if integration uses services + has_services = grep_dir( + integration.path, + "**/*.py", + r"(hass\.services\.(register|async_register))|async_register_entity_service|async_register_admin_service", + ) + + if has_services: + integration.add_error( + "services", "Registers services but has no services.yaml" + ) return except HomeAssistantError: - integration.add_error( - "services", "Registers services but unable to load services.yaml" - ) + integration.add_error("services", "Unable to load services.yaml") return try: From 42c48e8cf93ac5b46ba78cd0b9673a0f134ef210 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 May 2021 18:52:48 -1000 Subject: [PATCH 111/852] Add reauth support to myq (#49998) --- .coveragerc | 1 + homeassistant/components/myq/__init__.py | 7 +- homeassistant/components/myq/binary_sensor.py | 2 +- homeassistant/components/myq/config_flow.py | 113 ++++++++++-------- homeassistant/components/myq/cover.py | 8 +- homeassistant/components/myq/strings.json | 10 +- .../components/myq/translations/en.json | 10 +- tests/components/myq/test_config_flow.py | 95 +++++++++++---- 8 files changed, 161 insertions(+), 85 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9c030123f72..deadd4e0b19 100644 --- a/.coveragerc +++ b/.coveragerc @@ -656,6 +656,7 @@ omit = homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py homeassistant/components/mystrom/switch.py + homeassistant/components/myq/__init__.py homeassistant/components/n26/* homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index fd3a46bbb5a..a299968712a 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -8,7 +8,7 @@ from pymyq.errors import InvalidCredentialsError, MyQError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -27,8 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: myq = await pymyq.login(conf[CONF_USERNAME], conf[CONF_PASSWORD], websession) except InvalidCredentialsError as err: - _LOGGER.error("There was an error while logging in: %s", err) - return False + raise ConfigEntryAuthFailed from err except MyQError as err: raise ConfigEntryNotReady from err @@ -37,6 +36,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_update_data(): try: return await myq.update_device_info() + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed from err except MyQError as err: raise UpdateFailed(str(err)) from err diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index e3832458b9b..b1b3680343d 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for device in myq.gateways.values(): entities.append(MyQBinarySensorEntity(coordinator, device)) - async_add_entities(entities, True) + async_add_entities(entities) class MyQBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index fa3d6b502cc..78a751a18b1 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -5,7 +5,7 @@ import pymyq from pymyq.errors import InvalidCredentialsError, MyQError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client @@ -18,72 +18,81 @@ DATA_SCHEMA = vol.Schema( ) -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. - """ - - websession = aiohttp_client.async_get_clientsession(hass) - - try: - await pymyq.login(data[CONF_USERNAME], data[CONF_PASSWORD], websession) - except InvalidCredentialsError as err: - raise InvalidAuth from err - except MyQError as err: - raise CannotConnect from err - - return {"title": data[CONF_USERNAME]} - - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for MyQ.""" VERSION = 1 + def __init__(self): + """Start a myq config flow.""" + self._reauth_unique_id = None + + async def _async_validate_input(self, username, password): + """Validate the user input allows us to connect.""" + websession = aiohttp_client.async_get_clientsession(self.hass) + try: + await pymyq.login(username, password, websession) + except InvalidCredentialsError: + return {CONF_PASSWORD: "invalid_auth"} + except MyQError: + return {"base": "cannot_connect"} + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return {"base": "unknown"} + + return None + 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) - 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" - - if "base" not in errors: + errors = await self._async_validate_input( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) + if not errors: await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() - return self.async_create_entry(title=info["title"], data=user_input) + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, discovery_info): - """Handle HomeKit discovery.""" - if self._async_current_entries(): - # We can see myq on the network to tell them to configure - # it, but since the device will not give up the account it is - # bound to and there can be multiple myq gateways on a single - # account, we avoid showing the device as discovered once - # they already have one configured as they can always - # add a new one via "+" - return self.async_abort(reason="already_configured") - properties = { - key.lower(): value for (key, value) in discovery_info["properties"].items() - } - await self.async_set_unique_id(properties["id"]) - return await self.async_step_user() + async def async_step_reauth(self, user_input=None): + """Handle reauth.""" + self._reauth_unique_id = self.context["unique_id"] + return await self.async_step_reauth_confirm() + async def async_step_reauth_confirm(self, user_input=None): + """Handle reauth input.""" + errors = {} + existing_entry = await self.async_set_unique_id(self._reauth_unique_id) + if user_input is not None: + errors = await self._async_validate_input( + existing_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD] + ) + if not errors: + self.hass.config_entries.async_update_entry( + existing_entry, + data={ + **existing_entry.data, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" + return self.async_show_form( + description_placeholders={ + CONF_USERNAME: existing_entry.data[CONF_USERNAME] + }, + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index e26a969e724..3d587635f2d 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator = data[MYQ_COORDINATOR] async_add_entities( - [MyQDevice(coordinator, device) for device in myq.covers.values()], True + [MyQDevice(coordinator, device) for device in myq.covers.values()] ) @@ -158,9 +158,3 @@ class MyQDevice(CoordinatorEntity, CoverEntity): if self._device.parent_device_id: device_info["via_device"] = (DOMAIN, self._device.parent_device_id) return device_info - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) diff --git a/homeassistant/components/myq/strings.json b/homeassistant/components/myq/strings.json index 19717907b0f..e8a0baa85ff 100644 --- a/homeassistant/components/myq/strings.json +++ b/homeassistant/components/myq/strings.json @@ -7,7 +7,14 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } - } + }, + "reauth_confirm": { + "description": "The password for {username} is no longer valid.", + "title": "Reauthenticate your MyQ Account", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -15,6 +22,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } diff --git a/homeassistant/components/myq/translations/en.json b/homeassistant/components/myq/translations/en.json index 9dad2d10cad..5dc6d811c87 100644 --- a/homeassistant/components/myq/translations/en.json +++ b/homeassistant/components/myq/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,6 +10,13 @@ "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The password for {username} is no longer valid.", + "title": "Reauthenticate your MyQ Account" + }, "user": { "data": { "password": "Password", diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index 3ae2da82f46..3b0d79f6f03 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -57,7 +57,7 @@ async def test_form_invalid_auth(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} + assert result2["errors"] == {"password": "invalid_auth"} async def test_form_cannot_connect(hass): @@ -79,32 +79,87 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_homekit(hass): - """Test that we abort from homekit if myq is already setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - +async def test_form_unknown_exception(hass): + """Test we handle unknown exceptions.""" result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HOMEKIT}, - data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" - assert result["errors"] == {} - flow = next( - flow - for flow in hass.config_entries.flow.async_progress() - if flow["flow_id"] == result["flow_id"] - ) - assert flow["context"]["unique_id"] == "AA:BB:CC:DD:EE:FF" + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_reauth(hass): + """Test we can reauth.""" entry = MockConfigEntry( - domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} + domain=DOMAIN, + data={ + CONF_USERNAME: "test@test.org", + CONF_PASSWORD: "secret", + }, + unique_id="test@test.org", ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_HOMEKIT}, - data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, + context={"source": config_entries.SOURCE_REAUTH, "unique_id": "test@test.org"}, ) - assert result["type"] == "abort" + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", + side_effect=InvalidCredentialsError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"password": "invalid_auth"} + + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", + side_effect=MyQError, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", + return_value=True, + ), patch( + "homeassistant.components.myq.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert mock_setup_entry.called + assert result4["type"] == "abort" + assert result4["reason"] == "reauth_successful" From 301d4b065774d352f0369aec78e0c57d8fbc3cbb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 May 2021 07:11:57 +0200 Subject: [PATCH 112/852] Fix saving a scene (#49980) Co-authored-by: Paulus Schoutsen --- homeassistant/components/config/scene.py | 34 ++++++++-------- tests/components/config/test_scene.py | 49 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 8507fbbe47d..41b8dce0957 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -1,5 +1,4 @@ """Provide configuration end points for Scenes.""" -from collections import OrderedDict import uuid from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA @@ -48,24 +47,9 @@ class EditSceneConfigView(EditIdBasedConfigView): def _write_value(self, hass, data, config_key, new_value): """Set value.""" - index = None - for index, cur_value in enumerate(data): - # When people copy paste their scenes to the config file, - # they sometimes forget to add IDs. Fix it here. - if CONF_ID not in cur_value: - cur_value[CONF_ID] = uuid.uuid4().hex - - elif cur_value[CONF_ID] == config_key: - break - else: - cur_value = {} - cur_value[CONF_ID] = config_key - index = len(data) - data.append(cur_value) - # Iterate through some keys that we want to have ordered in the output - updated_value = OrderedDict() - for key in ("id", "name", "entities"): + updated_value = {CONF_ID: config_key} + for key in ("name", "entities"): if key in new_value: updated_value[key] = new_value[key] @@ -73,4 +57,16 @@ class EditSceneConfigView(EditIdBasedConfigView): # supporting more fields in the future. updated_value.update(new_value) - data[index] = updated_value + updated = False + for index, cur_value in enumerate(data): + # When people copy paste their scenes to the config file, + # they sometimes forget to add IDs. Fix it here. + if CONF_ID not in cur_value: + cur_value[CONF_ID] = uuid.uuid4().hex + + elif cur_value[CONF_ID] == config_key: + data[index] = updated_value + updated = True + + if not updated: + data.append(updated_value) diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index 8e4276cc9fd..429fb807883 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -8,6 +8,55 @@ from homeassistant.helpers import entity_registry as er from homeassistant.util.yaml import dump +async def test_create_scene(hass, hass_client): + """Test creating a scene.""" + with patch.object(config, "SECTIONS", ["scene"]): + await async_setup_component(hass, "config", {}) + + client = await hass_client() + + def mock_read(path): + """Mock reading data.""" + return None + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + data = dump(data) + written.append(data) + + with patch("homeassistant.components.config._read", mock_read), patch( + "homeassistant.components.config._write", mock_write + ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): + resp = await client.post( + "/api/config/scene/config/light_off", + data=json.dumps( + { + # "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ), + ) + + assert resp.status == 200 + result = await resp.json() + assert result == {"result": "ok"} + + assert len(written) == 1 + written_yaml = written[0] + assert ( + written_yaml + == """- id: light_off + name: Lights off + entities: + light.bedroom: + state: 'off' +""" + ) + + async def test_update_scene(hass, hass_client): """Test updating a scene.""" with patch.object(config, "SECTIONS", ["scene"]): From a4432557d389fbdbc83e0865cba2840a6a1d1a55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 May 2021 19:48:49 -1000 Subject: [PATCH 113/852] Defer writing http config until after startup has calmed down (#50000) --- homeassistant/components/http/__init__.py | 4 ++-- tests/components/http/test_init.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 8ebb0397579..ed3a9510f5a 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -64,7 +64,7 @@ MAX_CLIENT_SIZE: int = 1024 ** 2 * 16 STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 - +SAVE_DELAY = 180 HTTP_SCHEMA = vol.All( cv.deprecated(CONF_BASE_URL), @@ -371,7 +371,7 @@ async def start_http_server_and_save_config( str(ip.network_address) for ip in conf[CONF_TRUSTED_PROXIES] ] - await store.async_save(conf) + store.async_delay_save(lambda: conf, SAVE_DELAY) current_request: ContextVar[web.Request | None] = ContextVar( diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 993f0dba1fd..65f01118c71 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,4 +1,5 @@ """The tests for the Home Assistant HTTP component.""" +from datetime import timedelta from ipaddress import ip_network import logging from unittest.mock import Mock, patch @@ -7,8 +8,11 @@ import pytest import homeassistant.components.http as http from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.ssl import server_context_intermediate, server_context_modern +from tests.common import async_fire_time_changed + @pytest.fixture def mock_stack(): @@ -189,6 +193,10 @@ async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port): assert await async_setup_component(hass, http.DOMAIN, {http.DOMAIN: config}) await hass.async_start() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + restored = await hass.components.http.async_get_last_config() restored["trusted_proxies"][0] = ip_network(restored["trusted_proxies"][0]) From 0a38827544c6512b79aaf2f67ff6bc9a3fb7bc80 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 3 May 2021 09:49:13 +0300 Subject: [PATCH 114/852] Fix Shelly battery operated devices value rounding (#49966) --- homeassistant/components/shelly/entity.py | 35 ++++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 48d37312225..44ef41b82b6 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -7,7 +7,6 @@ from typing import Any, Callable import aioshelly -from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.helpers import ( device_registry, @@ -38,7 +37,7 @@ async def async_setup_entry_attribute_entities( ) else: await async_restore_block_attribute_entities( - hass, config_entry, async_add_entities, wrapper, sensor_class + hass, config_entry, async_add_entities, wrapper, sensors, sensor_class ) @@ -80,7 +79,7 @@ async def async_setup_block_attribute_entities( async def async_restore_block_attribute_entities( - hass, config_entry, async_add_entities, wrapper, sensor_class + hass, config_entry, async_add_entities, wrapper, sensors, sensor_class ): """Restore block attributes entities.""" entities = [] @@ -104,7 +103,9 @@ async def async_restore_block_attribute_entities( device_class=entry.device_class, ) - entities.append(sensor_class(wrapper, None, attribute, description, entry)) + entities.append( + sensor_class(wrapper, None, attribute, description, entry, sensors) + ) if not entities: return @@ -162,7 +163,7 @@ class RestAttributeDescription: name: str icon: str | None = None unit: str | None = None - value: Callable[[dict, Any], Any] = None + value: Callable[[dict, Any], Any] | None = None device_class: str | None = None default_enabled: bool = True extra_state_attributes: Callable[[dict], dict | None] | None = None @@ -238,8 +239,8 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): if callable(unit): unit = unit(block.info(attribute)) - self._unit = unit - self._unique_id = f"{super().unique_id}-{self.attribute}" + self._unit: None | str | Callable[[dict], str] = unit + self._unique_id: None | str = f"{super().unique_id}-{self.attribute}" self._name = get_entity_name(wrapper.device, block, self.description.name) @property @@ -359,7 +360,7 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): return f"{self.wrapper.mac}-{self.attribute}" @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict | None: """Return the state attributes.""" if self.description.extra_state_attributes is None: return None @@ -377,9 +378,11 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti block: aioshelly.Block, attribute: str, description: BlockAttributeDescription, - entry: ConfigEntry | None = None, + entry: entity_registry.RegistryEntry | None = None, + sensors: set | None = None, ) -> None: """Initialize the sleeping sensor.""" + self.sensors = sensors self.last_state = None self.wrapper = wrapper self.attribute = attribute @@ -395,7 +398,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti self._name = get_entity_name( self.wrapper.device, block, self.description.name ) - else: + elif entry is not None: self._unique_id = entry.unique_id self._name = entry.original_name @@ -411,7 +414,11 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti @callback def _update_callback(self): """Handle device update.""" - if self.block is not None or not self.wrapper.device.initialized: + if ( + self.block is not None + or not self.wrapper.device.initialized + or self.sensors is None + ): super()._update_callback() return @@ -425,7 +432,13 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti if sensor_id != entity_sensor: continue + description = self.sensors.get((block.type, sensor_id)) + if description is None: + continue + self.block = block + self.description = description + _LOGGER.debug("Entity %s attached to block", self.name) super()._update_callback() return From a29dfe0bf5f86fa5421fc38487b25f09a28f1645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Hellstr=C3=B6m?= Date: Mon, 3 May 2021 10:21:57 +0200 Subject: [PATCH 115/852] Update smhi package to 1.0.15 (#49987) --- homeassistant/components/smhi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index 9d762df831d..7f6fd46e7ac 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -3,7 +3,7 @@ "name": "SMHI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smhi", - "requirements": ["smhi-pkg==1.0.13"], + "requirements": ["smhi-pkg==1.0.15"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 159252d6da8..181d5252128 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2102,7 +2102,7 @@ smarthab==0.21 # smbus-cffi==0.5.1 # homeassistant.components.smhi -smhi-pkg==1.0.13 +smhi-pkg==1.0.15 # homeassistant.components.snapcast snapcast==2.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e22c4a41ce..214a20893e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1116,7 +1116,7 @@ smart-meter-texas==0.4.0 smarthab==0.21 # homeassistant.components.smhi -smhi-pkg==1.0.13 +smhi-pkg==1.0.15 # homeassistant.components.solaredge solaredge==0.0.2 From 9d08178ed1e439bea156a660c93ac67d126e8160 Mon Sep 17 00:00:00 2001 From: Unai Date: Mon, 3 May 2021 10:22:47 +0200 Subject: [PATCH 116/852] Upgrade maxcube-api dependency to 0.4.3 (#49982) This new version only contains improvements in logs to try to debug issue #49482 --- homeassistant/components/maxcube/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index ba263b5e0d9..fa4bcc44cc6 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -2,7 +2,7 @@ "domain": "maxcube", "name": "eQ-3 MAX!", "documentation": "https://www.home-assistant.io/integrations/maxcube", - "requirements": ["maxcube-api==0.4.2"], + "requirements": ["maxcube-api==0.4.3"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 181d5252128..eda053c2417 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -918,7 +918,7 @@ magicseaweed==1.0.3 matrix-client==0.3.2 # homeassistant.components.maxcube -maxcube-api==0.4.2 +maxcube-api==0.4.3 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 214a20893e7..ca9c8ad9e35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -490,7 +490,7 @@ logi_circle==0.2.2 luftdaten==0.6.4 # homeassistant.components.maxcube -maxcube-api==0.4.2 +maxcube-api==0.4.3 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 From e8446cb4d93213f25794835109584b88aa410f23 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 3 May 2021 01:43:23 -0700 Subject: [PATCH 117/852] Fix types for shell command (#50004) --- homeassistant/components/shell_command/__init__.py | 6 ++++-- mypy.ini | 2 +- script/hassfest/mypy_config.py | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index a86bf5b3566..dc0deef1b82 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -1,4 +1,6 @@ """Expose regular shell commands as services.""" +from __future__ import annotations + import asyncio from contextlib import suppress import logging @@ -26,7 +28,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the shell_command component.""" conf = config.get(DOMAIN, {}) - cache = {} + cache: dict[str, tuple[str, str | None, template.Template | None]] = {} async def async_service_handler(service: ServiceCall) -> None: """Execute a shell command service.""" @@ -89,7 +91,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) if process: with suppress(TypeError): - await process.kill() + process.kill() del process return diff --git a/mypy.ini b/mypy.ini index e5050ba7f34..636bb1589cb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -61,5 +61,5 @@ warn_return_any = false warn_unreachable = false warn_unused_ignores = false -[mypy-homeassistant.components.adguard.*,homeassistant.components.aemet.*,homeassistant.components.airly.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.amcrest.*,homeassistant.components.analytics.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aurora.*,homeassistant.components.awair.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.blueprint.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.bsblan.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.config.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.deconz.*,homeassistant.components.demo.*,homeassistant.components.denonavr.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dhcp.*,homeassistant.components.directv.*,homeassistant.components.doorbird.*,homeassistant.components.dsmr.*,homeassistant.components.dynalite.*,homeassistant.components.eafm.*,homeassistant.components.edl21.*,homeassistant.components.elkm1.*,homeassistant.components.emonitor.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.esphome.*,homeassistant.components.evohome.*,homeassistant.components.fan.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.flo.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.freebox.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.garmin_connect.*,homeassistant.components.geniushub.*,homeassistant.components.gios.*,homeassistant.components.glances.*,homeassistant.components.gogogate2.*,homeassistant.components.google_assistant.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.gpmdp.*,homeassistant.components.gree.*,homeassistant.components.growatt_server.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.hdmi_cec.*,homeassistant.components.here_travel_time.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.honeywell.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.image.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.insteon.*,homeassistant.components.ipp.*,homeassistant.components.isy994.*,homeassistant.components.izone.*,homeassistant.components.kaiterra.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.lifx.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.lovelace.*,homeassistant.components.luftdaten.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lyric.*,homeassistant.components.marytts.*,homeassistant.components.media_source.*,homeassistant.components.melcloud.*,homeassistant.components.meteo_france.*,homeassistant.components.metoffice.*,homeassistant.components.minecraft_server.*,homeassistant.components.mobile_app.*,homeassistant.components.modbus.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mqtt.*,homeassistant.components.mullvad.*,homeassistant.components.mysensors.*,homeassistant.components.n26.*,homeassistant.components.neato.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netio.*,homeassistant.components.nightscout.*,homeassistant.components.nilu.*,homeassistant.components.nmap_tracker.*,homeassistant.components.norway_air.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nuki.*,homeassistant.components.nws.*,homeassistant.components.nzbget.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onvif.*,homeassistant.components.ovo_energy.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_viera.*,homeassistant.components.philips_js.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.plaato.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.point.*,homeassistant.components.profiler.*,homeassistant.components.proxmoxve.*,homeassistant.components.rachio.*,homeassistant.components.rainmachine.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.reddit.*,homeassistant.components.ring.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.roku.*,homeassistant.components.rpi_power.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.sabnzbd.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.search.*,homeassistant.components.sense.*,homeassistant.components.sentry.*,homeassistant.components.sesame.*,homeassistant.components.sharkiq.*,homeassistant.components.shell_command.*,homeassistant.components.shelly.*,homeassistant.components.sma.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.solaredge.*,homeassistant.components.solarlog.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.spotify.*,homeassistant.components.stream.*,homeassistant.components.stt.*,homeassistant.components.surepetcare.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tasmota.*,homeassistant.components.tcp.*,homeassistant.components.telegram_bot.*,homeassistant.components.template.*,homeassistant.components.tesla.*,homeassistant.components.timer.*,homeassistant.components.todoist.*,homeassistant.components.toon.*,homeassistant.components.tplink.*,homeassistant.components.trace.*,homeassistant.components.tradfri.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.unifi.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.velbus.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.vizio.*,homeassistant.components.volumio.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.wink.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wunderground.*,homeassistant.components.xbox.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.yamaha.*,homeassistant.components.yeelight.*,homeassistant.components.zerproc.*,homeassistant.components.zha.*,homeassistant.components.zwave.*] +[mypy-homeassistant.components.adguard.*,homeassistant.components.aemet.*,homeassistant.components.airly.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.amcrest.*,homeassistant.components.analytics.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aurora.*,homeassistant.components.awair.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.blueprint.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.bsblan.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.config.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.deconz.*,homeassistant.components.demo.*,homeassistant.components.denonavr.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dhcp.*,homeassistant.components.directv.*,homeassistant.components.doorbird.*,homeassistant.components.dsmr.*,homeassistant.components.dynalite.*,homeassistant.components.eafm.*,homeassistant.components.edl21.*,homeassistant.components.elkm1.*,homeassistant.components.emonitor.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.esphome.*,homeassistant.components.evohome.*,homeassistant.components.fan.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.flo.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.freebox.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.garmin_connect.*,homeassistant.components.geniushub.*,homeassistant.components.gios.*,homeassistant.components.glances.*,homeassistant.components.gogogate2.*,homeassistant.components.google_assistant.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.gpmdp.*,homeassistant.components.gree.*,homeassistant.components.growatt_server.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.hdmi_cec.*,homeassistant.components.here_travel_time.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.honeywell.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.image.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.insteon.*,homeassistant.components.ipp.*,homeassistant.components.isy994.*,homeassistant.components.izone.*,homeassistant.components.kaiterra.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.lifx.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.lovelace.*,homeassistant.components.luftdaten.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lyric.*,homeassistant.components.marytts.*,homeassistant.components.media_source.*,homeassistant.components.melcloud.*,homeassistant.components.meteo_france.*,homeassistant.components.metoffice.*,homeassistant.components.minecraft_server.*,homeassistant.components.mobile_app.*,homeassistant.components.modbus.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mqtt.*,homeassistant.components.mullvad.*,homeassistant.components.mysensors.*,homeassistant.components.n26.*,homeassistant.components.neato.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netio.*,homeassistant.components.nightscout.*,homeassistant.components.nilu.*,homeassistant.components.nmap_tracker.*,homeassistant.components.norway_air.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nuki.*,homeassistant.components.nws.*,homeassistant.components.nzbget.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onvif.*,homeassistant.components.ovo_energy.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_viera.*,homeassistant.components.philips_js.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.plaato.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.point.*,homeassistant.components.profiler.*,homeassistant.components.proxmoxve.*,homeassistant.components.rachio.*,homeassistant.components.rainmachine.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.reddit.*,homeassistant.components.ring.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.roku.*,homeassistant.components.rpi_power.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.sabnzbd.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.search.*,homeassistant.components.sense.*,homeassistant.components.sentry.*,homeassistant.components.sesame.*,homeassistant.components.sharkiq.*,homeassistant.components.shelly.*,homeassistant.components.sma.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.solaredge.*,homeassistant.components.solarlog.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.spotify.*,homeassistant.components.stream.*,homeassistant.components.stt.*,homeassistant.components.surepetcare.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tasmota.*,homeassistant.components.tcp.*,homeassistant.components.telegram_bot.*,homeassistant.components.template.*,homeassistant.components.tesla.*,homeassistant.components.timer.*,homeassistant.components.todoist.*,homeassistant.components.toon.*,homeassistant.components.tplink.*,homeassistant.components.trace.*,homeassistant.components.tradfri.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.unifi.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.velbus.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.vizio.*,homeassistant.components.volumio.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.wink.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wunderground.*,homeassistant.components.xbox.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.yamaha.*,homeassistant.components.yeelight.*,homeassistant.components.zerproc.*,homeassistant.components.zha.*,homeassistant.components.zwave.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 345b2c71827..3f6f46ab894 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -192,7 +192,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sentry.*", "homeassistant.components.sesame.*", "homeassistant.components.sharkiq.*", - "homeassistant.components.shell_command.*", "homeassistant.components.shelly.*", "homeassistant.components.sma.*", "homeassistant.components.smart_meter_texas.*", From 0a6f981b4c23ef7f0d2d5ab2f76bd2b59b415cd4 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 3 May 2021 11:12:06 +0200 Subject: [PATCH 118/852] Fix KNX light unique_id (#49967) * migrate light unique_id * review changes --- homeassistant/components/knx/light.py | 85 +++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 63d7d40b7fc..693816635af 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -5,6 +5,7 @@ from collections.abc import Iterable from typing import Any, Callable from xknx.devices import Light as XknxLight +from xknx.telegram.address import parse_device_group_address from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -17,12 +18,13 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.color as color_util -from .const import DOMAIN +from .const import DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import LightSchema @@ -38,6 +40,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up lights for KNX platform.""" + _async_migrate_unique_id(hass, discovery_info) entities = [] for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxLight): @@ -45,6 +48,77 @@ async def async_setup_platform( async_add_entities(entities) +@callback +def _async_migrate_unique_id( + hass: HomeAssistant, discovery_info: DiscoveryInfoType | None +) -> None: + """Change unique_ids used in 2021.4 to exchange individual color switch address for brightness address.""" + entity_registry = er.async_get(hass) + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + for entity_config in platform_config: + individual_colors_config = entity_config.get(LightSchema.CONF_INDIVIDUAL_COLORS) + if individual_colors_config is None: + continue + try: + ga_red_switch = individual_colors_config[LightSchema.CONF_RED][KNX_ADDRESS][ + 0 + ] + ga_green_switch = individual_colors_config[LightSchema.CONF_GREEN][ + KNX_ADDRESS + ][0] + ga_blue_switch = individual_colors_config[LightSchema.CONF_BLUE][ + KNX_ADDRESS + ][0] + except KeyError: + continue + # normalize group address strings + ga_red_switch = parse_device_group_address(ga_red_switch) + ga_green_switch = parse_device_group_address(ga_green_switch) + ga_blue_switch = parse_device_group_address(ga_blue_switch) + # white config is optional so it has to be checked for `None` extra + white_config = individual_colors_config.get(LightSchema.CONF_WHITE) + white_switch = ( + white_config.get(KNX_ADDRESS) if white_config is not None else None + ) + ga_white_switch = ( + parse_device_group_address(white_switch[0]) + if white_switch is not None + else None + ) + + old_uid = ( + f"{ga_red_switch}_" + f"{ga_green_switch}_" + f"{ga_blue_switch}_" + f"{ga_white_switch}" + ) + entity_id = entity_registry.async_get_entity_id("light", DOMAIN, old_uid) + if entity_id is None: + continue + + ga_red_brightness = parse_device_group_address( + individual_colors_config[LightSchema.CONF_RED][ + LightSchema.CONF_BRIGHTNESS_ADDRESS + ][0] + ) + ga_green_brightness = parse_device_group_address( + individual_colors_config[LightSchema.CONF_GREEN][ + LightSchema.CONF_BRIGHTNESS_ADDRESS + ][0] + ) + ga_blue_brightness = parse_device_group_address( + individual_colors_config[LightSchema.CONF_BLUE][ + LightSchema.CONF_BRIGHTNESS_ADDRESS + ][0] + ) + + new_uid = f"{ga_red_brightness}_{ga_green_brightness}_{ga_blue_brightness}" + entity_registry.async_update_entity(entity_id, new_unique_id=new_uid) + + class KNXLight(KnxEntity, LightEntity): """Representation of a KNX light.""" @@ -67,10 +141,9 @@ class KNXLight(KnxEntity, LightEntity): if self._device.switch.group_address is not None: return f"{self._device.switch.group_address}" return ( - f"{self._device.red.switch.group_address}_" - f"{self._device.green.switch.group_address}_" - f"{self._device.blue.switch.group_address}_" - f"{self._device.white.switch.group_address}" + f"{self._device.red.brightness.group_address}_" + f"{self._device.green.brightness.group_address}_" + f"{self._device.blue.brightness.group_address}" ) @property From cfabb06a7abc9d33a8020857fffa9838a4d9bc41 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 3 May 2021 11:28:02 +0200 Subject: [PATCH 119/852] Add color modes to KNX light (#49883) * color_modes support * return brightness for color lights even when no brightness address is set * apply brightness for color lights * remove unneeded constants --- homeassistant/components/knx/light.py | 189 +++++++++++++------------- 1 file changed, 93 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 693816635af..d3086cacd0f 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Any, Callable +from typing import Any, Callable, cast from xknx.devices import Light as XknxLight from xknx.telegram.address import parse_device_group_address @@ -10,12 +10,13 @@ from xknx.telegram.address import parse_device_group_address from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, LightEntity, ) from homeassistant.core import HomeAssistant, callback @@ -28,10 +29,6 @@ from .const import DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import LightSchema -DEFAULT_COLOR = (0.0, 0.0) -DEFAULT_BRIGHTNESS = 255 -DEFAULT_WHITE_VALUE = 255 - async def async_setup_platform( hass: HomeAssistant, @@ -146,39 +143,39 @@ class KNXLight(KnxEntity, LightEntity): f"{self._device.blue.brightness.group_address}" ) + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return bool(self._device.state) + @property def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" if self._device.supports_brightness: return self._device.current_brightness - hsv_color = self._hsv_color - if self._device.supports_color and hsv_color: - return round(hsv_color[-1] / 100 * 255) + if (rgb := self.rgb_color) is not None: + return max(rgb) return None @property - def hs_color(self) -> tuple[float, float] | None: - """Return the HS color value.""" - rgb: tuple[int, int, int] | None = None - if self._device.supports_rgbw or self._device.supports_color: + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int].""" + if (rgbw := self.rgbw_color) is not None: + # used in brightness calculation when no address is given + return color_util.color_rgbw_to_rgb(*rgbw) + if self._device.supports_color: rgb, _ = self._device.current_color - return color_util.color_RGB_to_hs(*rgb) if rgb else None + return rgb + return None @property - def _hsv_color(self) -> tuple[float, float, float] | None: - """Return the HSV color value.""" - rgb: tuple[int, int, int] | None = None - if self._device.supports_rgbw or self._device.supports_color: - rgb, _ = self._device.current_color - return color_util.color_RGB_to_hsv(*rgb) if rgb else None - - @property - def white_value(self) -> int | None: - """Return the white value.""" - white: int | None = None + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the rgbw color value [int, int, int, int].""" if self._device.supports_rgbw: - _, white = self._device.current_color - return white + rgb, white = self._device.current_color + if rgb is not None and white is not None: + return (*rgb, white) + return None @property def color_temp(self) -> int | None: @@ -210,83 +207,80 @@ class KNXLight(KnxEntity, LightEntity): return self._max_mireds @property - def effect_list(self) -> list[str] | None: - """Return the list of supported effects.""" - return None - - @property - def effect(self) -> str | None: - """Return the current effect.""" - return None - - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return bool(self._device.state) - - @property - def supported_features(self) -> int: - """Flag supported features.""" - flags = 0 - if self._device.supports_brightness: - flags |= SUPPORT_BRIGHTNESS - if self._device.supports_color: - flags |= SUPPORT_COLOR | SUPPORT_BRIGHTNESS + def color_mode(self) -> str | None: + """Return the color mode of the light.""" if self._device.supports_rgbw: - flags |= SUPPORT_COLOR | SUPPORT_WHITE_VALUE + return COLOR_MODE_RGBW + if self._device.supports_color: + return COLOR_MODE_RGB if ( self._device.supports_color_temperature or self._device.supports_tunable_white ): - flags |= SUPPORT_COLOR_TEMP - return flags + return COLOR_MODE_COLOR_TEMP + if self._device.supports_brightness: + return COLOR_MODE_BRIGHTNESS + return COLOR_MODE_ONOFF + + @property + def supported_color_modes(self) -> set | None: + """Flag supported color modes.""" + return {self.color_mode} async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) - hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) - white_value = kwargs.get(ATTR_WHITE_VALUE, self.white_value) - mireds = kwargs.get(ATTR_COLOR_TEMP, self.color_temp) + # ignore arguments if not supported to fall back to set_on() + brightness = ( + kwargs.get(ATTR_BRIGHTNESS) + if self._device.supports_brightness + or self.color_mode in (COLOR_MODE_RGB, COLOR_MODE_RGBW) + else None + ) + mireds = ( + kwargs.get(ATTR_COLOR_TEMP) + if self.color_mode == COLOR_MODE_COLOR_TEMP + else None + ) + rgb = kwargs.get(ATTR_RGB_COLOR) if self.color_mode == COLOR_MODE_RGB else None + rgbw = ( + kwargs.get(ATTR_RGBW_COLOR) if self.color_mode == COLOR_MODE_RGBW else None + ) - update_brightness = ATTR_BRIGHTNESS in kwargs - update_color = ATTR_HS_COLOR in kwargs - update_white_value = ATTR_WHITE_VALUE in kwargs - update_color_temp = ATTR_COLOR_TEMP in kwargs - - # avoid conflicting changes and weird effects - if not ( - self.is_on - or update_brightness - or update_color - or update_white_value - or update_color_temp + if ( + not self.is_on + and brightness is None + and mireds is None + and rgb is None + and rgbw is None ): await self._device.set_on() + return - if self._device.supports_brightness and ( - update_brightness and not update_color - ): - # if we don't need to update the color, try updating brightness - # directly if supported; don't do it if color also has to be - # changed, as RGB color implicitly sets the brightness as well - await self._device.set_brightness(brightness) - elif (self._device.supports_rgbw or self._device.supports_color) and ( - update_brightness or update_color or update_white_value - ): - # change RGB color, white value (if supported), and brightness - # if brightness or hs_color was not yet set use the default value - # to calculate RGB from as a fallback - if brightness is None: - brightness = DEFAULT_BRIGHTNESS - if hs_color is None: - hs_color = DEFAULT_COLOR - if white_value is None and self._device.supports_rgbw: - white_value = DEFAULT_WHITE_VALUE - hsv_color = hs_color + (brightness * 100 / 255,) - rgb = color_util.color_hsv_to_RGB(*hsv_color) - await self._device.set_color(rgb, white_value) + async def set_color( + rgb: tuple[int, int, int], white: int | None, brightness: int | None + ) -> None: + """Set color of light. Normalize colors for brightness when not writable.""" + if brightness: + if self._device.brightness.writable: + await self._device.set_color(rgb, white) + await self._device.set_brightness(brightness) + return + rgb = cast( + tuple[int, int, int], + tuple(color * brightness // 255 for color in rgb), + ) + white = white * brightness // 255 if white is not None else None + await self._device.set_color(rgb, white) - if update_color_temp: + # return after RGB(W) color has changed as it implicitly sets the brightness + if rgbw is not None: + await set_color(rgbw[:3], rgbw[3], brightness) + return + if rgb is not None: + await set_color(rgb, None, brightness) + return + + if mireds is not None: kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) kelvin = min(self._max_kelvin, max(self._min_kelvin, kelvin)) @@ -300,6 +294,9 @@ class KNXLight(KnxEntity, LightEntity): ) await self._device.set_tunable_white(relative_ct) + if brightness is not None: + await self._device.set_brightness(brightness) + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.set_off() From f8d82bbf8034cc1b61e9c62d087ae39f40f78e86 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Mon, 3 May 2021 02:38:59 -0700 Subject: [PATCH 120/852] Add unique_id to TotalConnect alarm_control_panel (#49961) * add unique_id to alarm_control_panel * Update homeassistant/components/totalconnect/alarm_control_panel.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../components/totalconnect/alarm_control_panel.py | 6 ++++++ tests/components/totalconnect/common.py | 4 +++- tests/components/totalconnect/test_alarm_control_panel.py | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index c277198b683..ae999ade9ac 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -42,6 +42,7 @@ class TotalConnectAlarm(alarm.AlarmControlPanelEntity): """Initialize the TotalConnect status.""" self._name = name self._location_id = location_id + self._unique_id = str(location_id) self._client = client self._state = None self._extra_state_attributes = {} @@ -51,6 +52,11 @@ class TotalConnectAlarm(alarm.AlarmControlPanelEntity): """Return the name of the device.""" return self._name + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def state(self): """Return the state of the device.""" diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index d4285c07425..6f9fef4b7c0 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -9,8 +9,10 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +LOCATION_ID = "123456" + LOCATION_INFO_BASIC_NORMAL = { - "LocationID": "123456", + "LocationID": LOCATION_ID, "LocationName": "test", "SecurityDeviceID": "987654", "PhotoURL": "http://www.example.com/some/path/to/file.jpg", diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 12ad53733b5..9d8dbaf0358 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -17,6 +17,7 @@ from homeassistant.const import ( from homeassistant.exceptions import HomeAssistantError from .common import ( + LOCATION_ID, RESPONSE_ARM_FAILURE, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY, @@ -45,6 +46,11 @@ async def test_attributes(hass): mock_request.assert_called_once() assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get(ENTITY_ID) + # TotalConnect alarm device unique_id is the location_id + assert entry.unique_id == LOCATION_ID + async def test_arm_home_success(hass): """Test arm home method success.""" From 779f34a8edc44c231e54d62ea2982ef45db3a581 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 May 2021 23:41:20 -1000 Subject: [PATCH 121/852] Add dhcp discovery to hunterdouglas_powerview (#49993) * Add dhcp discovery to hunterdouglas_powerview * avoid dupe flow * cleanup * cleanup --- .../hunterdouglas_powerview/config_flow.py | 83 ++++++++------ .../hunterdouglas_powerview/manifest.json | 6 + homeassistant/generated/dhcp.py | 5 + .../test_config_flow.py | 105 +++++++++++++++--- 4 files changed, 152 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index ec81f25c0f2..869d703b641 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -1,11 +1,14 @@ """Config flow for Hunter Douglas PowerView integration.""" +from __future__ import annotations + import logging from aiopvapi.helpers.aiorequest import AioRequest import async_timeout import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, core, data_entry_flow, exceptions +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -18,13 +21,12 @@ DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) HAP_SUFFIX = "._hap._tcp.local." -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: core.HomeAssistant, hub_address: str) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ - hub_address = data[CONF_HOST] websession = async_get_clientsession(hass) pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession) @@ -34,8 +36,6 @@ async def validate_input(hass: core.HomeAssistant, data): device_info = await async_get_device_info(pv_request) except HUB_EXCEPTIONS as err: raise CannotConnect from err - if not device_info: - raise CannotConnect # Return info that you want to store in the config entry. return { @@ -52,56 +52,75 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the powerview config flow.""" self.powerview_config = {} + self.discovered_ip = None + self.discovered_name = None async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: - if self._host_already_configured(user_input[CONF_HOST]): - return self.async_abort(reason="already_configured") - try: - info = await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - if not errors: + info, error = await self._async_validate_or_error(user_input[CONF_HOST]) + if not error: await self.async_set_unique_id(info["unique_id"]) return self.async_create_entry( title=info["title"], data={CONF_HOST: user_input[CONF_HOST]} ) + errors["base"] = error return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, discovery_info): - """Handle HomeKit discovery.""" - - # If we already have the host configured do - # not open connections to it if we can avoid it. - if self._host_already_configured(discovery_info[CONF_HOST]): - return self.async_abort(reason="already_configured") + async def _async_validate_or_error(self, host): + if self._host_already_configured(host): + raise data_entry_flow.AbortFlow("already_configured") try: - info = await validate_input(self.hass, discovery_info) + info = await validate_input(self.hass, host) except CannotConnect: - return self.async_abort(reason="cannot_connect") + return None, "cannot_connect" except Exception: # pylint: disable=broad-except - return self.async_abort(reason="unknown") + _LOGGER.exception("Unexpected exception") + return None, "unknown" - await self.async_set_unique_id(info["unique_id"], raise_on_progress=False) - self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]}) + return info, None - name = discovery_info["name"] + async def async_step_dhcp(self, discovery_info): + """Handle DHCP discovery.""" + self.discovered_ip = discovery_info[IP_ADDRESS] + self.discovered_name = discovery_info[HOSTNAME] + return await self.async_step_discovery_confirm() + + async def async_step_homekit(self, discovery_info): + """Handle HomeKit discovery.""" + self.discovered_ip = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] if name.endswith(HAP_SUFFIX): name = name[: -len(HAP_SUFFIX)] + self.discovered_name = name + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm(self): + """Confirm dhcp or homekit discovery.""" + # If we already have the host configured do + # not open connections to it if we can avoid it. + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == self.discovered_ip: + return self.async_abort(reason="already_in_progress") + + if self._host_already_configured(self.discovered_ip): + return self.async_abort(reason="already_configured") + + info, error = await self._async_validate_or_error(self.discovered_ip) + if error: + return self.async_abort(reason=error) + + await self.async_set_unique_id(info["unique_id"], raise_on_progress=False) + self._abort_if_unique_id_configured({CONF_HOST: self.discovered_ip}) self.powerview_config = { - CONF_HOST: discovery_info["host"], - CONF_NAME: name, + CONF_HOST: self.discovered_ip, + CONF_NAME: self.discovered_name, } return await self.async_step_link() @@ -113,6 +132,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data={CONF_HOST: self.powerview_config[CONF_HOST]}, ) + self.context[CONF_HOST] = self.discovered_ip + self._set_confirm_only() return self.async_show_form( step_id="link", description_placeholders=self.powerview_config ) diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 183f4b45472..15a993d6d48 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -8,5 +8,11 @@ "homekit": { "models": ["PowerView"] }, + "dhcp": [ + { + "hostname": "hunter*", + "macaddress": "002674*" + } + ], "iot_class": "local_polling" } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index aa70d978e6c..d53cf133ac1 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -72,6 +72,11 @@ DHCP = [ "hostname": "flume-gw-*", "macaddress": "B4E62D*" }, + { + "domain": "hunterdouglas_powerview", + "hostname": "hunter*", + "macaddress": "002674*" + }, { "domain": "lyric", "hostname": "lyric-*", diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index f88e65ff854..60ee532c73f 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -3,11 +3,32 @@ import asyncio import json from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from homeassistant import config_entries, setup from homeassistant.components.hunterdouglas_powerview.const import DOMAIN from tests.common import MockConfigEntry, load_fixture +HOMEKIT_DISCOVERY_INFO = { + "name": "Hunter Douglas Powerview Hub._hap._tcp.local.", + "host": "1.2.3.4", + "properties": {"id": "AA::BB::CC::DD::EE::FF"}, +} + +DHCP_DISCOVERY_INFO = {"hostname": "Hunter Douglas Powerview Hub", "ip": "1.2.3.4"} + +DISCOVERY_DATA = [ + ( + config_entries.SOURCE_HOMEKIT, + HOMEKIT_DISCOVERY_INFO, + ), + ( + config_entries.SOURCE_DHCP, + DHCP_DISCOVERY_INFO, + ), +] + def _get_mock_powerview_userdata(userdata=None, get_resources=None): mock_powerview_userdata = MagicMock() @@ -65,8 +86,36 @@ async def test_user_form(hass): assert result4["type"] == "abort" -async def test_form_homekit(hass): - """Test we get the form with homekit source.""" +@pytest.mark.parametrize("source, discovery_info", DISCOVERY_DATA) +async def test_form_homekit_and_dhcp_cannot_connect(hass, source, discovery_info): + """Test we get the form with homekit and dhcp source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + ignored_config_entry = MockConfigEntry( + domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE + ) + ignored_config_entry.add_to_hass(hass) + + mock_powerview_userdata = _get_mock_powerview_userdata( + get_resources=asyncio.TimeoutError + ) + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data=discovery_info, + ) + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +@pytest.mark.parametrize("source, discovery_info", DISCOVERY_DATA) +async def test_form_homekit_and_dhcp(hass, source, discovery_info): + """Test we get the form with homekit and dhcp source.""" await setup.async_setup_component(hass, "persistent_notification", {}) ignored_config_entry = MockConfigEntry( @@ -81,12 +130,8 @@ async def test_form_homekit(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_HOMEKIT}, - data={ - "host": "1.2.3.4", - "properties": {"id": "AA::BB::CC::DD::EE::FF"}, - "name": "PowerViewHub._hap._tcp.local.", - }, + context={"source": source}, + data=discovery_info, ) assert result["type"] == "form" @@ -94,7 +139,7 @@ async def test_form_homekit(hass): assert result["errors"] is None assert result["description_placeholders"] == { "host": "1.2.3.4", - "name": "PowerViewHub", + "name": "Hunter Douglas Powerview Hub", } with patch( @@ -108,7 +153,7 @@ async def test_form_homekit(hass): await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["title"] == "PowerViewHub" + assert result2["title"] == "Hunter Douglas Powerview Hub" assert result2["data"] == {"host": "1.2.3.4"} assert result2["result"].unique_id == "ABC123" @@ -116,16 +161,44 @@ async def test_form_homekit(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_HOMEKIT}, - data={ - "host": "1.2.3.4", - "properties": {"id": "AA::BB::CC::DD::EE::FF"}, - "name": "PowerViewHub._hap._tcp.local.", - }, + context={"source": source}, + data=discovery_info, ) assert result3["type"] == "abort" +async def test_discovered_by_homekit_and_dhcp(hass): + """Test we get the form with homekit and abort for dhcp source when we get both.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_powerview_userdata = _get_mock_powerview_userdata() + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data=HOMEKIT_DISCOVERY_INFO, + ) + + assert result["type"] == "form" + assert result["step_id"] == "link" + + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=DHCP_DISCOVERY_INFO, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_in_progress" + + async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( From 1ad9f1d71426bd33990c585ad973bee584fb4413 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 3 May 2021 12:52:22 +0200 Subject: [PATCH 122/852] Title and internal descriptions updates for Fritzbox (#49047) --- homeassistant/components/fritzbox/__init__.py | 6 +++--- homeassistant/components/fritzbox/binary_sensor.py | 4 ++-- homeassistant/components/fritzbox/climate.py | 7 ++++--- homeassistant/components/fritzbox/config_flow.py | 4 ++-- homeassistant/components/fritzbox/const.py | 2 +- homeassistant/components/fritzbox/sensor.py | 8 ++++---- homeassistant/components/fritzbox/strings.json | 2 +- homeassistant/components/fritzbox/switch.py | 6 +++--- 8 files changed, 20 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 16b005359e1..dc200195748 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,4 +1,4 @@ -"""Support for AVM Fritz!Box smarthome devices.""" +"""Support for AVM FRITZ!SmartHome devices.""" from __future__ import annotations from datetime import timedelta @@ -28,7 +28,7 @@ from .const import CONF_CONNECTIONS, CONF_COORDINATOR, DOMAIN, LOGGER, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up the AVM Fritz!Box platforms.""" + """Set up the AVM FRITZ!SmartHome platforms.""" fritz = Fritzhome( host=entry.data[CONF_HOST], user=entry.data[CONF_USERNAME], @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unloading the AVM Fritz!Box platforms.""" + """Unloading the AVM FRITZ!SmartHome platforms.""" fritz = hass.data[DOMAIN][entry.entry_id][CONF_CONNECTIONS] await hass.async_add_executor_job(fritz.logout) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 993e10c11d7..807ca41ca64 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -20,7 +20,7 @@ from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the Fritzbox binary sensor from ConfigEntry.""" + """Set up the FRITZ!SmartHome binary sensor from ConfigEntry.""" entities = [] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] @@ -45,7 +45,7 @@ async def async_setup_entry( class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): - """Representation of a binary Fritzbox device.""" + """Representation of a binary FRITZ!SmartHome device.""" @property def is_on(self): diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 991f57a4269..947a95a7a5b 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -1,4 +1,4 @@ -"""Support for AVM Fritz!Box smarthome thermostate devices.""" +"""Support for AVM FRITZ!SmartHome thermostate devices.""" from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, @@ -54,7 +54,7 @@ OFF_REPORT_SET_TEMPERATURE = 0.0 async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the Fritzbox smarthome thermostat from ConfigEntry.""" + """Set up the FRITZ!SmartHome thermostat from ConfigEntry.""" entities = [] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] @@ -79,7 +79,7 @@ async def async_setup_entry( class FritzboxThermostat(FritzBoxEntity, ClimateEntity): - """The thermostat class for Fritzbox smarthome thermostates.""" + """The thermostat class for FRITZ!SmartHome thermostates.""" @property def supported_features(self): @@ -159,6 +159,7 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): return PRESET_COMFORT if self.device.target_temperature == self.device.eco_temperature: return PRESET_ECO + return None @property def preset_modes(self): diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index ed7cea3bc61..79763d18d2a 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for AVM Fritz!Box.""" +"""Config flow for AVM FRITZ!SmartHome.""" from urllib.parse import urlparse from pyfritzhome import Fritzhome, LoginError @@ -37,7 +37,7 @@ RESULT_SUCCESS = "success" class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a AVM Fritz!Box config flow.""" + """Handle a AVM FRITZ!SmartHome config flow.""" VERSION = 1 diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 9189fbd81c6..edfc13d49fe 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -1,4 +1,4 @@ -"""Constants for the AVM Fritz!Box integration.""" +"""Constants for the AVM FRITZ!SmartHome integration.""" import logging ATTR_STATE_BATTERY_LOW = "battery_low" diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index eb7d96c8d43..836b4fb407c 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,4 +1,4 @@ -"""Support for AVM Fritz!Box smarthome temperature sensor only devices.""" +"""Support for AVM FRITZ!SmartHome temperature sensor only devices.""" from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -25,7 +25,7 @@ from .const import ( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the Fritzbox smarthome sensor from ConfigEntry.""" + """Set up the FRITZ!SmartHome sensor from ConfigEntry.""" entities = [] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] @@ -66,7 +66,7 @@ async def async_setup_entry( class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity): - """The entity class for Fritzbox sensors.""" + """The entity class for FRITZ!SmartHome sensors.""" @property def state(self): @@ -75,7 +75,7 @@ class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity): class FritzBoxTempSensor(FritzBoxEntity, SensorEntity): - """The entity class for Fritzbox temperature sensors.""" + """The entity class for FRITZ!SmartHome temperature sensors.""" @property def state(self): diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index 6de6b6d9d9a..50b86611814 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "AVM FRITZ!SmartHome: {name}", "step": { "user": { "description": "Enter your AVM FRITZ!Box information.", diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 040cdfdcb28..39d8c4e8ec2 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -1,4 +1,4 @@ -"""Support for AVM Fritz!Box smarthome switch devices.""" +"""Support for AVM FRITZ!SmartHome switch devices.""" from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -30,7 +30,7 @@ ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the Fritzbox smarthome switch from ConfigEntry.""" + """Set up the FRITZ!SmartHome switch from ConfigEntry.""" entities = [] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] @@ -55,7 +55,7 @@ async def async_setup_entry( class FritzboxSwitch(FritzBoxEntity, SwitchEntity): - """The switch class for Fritzbox switches.""" + """The switch class for FRITZ!SmartHome switches.""" @property def available(self): From 378cee01b429b7597d44c95988e1675f1bda941e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 14:22:38 +0200 Subject: [PATCH 123/852] Add typing to async_register_entity_service (#50015) --- homeassistant/components/zwave_js/lock.py | 4 ++-- homeassistant/helpers/config_validation.py | 22 ++++++++++++---------- homeassistant/helpers/entity_platform.py | 11 +++++++++-- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 437ebf509a5..37923c49832 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -71,7 +71,7 @@ async def async_setup_entry( platform = entity_platform.current_platform.get() assert platform - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_SET_LOCK_USERCODE, { vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), @@ -80,7 +80,7 @@ async def async_setup_entry( "async_set_lock_usercode", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_CLEAR_LOCK_USERCODE, { vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 8ad8d4a45a2..ed619cc9678 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -862,17 +862,19 @@ ENTITY_SERVICE_FIELDS = { def make_entity_service_schema( schema: dict, *, extra: int = vol.PREVENT_EXTRA -) -> vol.All: +) -> vol.Schema: """Create an entity service schema.""" - return vol.All( - vol.Schema( - { - **schema, - **ENTITY_SERVICE_FIELDS, - }, - extra=extra, - ), - has_at_least_one_key(*ENTITY_SERVICE_FIELDS), + return vol.Schema( + vol.All( + vol.Schema( + { + **schema, + **ENTITY_SERVICE_FIELDS, + }, + extra=extra, + ), + has_at_least_one_key(*ENTITY_SERVICE_FIELDS), + ) ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6d7581f6f2b..83e72c7491d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -8,9 +8,10 @@ from datetime import datetime, timedelta import logging from logging import Logger from types import ModuleType -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Any, Callable from typing_extensions import Protocol +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( @@ -625,7 +626,13 @@ class EntityPlatform: ) @callback - def async_register_entity_service(self, name, schema, func, required_features=None): # type: ignore[no-untyped-def] + def async_register_entity_service( + self, + name: str, + schema: dict | vol.Schema, + func: str | Callable[..., Any], + required_features: Iterable[int] | None = None, + ) -> None: """Register an entity service. Services will automatically be shared by all platforms of the same domain. From 9b89acea97adaa0183f4d22ff52097e21c3f4348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 3 May 2021 14:26:25 +0200 Subject: [PATCH 124/852] Handle Timeout exceptions in system_health (#50017) --- homeassistant/components/system_health/__init__.py | 8 +++++--- tests/components/system_health/test_init.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index a7a92d3baf7..c8200e0e10a 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -216,6 +216,8 @@ async def async_check_can_reach_url( return "ok" except aiohttp.ClientError: data = {"type": "failed", "error": "unreachable"} - if more_info is not None: - data["more_info"] = more_info - return data + except asyncio.TimeoutError: + data = {"type": "failed", "error": "timeout"} + if more_info is not None: + data["more_info"] = more_info + return data diff --git a/tests/components/system_health/test_init.py b/tests/components/system_health/test_init.py index 212ec544629..672846ff4e2 100644 --- a/tests/components/system_health/test_init.py +++ b/tests/components/system_health/test_init.py @@ -113,6 +113,7 @@ async def test_platform_loading(hass, hass_ws_client, aioclient_mock): """Test registering via platform.""" aioclient_mock.get("http://example.com/status", text="") aioclient_mock.get("http://example.com/status_fail", exc=ClientError) + aioclient_mock.get("http://example.com/timeout", exc=asyncio.TimeoutError) hass.config.components.add("fake_integration") mock_platform( hass, @@ -130,6 +131,11 @@ async def test_platform_loading(hass, hass_ws_client, aioclient_mock): "http://example.com/status_fail", more_info="http://more-info-url.com", ), + "server_timeout": system_health.async_check_can_reach_url( + hass, + "http://example.com/timeout", + more_info="http://more-info-url.com", + ), "async_crash": AsyncMock(side_effect=ValueError)(), } ), @@ -150,6 +156,11 @@ async def test_platform_loading(hass, hass_ws_client, aioclient_mock): "error": "unreachable", "more_info": "http://more-info-url.com", }, + "server_timeout": { + "type": "failed", + "error": "timeout", + "more_info": "http://more-info-url.com", + }, "async_crash": { "type": "failed", "error": "unknown", From e5bfef719fc88972240ccdc2456aff85750543d9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 14:57:11 +0200 Subject: [PATCH 125/852] Fix Blink entity service schema (#50019) --- homeassistant/components/blink/camera.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index a25b978ee7b..f98e243d2ff 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -1,12 +1,8 @@ """Support for Blink system camera.""" import logging -import voluptuous as vol - from homeassistant.components.camera import Camera -from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers import entity_platform -import homeassistant.helpers.config_validation as cv from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER @@ -15,23 +11,18 @@ _LOGGER = logging.getLogger(__name__) ATTR_VIDEO_CLIP = "video" ATTR_IMAGE = "image" -SERVICE_TRIGGER_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) - async def async_setup_entry(hass, config, async_add_entities): """Set up a Blink Camera.""" data = hass.data[DOMAIN][config.entry_id] - entities = [] - for name, camera in data.cameras.items(): - entities.append(BlinkCamera(data, name, camera)) + entities = [ + BlinkCamera(data, name, camera) for name, camera in data.cameras.items() + ] async_add_entities(entities) platform = entity_platform.current_platform.get() - - platform.async_register_entity_service( - SERVICE_TRIGGER, SERVICE_TRIGGER_SCHEMA, "trigger_camera" - ) + platform.async_register_entity_service(SERVICE_TRIGGER, {}, "trigger_camera") class BlinkCamera(Camera): From 6e98b020acda9a0c8b0c892b0a0e280872586bf6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 3 May 2021 15:10:20 +0200 Subject: [PATCH 126/852] Improve type annotations in Brother integration (#49989) --- homeassistant/components/brother/const.py | 99 ++++++++++------------ homeassistant/components/brother/model.py | 13 +++ homeassistant/components/brother/sensor.py | 5 +- 3 files changed, 62 insertions(+), 55 deletions(-) create mode 100644 homeassistant/components/brother/model.py diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index a812e81f0ee..2a2e8724821 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -1,57 +1,61 @@ """Constants for Brother integration.""" from __future__ import annotations -from typing import TypedDict +from typing import Final from homeassistant.const import PERCENTAGE -ATTR_BELT_UNIT_REMAINING_LIFE = "belt_unit_remaining_life" -ATTR_BLACK_DRUM_COUNTER = "black_drum_counter" -ATTR_BLACK_DRUM_REMAINING_LIFE = "black_drum_remaining_life" -ATTR_BLACK_DRUM_REMAINING_PAGES = "black_drum_remaining_pages" -ATTR_BLACK_INK_REMAINING = "black_ink_remaining" -ATTR_BLACK_TONER_REMAINING = "black_toner_remaining" -ATTR_BW_COUNTER = "b/w_counter" -ATTR_COLOR_COUNTER = "color_counter" -ATTR_CYAN_DRUM_COUNTER = "cyan_drum_counter" -ATTR_CYAN_DRUM_REMAINING_LIFE = "cyan_drum_remaining_life" -ATTR_CYAN_DRUM_REMAINING_PAGES = "cyan_drum_remaining_pages" -ATTR_CYAN_INK_REMAINING = "cyan_ink_remaining" -ATTR_CYAN_TONER_REMAINING = "cyan_toner_remaining" -ATTR_DRUM_COUNTER = "drum_counter" -ATTR_DRUM_REMAINING_LIFE = "drum_remaining_life" -ATTR_DRUM_REMAINING_PAGES = "drum_remaining_pages" -ATTR_DUPLEX_COUNTER = "duplex_unit_pages_counter" -ATTR_FUSER_REMAINING_LIFE = "fuser_remaining_life" -ATTR_LASER_REMAINING_LIFE = "laser_remaining_life" -ATTR_MAGENTA_DRUM_COUNTER = "magenta_drum_counter" -ATTR_MAGENTA_DRUM_REMAINING_LIFE = "magenta_drum_remaining_life" -ATTR_MAGENTA_DRUM_REMAINING_PAGES = "magenta_drum_remaining_pages" -ATTR_MAGENTA_INK_REMAINING = "magenta_ink_remaining" -ATTR_MAGENTA_TONER_REMAINING = "magenta_toner_remaining" -ATTR_MANUFACTURER = "Brother" -ATTR_PAGE_COUNTER = "page_counter" -ATTR_PF_KIT_1_REMAINING_LIFE = "pf_kit_1_remaining_life" -ATTR_PF_KIT_MP_REMAINING_LIFE = "pf_kit_mp_remaining_life" -ATTR_STATUS = "status" -ATTR_UPTIME = "uptime" -ATTR_YELLOW_DRUM_COUNTER = "yellow_drum_counter" -ATTR_YELLOW_DRUM_REMAINING_LIFE = "yellow_drum_remaining_life" -ATTR_YELLOW_DRUM_REMAINING_PAGES = "yellow_drum_remaining_pages" -ATTR_YELLOW_INK_REMAINING = "yellow_ink_remaining" -ATTR_YELLOW_TONER_REMAINING = "yellow_toner_remaining" +from .model import SensorDescription -DATA_CONFIG_ENTRY = "config_entry" +ATTR_BELT_UNIT_REMAINING_LIFE: Final = "belt_unit_remaining_life" +ATTR_BLACK_DRUM_COUNTER: Final = "black_drum_counter" +ATTR_BLACK_DRUM_REMAINING_LIFE: Final = "black_drum_remaining_life" +ATTR_BLACK_DRUM_REMAINING_PAGES: Final = "black_drum_remaining_pages" +ATTR_BLACK_INK_REMAINING: Final = "black_ink_remaining" +ATTR_BLACK_TONER_REMAINING: Final = "black_toner_remaining" +ATTR_BW_COUNTER: Final = "b/w_counter" +ATTR_COLOR_COUNTER: Final = "color_counter" +ATTR_COUNTER: Final = "counter" +ATTR_CYAN_DRUM_COUNTER: Final = "cyan_drum_counter" +ATTR_CYAN_DRUM_REMAINING_LIFE: Final = "cyan_drum_remaining_life" +ATTR_CYAN_DRUM_REMAINING_PAGES: Final = "cyan_drum_remaining_pages" +ATTR_CYAN_INK_REMAINING: Final = "cyan_ink_remaining" +ATTR_CYAN_TONER_REMAINING: Final = "cyan_toner_remaining" +ATTR_DRUM_COUNTER: Final = "drum_counter" +ATTR_DRUM_REMAINING_LIFE: Final = "drum_remaining_life" +ATTR_DRUM_REMAINING_PAGES: Final = "drum_remaining_pages" +ATTR_DUPLEX_COUNTER: Final = "duplex_unit_pages_counter" +ATTR_FUSER_REMAINING_LIFE: Final = "fuser_remaining_life" +ATTR_LASER_REMAINING_LIFE: Final = "laser_remaining_life" +ATTR_MAGENTA_DRUM_COUNTER: Final = "magenta_drum_counter" +ATTR_MAGENTA_DRUM_REMAINING_LIFE: Final = "magenta_drum_remaining_life" +ATTR_MAGENTA_DRUM_REMAINING_PAGES: Final = "magenta_drum_remaining_pages" +ATTR_MAGENTA_INK_REMAINING: Final = "magenta_ink_remaining" +ATTR_MAGENTA_TONER_REMAINING: Final = "magenta_toner_remaining" +ATTR_MANUFACTURER: Final = "Brother" +ATTR_PAGE_COUNTER: Final = "page_counter" +ATTR_PF_KIT_1_REMAINING_LIFE: Final = "pf_kit_1_remaining_life" +ATTR_PF_KIT_MP_REMAINING_LIFE: Final = "pf_kit_mp_remaining_life" +ATTR_REMAINING_PAGES: Final = "remaining_pages" +ATTR_STATUS: Final = "status" +ATTR_UPTIME: Final = "uptime" +ATTR_YELLOW_DRUM_COUNTER: Final = "yellow_drum_counter" +ATTR_YELLOW_DRUM_REMAINING_LIFE: Final = "yellow_drum_remaining_life" +ATTR_YELLOW_DRUM_REMAINING_PAGES: Final = "yellow_drum_remaining_pages" +ATTR_YELLOW_INK_REMAINING: Final = "yellow_ink_remaining" +ATTR_YELLOW_TONER_REMAINING: Final = "yellow_toner_remaining" -DOMAIN = "brother" +DATA_CONFIG_ENTRY: Final = "config_entry" -UNIT_PAGES = "p" +DOMAIN: Final = "brother" -PRINTER_TYPES = ["laser", "ink"] +UNIT_PAGES: Final = "p" -SNMP = "snmp" +PRINTER_TYPES: Final = ["laser", "ink"] -ATTRS_MAP: dict[str, tuple[str, str]] = { +SNMP: Final = "snmp" + +ATTRS_MAP: Final[dict[str, tuple[str, str]]] = { ATTR_DRUM_REMAINING_LIFE: (ATTR_DRUM_REMAINING_PAGES, ATTR_DRUM_COUNTER), ATTR_BLACK_DRUM_REMAINING_LIFE: ( ATTR_BLACK_DRUM_REMAINING_PAGES, @@ -71,7 +75,7 @@ ATTRS_MAP: dict[str, tuple[str, str]] = { ), } -SENSOR_TYPES: dict[str, SensorDescription] = { +SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_STATUS: { "icon": "mdi:printer", "label": ATTR_STATUS.title(), @@ -217,12 +221,3 @@ SENSOR_TYPES: dict[str, SensorDescription] = { "enabled": False, }, } - - -class SensorDescription(TypedDict): - """Sensor description class.""" - - icon: str | None - label: str - unit: str | None - enabled: bool diff --git a/homeassistant/components/brother/model.py b/homeassistant/components/brother/model.py new file mode 100644 index 00000000000..22aa95eda50 --- /dev/null +++ b/homeassistant/components/brother/model.py @@ -0,0 +1,13 @@ +"""Type definitions for Brother integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class SensorDescription(TypedDict): + """Sensor description class.""" + + icon: str | None + label: str + unit: str | None + enabled: bool diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 8cb153ac199..2f66e1c75d5 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -13,7 +13,9 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import BrotherDataUpdateCoordinator from .const import ( + ATTR_COUNTER, ATTR_MANUFACTURER, + ATTR_REMAINING_PAGES, ATTR_UPTIME, ATTRS_MAP, DATA_CONFIG_ENTRY, @@ -21,9 +23,6 @@ from .const import ( SENSOR_TYPES, ) -ATTR_COUNTER = "counter" -ATTR_REMAINING_PAGES = "remaining_pages" - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback From efb1bb08a47b685ed19f626d39a723b737a5372f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 16:46:42 +0200 Subject: [PATCH 127/852] Add small async_get_current_platform helper method (#50014) --- homeassistant/components/wled/light.py | 3 +-- homeassistant/helpers/entity_platform.py | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 98e60b9bb81..0151737ece9 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -57,8 +57,7 @@ async def async_setup_entry( """Set up WLED light based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - platform = entity_platform.current_platform.get() - + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_EFFECT, { diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 83e72c7491d..6350467c05b 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -696,6 +696,15 @@ current_platform: ContextVar[EntityPlatform | None] = ContextVar( ) +@callback +def async_get_current_platform() -> EntityPlatform: + """Get the current platform from context.""" + platform = current_platform.get() + if platform is None: + raise RuntimeError("Cannot get non-set current platform") + return platform + + @callback def async_get_platforms( hass: HomeAssistant, integration_name: str From 13dee0f028789d0fe262155d800153dd3b05bc38 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Mon, 3 May 2021 17:06:46 +0200 Subject: [PATCH 128/852] Mitigate NMBS key errors (#50026) on Liveboard and connections as documented in issue #48824 --- homeassistant/components/nmbs/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 32e4fd87e29..58ad547eaec 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -152,7 +152,7 @@ class NMBSLiveBoard(SensorEntity): """Set the state equal to the next departure.""" liveboard = self._api_client.get_liveboard(self._station) - if liveboard is None or not liveboard["departures"]: + if liveboard is None or not liveboard.get("departures"): return next_departure = liveboard["departures"]["departure"][0] @@ -269,7 +269,7 @@ class NMBSSensor(SensorEntity): self._station_from, self._station_to ) - if connections is None or not connections["connection"]: + if connections is None or not connections.get("connection"): return if int(connections["connection"][0]["departure"]["left"]) > 0: From 0627b316e3803898b929dbabc3f3128b5af7fb95 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 17:32:10 +0200 Subject: [PATCH 129/852] Add Identify service to Elgato integration (#49990) --- homeassistant/components/elgato/const.py | 3 ++ homeassistant/components/elgato/light.py | 18 ++++++- homeassistant/components/elgato/services.yaml | 9 ++++ tests/components/elgato/test_light.py | 48 +++++++++++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/elgato/services.yaml diff --git a/homeassistant/components/elgato/const.py b/homeassistant/components/elgato/const.py index b2535ce0e4f..8b931fc6328 100644 --- a/homeassistant/components/elgato/const.py +++ b/homeassistant/components/elgato/const.py @@ -14,3 +14,6 @@ ATTR_ON = "on" ATTR_SOFTWARE_VERSION = "sw_version" CONF_SERIAL_NUMBER = "serial_number" + +# Services +SERVICE_IDENTIFY = "identify" diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 89e077f8f49..7f4987b2620 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -17,8 +17,9 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import async_get_current_platform -from .const import DATA_ELGATO_CLIENT, DOMAIN +from .const import DATA_ELGATO_CLIENT, DOMAIN, SERVICE_IDENTIFY _LOGGER = logging.getLogger(__name__) @@ -36,6 +37,13 @@ async def async_setup_entry( info = await elgato.info() async_add_entities([ElgatoLight(elgato, info)], True) + platform = async_get_current_platform() + platform.async_register_entity_service( + SERVICE_IDENTIFY, + {}, + ElgatoLight.async_identify.__name__, + ) + class ElgatoLight(LightEntity): """Defines a Elgato Key Light.""" @@ -144,3 +152,11 @@ class ElgatoLight(LightEntity): "model": self._info.product_name, "sw_version": f"{self._info.firmware_version} ({self._info.firmware_build_number})", } + + async def async_identify(self) -> None: + """Identify the light, will make it blink.""" + try: + await self.elgato.identify() + except ElgatoError: + _LOGGER.exception("An error occurred while identifying the Elgato Light") + self._state = None diff --git a/homeassistant/components/elgato/services.yaml b/homeassistant/components/elgato/services.yaml new file mode 100644 index 00000000000..05d341a7041 --- /dev/null +++ b/homeassistant/components/elgato/services.yaml @@ -0,0 +1,9 @@ +identify: + name: Identify + description: >- + Identify an Elgato Light. Blinks the light, which can be useful + for, e.g., a visual notification. + target: + entity: + integration: elgato + domain: light diff --git a/tests/components/elgato/test_light.py b/tests/components/elgato/test_light.py index 6c4de76719f..38da5856f75 100644 --- a/tests/components/elgato/test_light.py +++ b/tests/components/elgato/test_light.py @@ -3,6 +3,7 @@ from unittest.mock import patch from elgato import ElgatoError +from homeassistant.components.elgato.const import DOMAIN, SERVICE_IDENTIFY from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -106,3 +107,50 @@ async def test_light_unavailable( await hass.async_block_till_done() state = hass.states.get("light.frenck") assert state.state == STATE_UNAVAILABLE + + +async def test_light_identify( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test identifying an Elgato Light.""" + await init_integration(hass, aioclient_mock) + + with patch( + "homeassistant.components.elgato.light.Elgato.identify", + return_value=mock_coro(), + ) as mock_identify: + await hass.services.async_call( + DOMAIN, + SERVICE_IDENTIFY, + { + ATTR_ENTITY_ID: "light.frenck", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_identify.mock_calls) == 1 + mock_identify.assert_called_with() + + +async def test_light_identify_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test error occurred during identifying an Elgato Light.""" + await init_integration(hass, aioclient_mock) + + with patch( + "homeassistant.components.elgato.light.Elgato.identify", + side_effect=ElgatoError, + ) as mock_identify: + await hass.services.async_call( + DOMAIN, + SERVICE_IDENTIFY, + { + ATTR_ENTITY_ID: "light.frenck", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_identify.mock_calls) == 1 + + assert "An error occurred while identifying the Elgato Light" in caplog.text From 672d2e332f26e21fa6e66dbe04267d5b90ac5251 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 May 2021 18:32:45 +0200 Subject: [PATCH 130/852] Update frontend to 20210503.0 (#50036) --- 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 549a64886c1..bfd602471a7 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==20210430.0" + "home-assistant-frontend==20210503.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index be78796e8de..0d9cbe7990b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210430.0 +home-assistant-frontend==20210503.0 httpx==0.18.0 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index eda053c2417..649da5c6366 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210430.0 +home-assistant-frontend==20210503.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca9c8ad9e35..6ba3edb332d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210430.0 +home-assistant-frontend==20210503.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 2ed386f9e64f059f356bdfe8900f48f785fc1b97 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 18:34:28 +0200 Subject: [PATCH 131/852] Migrate to async_get_current_platform everywhere (#50034) --- homeassistant/components/advantage_air/climate.py | 2 +- homeassistant/components/advantage_air/sensor.py | 2 +- homeassistant/components/agent_dvr/camera.py | 2 +- homeassistant/components/alarmdecoder/alarm_control_panel.py | 3 +-- homeassistant/components/androidtv/media_player.py | 2 +- homeassistant/components/blink/camera.py | 2 +- homeassistant/components/channels/media_player.py | 2 +- homeassistant/components/deconz/alarm_control_panel.py | 2 +- homeassistant/components/denonavr/media_player.py | 2 +- homeassistant/components/dyson/fan.py | 2 +- homeassistant/components/ecobee/climate.py | 2 +- homeassistant/components/elkm1/alarm_control_panel.py | 2 +- homeassistant/components/elkm1/sensor.py | 2 +- homeassistant/components/epson/media_player.py | 2 +- homeassistant/components/flo/switch.py | 2 +- homeassistant/components/foscam/camera.py | 2 +- homeassistant/components/geniushub/switch.py | 2 +- homeassistant/components/guardian/switch.py | 2 +- homeassistant/components/harmony/remote.py | 2 +- homeassistant/components/hive/climate.py | 2 +- homeassistant/components/hive/water_heater.py | 2 +- homeassistant/components/homeassistant/scene.py | 2 +- homeassistant/components/isy994/services.py | 2 +- homeassistant/components/izone/climate.py | 2 +- homeassistant/components/kef/media_player.py | 2 +- homeassistant/components/kodi/media_player.py | 2 +- homeassistant/components/lifx/light.py | 2 +- homeassistant/components/litterrobot/vacuum.py | 2 +- homeassistant/components/lyric/climate.py | 2 +- homeassistant/components/melcloud/climate.py | 2 +- homeassistant/components/monoprice/media_player.py | 2 +- homeassistant/components/motion_blinds/cover.py | 2 +- homeassistant/components/neato/vacuum.py | 2 +- homeassistant/components/netatmo/camera.py | 2 +- homeassistant/components/netatmo/climate.py | 2 +- homeassistant/components/nexia/climate.py | 2 +- homeassistant/components/nuki/lock.py | 4 +--- homeassistant/components/nx584/alarm_control_panel.py | 2 +- homeassistant/components/omnilogic/switch.py | 2 +- homeassistant/components/onvif/camera.py | 2 +- homeassistant/components/openhome/media_player.py | 2 +- homeassistant/components/ozw/lock.py | 2 +- homeassistant/components/pi_hole/switch.py | 2 +- homeassistant/components/rachio/switch.py | 2 +- homeassistant/components/rainmachine/switch.py | 2 +- homeassistant/components/risco/binary_sensor.py | 2 +- homeassistant/components/roku/media_player.py | 2 +- homeassistant/components/roon/media_player.py | 2 +- homeassistant/components/snapcast/media_player.py | 2 +- homeassistant/components/songpal/media_player.py | 2 +- homeassistant/components/sonos/media_player.py | 2 +- homeassistant/components/squeezebox/media_player.py | 2 +- homeassistant/components/switcher_kis/switch.py | 2 +- homeassistant/components/tado/climate.py | 2 +- homeassistant/components/tado/water_heater.py | 2 +- homeassistant/components/upb/light.py | 2 +- homeassistant/components/upb/scene.py | 2 +- homeassistant/components/utility_meter/sensor.py | 2 +- homeassistant/components/verisure/camera.py | 4 ++-- homeassistant/components/verisure/lock.py | 4 ++-- homeassistant/components/vicare/climate.py | 2 +- homeassistant/components/vizio/media_player.py | 2 +- homeassistant/components/wemo/fan.py | 2 +- homeassistant/components/xiaomi_miio/remote.py | 2 +- homeassistant/components/xiaomi_miio/vacuum.py | 2 +- homeassistant/components/yamaha/media_player.py | 2 +- homeassistant/components/yeelight/light.py | 2 +- homeassistant/components/zha/lock.py | 3 +-- homeassistant/components/zwave_js/lock.py | 3 +-- 69 files changed, 71 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index ca25edbda4f..60caf15be25 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(AdvantageAirZone(instance, ac_key, zone_key)) async_add_entities(entities) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( ADVANTAGE_AIR_SERVICE_SET_MYZONE, {}, diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 19ac584ac2f..8f027b1bdaf 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key)) async_add_entities(entities) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( ADVANTAGE_AIR_SERVICE_SET_TIME_TO, {vol.Required("minutes"): cv.positive_int}, diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 24cd5dbb92c..6b2363f50d5 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -59,7 +59,7 @@ async def async_setup_entry( async_add_entities(cameras) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() for service, method in CAMERA_SERVICES.items(): platform.async_register_entity_service(service, {}, method) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index d081c9e56a3..47da48de66f 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -56,8 +56,7 @@ async def async_setup_entry( ) async_add_entities([entity]) - platform = entity_platform.current_platform.get() - + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_ALARM_TOGGLE_CHIME, { diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index b2a8ceffc9f..5db73d14914 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -261,7 +261,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND): return - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() async def service_adb_command(service): """Dispatch service calls to target entities.""" diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index f98e243d2ff..5085686494e 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass, config, async_add_entities): async_add_entities(entities) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service(SERVICE_TRIGGER, {}, "trigger_camera") diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 5376dc3fe97..2b62039989a 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -59,7 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device = ChannelsPlayer(config[CONF_NAME], config[CONF_HOST], config[CONF_PORT]) async_add_entities([device], True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SEEK_FORWARD, diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 59749c0680d..6bb4b72e89d 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() @callback def async_add_alarm_control_panel(sensors=gateway.api.sensors.values()) -> None: diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 14520f0ddaf..ae001bfc312 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -113,7 +113,7 @@ async def async_setup_entry( ) # Register additional services - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_GET_COMMAND, {vol.Required(ATTR_COMMAND): cv.string}, diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index e646babb944..5b24b4a9df7 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -122,7 +122,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(hass.data[DYSON_FAN_DEVICES]) # Register custom services - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_NIGHT_MODE, SET_NIGHT_MODE_SCHEMA, "set_night_mode" ) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 6de23f09c60..9e9e2eff1c8 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -181,7 +181,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(devices, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() def create_vacation_service(service): """Create a vacation on the target thermostat.""" diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 756166c86a6..b925026bfaa 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): create_elk_entities(elk_data, elk.areas, "area", ElkArea, entities) async_add_entities(entities, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_ALARM_ARM_VACATION, diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index e33196a08c0..4a75ccb242e 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities) async_add_entities(entities, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SENSOR_COUNTER_REFRESH, diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 6115cdd6ef4..0b6828b7747 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -72,7 +72,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): projector, config_entry.title, unique_id ) async_add_entities([projector_entity], True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SELECT_CMODE, {vol.Required(ATTR_CMODE): vol.All(cv.string, vol.Any(*CMODE_LIST_SET))}, diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index e5f00a6125f..ce9b48d1421 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(FloSwitch(device)) async_add_entities(entities) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_AWAY_MODE, {}, "async_set_mode_away" diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index ea20f0a07fb..31ac8c2cad9 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -90,7 +90,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Add a Foscam IP camera from a config entry.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_PTZ, { diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index faff6b8e2f9..6760ce57e01 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -44,7 +44,7 @@ async def async_setup_platform( ) # Register custom services - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SVC_SET_SWITCH_OVERRIDE, diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index ea6888bafbd..c8ec4c6c645 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -42,7 +42,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() for service_name, schema, method in [ (SERVICE_DISABLE_AP, {}, "async_disable_ap"), diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 54d6b0fa7d1..f98f5bb33e7 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -70,7 +70,7 @@ async def async_setup_entry( device = HarmonyRemote(data, default_activity, delay_secs, harmony_conf_file) async_add_entities([device]) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SYNC, diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index d5b60fa4b95..7639a07c82a 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass, entry, async_add_entities): entities.append(HiveClimateEntity(hive, dev)) async_add_entities(entities, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( "boost_heating", diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 0df10a9ed22..cca919b81d6 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass, entry, async_add_entities): entities.append(HiveWaterHeater(hive, dev)) async_add_entities(entities, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_BOOST_HOT_WATER, diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 3173d2d8c32..13a4ef66383 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -159,7 +159,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return # Store platform for later. - platform = hass.data[DATA_PLATFORM] = entity_platform.current_platform.get() + platform = hass.data[DATA_PLATFORM] = entity_platform.async_get_current_platform() async def reload_config(call): """Reload the scene config.""" diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 023f1022661..6d93b53b912 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -405,7 +405,7 @@ def async_unload_services(hass: HomeAssistant): @callback def async_setup_light_services(hass: HomeAssistant): """Create device-specific services for the ISY Integration.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_ON_LEVEL, SERVICE_SET_VALUE_SCHEMA, SERVICE_SET_ON_LEVEL diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 6d4630d4c46..253bdc6cb2b 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -97,7 +97,7 @@ async def async_setup_entry( # connect to register any further components async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( IZONE_SERVICE_AIRFLOW_MIN, IZONE_SERVICE_AIRFLOW_SCHEMA, diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 5316568ab52..9452e24a4f2 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -146,7 +146,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.data[DOMAIN][host] = media_player async_add_entities([media_player], update_before_add=True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_MODE, diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 72197f6b8e2..42943cffb13 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -226,7 +226,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Kodi media player platform.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_ADD_MEDIA, KODI_ADD_MEDIA_SCHEMA, "async_add_media_to_playlist" ) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index e366b810a94..167a1ceee0a 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -175,7 +175,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Priority 3: default interface interfaces = [{}] - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() lifx_manager = LIFXManager(hass, platform, async_add_entities) hass.data[DATA_LIFX_MANAGER] = lifx_manager diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 32fc92cd55a..fc398e4ad12 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -55,7 +55,7 @@ async def async_setup_entry( async_add_entities(entities, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_RESET_WASTE_DRAWER, {}, diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index d61d638b991..a13f0381499 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -105,7 +105,7 @@ async def async_setup_entry( async_add_entities(entities, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_HOLD_TIME, diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index d8bc89a45f0..49cb0fe462e 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -84,7 +84,7 @@ async def async_setup_entry( True, ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_VANE_HORIZONTAL, {vol.Required(CONF_POSITION): cv.string}, diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 4d6d337667e..8b3de8903a3 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): first_run = hass.data[DOMAIN][config_entry.entry_id][FIRST_RUN] async_add_entities(entities, first_run) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() def _call_service(entities, service_call): for entity in entities: diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 2c4fee5f8aa..a802ecfb667 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -116,7 +116,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_ABSOLUTE_POSITION, SET_ABSOLUTE_POSITION_SCHEMA, diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 2415b86fc62..b6cf43a6a3e 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -87,7 +87,7 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.debug("Adding vacuums %s", dev) async_add_entities(dev, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() assert platform is not None platform.async_register_entity_service( diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 5445231282c..11e674e0431 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -99,7 +99,7 @@ async def async_setup_entry(hass, entry, async_add_entities): await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_PERSONS_HOME, diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 9993b4efac2..e53b060d7cf 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass, entry, async_add_entities): await data_handler.unregister_data_class(HOMEDATA_DATA_CLASS_NAME, None) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() if home_data is not None: platform.async_register_entity_service( diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index aff3711cdae..c510b905dd5 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): nexia_home = nexia_data[NEXIA_DEVICE] coordinator = nexia_data[UPDATE_COORDINATOR] - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_HUMIDIFY_SETPOINT, diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index bd5d58ed42a..ca6e72bde8f 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -50,9 +50,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) async_add_entities(entities) - platform = entity_platform.current_platform.get() - assert platform is not None - + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( "lock_n_go", { diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 6366831b598..d5cdce5d64b 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -65,7 +65,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity = NX584Alarm(name, alarm_client, url) async_add_entities([entity]) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_BYPASS_ZONE, diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py index fce4fb019da..ef4d2b32cc5 100644 --- a/homeassistant/components/omnilogic/switch.py +++ b/homeassistant/components/omnilogic/switch.py @@ -47,7 +47,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) # register service - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_SPEED, diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 91f4c76abac..0e95d24ef78 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -44,7 +44,7 @@ from .const import ( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the ONVIF camera video stream.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() # Create PTZ service platform.async_register_entity_service( diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 270eb22ebda..7e333f7432b 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -53,7 +53,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([entity]) openhome_data.add(device.Uuid()) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_INVOKE_PIN, diff --git a/homeassistant/components/ozw/lock.py b/homeassistant/components/ozw/lock.py index 68acb3f9691..9cadb2862f1 100644 --- a/homeassistant/components/ozw/lock.py +++ b/homeassistant/components/ozw/lock.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect(hass, f"{DOMAIN}_new_{LOCK_DOMAIN}", async_add_lock) ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_USERCODE, diff --git a/homeassistant/components/pi_hole/switch.py b/homeassistant/components/pi_hole/switch.py index 015bab8fe60..955585243cf 100644 --- a/homeassistant/components/pi_hole/switch.py +++ b/homeassistant/components/pi_hole/switch.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(switches, True) # register service - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_DISABLE, { diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 41b253d97ee..65249d0b8ea 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -140,7 +140,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) if has_flex_sched: - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_ZONE_MOISTURE, {vol.Required(ATTR_PERCENT): cv.positive_int}, diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 9ad6ce113b1..a90091c6c3a 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -113,7 +113,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up RainMachine switches based on a config entry.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() alter_program_schema = {vol.Required(CONF_PROGRAM_ID): cv.positive_int} alter_zone_schema = {vol.Required(CONF_ZONE_ID): cv.positive_int} diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index ba32429c154..0e1d4d235c2 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -14,7 +14,7 @@ SERVICE_UNBYPASS_ZONE = "unbypass_zone" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Risco alarm control panel.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service(SERVICE_BYPASS_ZONE, {}, "async_bypass_zone") platform.async_register_entity_service( SERVICE_UNBYPASS_ZONE, {}, "async_unbypass_zone" diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 6fee53595ac..ce5a77f06f6 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass, entry, async_add_entities): unique_id = coordinator.data.info.serial_number async_add_entities([RokuMediaPlayer(unique_id, coordinator)], True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SEARCH, diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index ff55c0fb1fb..dd8d9e83c2d 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -72,7 +72,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): media_players = set() # Register entity services - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_TRANSFER, {vol.Required(ATTR_TRANSFER): cv.entity_id}, diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index e1c5b7d875b..dcb4b62b35a 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -58,7 +58,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= host = config.get(CONF_HOST) port = config.get(CONF_PORT, CONTROL_PORT) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service(SERVICE_SNAPSHOT, {}, "snapshot") platform.async_register_entity_service(SERVICE_RESTORE, {}, "async_restore") platform.async_register_entity_service( diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 5cc1f9b542a..1746d5ece0d 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -82,7 +82,7 @@ async def async_setup_entry( songpal_entity = SongpalEntity(name, device) async_add_entities([songpal_entity], True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SET_SOUND_SETTING, {vol.Required(PARAM_NAME): cv.string, vol.Required(PARAM_VALUE): cv.string}, diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2145063ff5c..c9e6612fc06 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -151,7 +151,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Sonos from a config entry.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() @callback def async_create_entities(speaker: SonosSpeaker) -> None: diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index c57f95266ff..baf8a011c65 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -210,7 +210,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): asyncio.create_task(_discovery()) # Register entity services - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_CALL_METHOD, { diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 7a646d3de4a..de47f11748f 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -91,7 +91,7 @@ async def async_setup_platform( device_data = hass.data[DOMAIN][DATA_DEVICE] async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])]) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_AUTO_OFF_NAME, diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index b86eb08b1b0..aa9852f643f 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -81,7 +81,7 @@ async def async_setup_entry( tado = hass.data[DOMAIN][entry.entry_id][DATA] entities = await hass.async_add_executor_job(_generate_entities, tado) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_CLIMATE_TIMER, diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index 3fcdb6426fb..56bd29bbd9b 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -68,7 +68,7 @@ async def async_setup_entry( tado = hass.data[DOMAIN][entry.entry_id][DATA] entities = await hass.async_add_executor_job(_generate_entities, tado) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_WATER_HEATER_TIMER, diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index 78a983254bb..404e45e0c62 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): UpbLight(upb.devices[dev], unique_id, upb) for dev in upb.devices ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_LIGHT_FADE_START, UPB_BRIGHTNESS_RATE_SCHEMA, "async_light_fade_start" diff --git a/homeassistant/components/upb/scene.py b/homeassistant/components/upb/scene.py index 81a2f23c3c6..c0c3fb0ffbc 100644 --- a/homeassistant/components/upb/scene.py +++ b/homeassistant/components/upb/scene.py @@ -20,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): unique_id = config_entry.entry_id async_add_entities(UpbLink(upb.links[link], unique_id, upb) for link in upb.links) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_LINK_DEACTIVATE, {}, "async_link_deactivate" diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index d28819a38cb..b8e7cef111c 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -94,7 +94,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(meters) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_CALIBRATE_METER, diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index b23551ddd65..bf244188a5e 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.entity_platform import current_platform +from homeassistant.helpers.entity_platform import async_get_current_platform from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DOMAIN, LOGGER, SERVICE_CAPTURE_SMARTCAM @@ -28,7 +28,7 @@ async def async_setup_entry( """Set up Verisure sensors based on a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - platform = current_platform.get() + platform = async_get_current_platform() platform.async_register_entity_service( SERVICE_CAPTURE_SMARTCAM, {}, diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index ee12fcca2cc..9c41053a34e 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.entity_platform import current_platform +from homeassistant.helpers.entity_platform import async_get_current_platform from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -36,7 +36,7 @@ async def async_setup_entry( """Set up Verisure alarm control panel from a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - platform = current_platform.get() + platform = async_get_current_platform() platform.async_register_entity_service( SERVICE_DISABLE_AUTOLOCK, {}, diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index c819c6593a1..cfbfa1ddec6 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -104,7 +104,7 @@ async def async_setup_platform( ] ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_VICARE_MODE, diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 042347a975e..3e8f0044759 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -120,7 +120,7 @@ async def async_setup_entry( entity = VizioDevice(config_entry, device, name, device_class, apps_coordinator) async_add_entities([entity], update_before_add=True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_UPDATE_SETTING, UPDATE_SETTING_SCHEMA, "async_update_setting" ) diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index a3da5edae76..6910a4c8536 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ] ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() # This will call WemoHumidifier.set_humidity(target_humidity=VALUE) platform.async_register_entity_service( diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 7d75e943d4d..5428d8a7bde 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -143,7 +143,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Timeout. No infrared command captured", title="Xiaomi Miio Remote" ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_LEARN, diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 8551a80ff89..d0bfc148594 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -150,7 +150,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): mirobo = MiroboVacuum(name, vacuum, config_entry, unique_id) entities.append(mirobo) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_START_REMOTE_CONTROL, diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 5147e57dfc6..3f7df115015 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -152,7 +152,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) # Register Service 'select_scene' - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SELECT_SCENE, {vol.Required(ATTR_SCENE): cv.string}, diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 218bcbbdb27..0782ab94c61 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -375,7 +375,7 @@ def _async_setup_services(hass: HomeAssistant): ) ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_MODE, diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 5684b22db6a..99f6230de0b 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -50,8 +50,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) - platform = entity_platform.current_platform.get() - assert platform + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( # type: ignore SERVICE_SET_LOCK_USER_CODE, diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 37923c49832..42230a9c267 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -68,8 +68,7 @@ async def async_setup_entry( ) ) - platform = entity_platform.current_platform.get() - assert platform + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_LOCK_USERCODE, From a2d12f9a517db59325856d66704436ab63e20556 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 18:40:01 +0200 Subject: [PATCH 132/852] Fix ELKM1 entity service schema (#50020) --- .../components/elkm1/alarm_control_panel.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index b925026bfaa..c3ed6bbc40d 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -14,7 +14,6 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_NIGHT, ) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, @@ -36,18 +35,15 @@ from .const import ( ELK_USER_CODE_SERVICE_SCHEMA, ) -DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids, - vol.Optional("clear", default=2): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), - vol.Optional("beep", default=False): cv.boolean, - vol.Optional("timeout", default=0): vol.All( - vol.Coerce(int), vol.Range(min=0, max=65535) - ), - vol.Optional("line1", default=""): cv.string, - vol.Optional("line2", default=""): cv.string, - } -) +DISPLAY_MESSAGE_SERVICE_SCHEMA = { + vol.Optional("clear", default=2): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), + vol.Optional("beep", default=False): cv.boolean, + vol.Optional("timeout", default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=65535) + ), + vol.Optional("line1", default=""): cv.string, + vol.Optional("line2", default=""): cv.string, +} SERVICE_ALARM_DISPLAY_MESSAGE = "alarm_display_message" SERVICE_ALARM_ARM_VACATION = "alarm_arm_vacation" From 2818df7aa9bf722bd085e38f9c379aa34bb871de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 18:40:49 +0200 Subject: [PATCH 133/852] Fix Nexia entity service schema (#50027) --- homeassistant/components/nexia/climate.py | 27 ++++++----------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index c510b905dd5..2dff498f281 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -34,12 +34,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send @@ -63,21 +58,13 @@ from .util import percent_conv SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" -SET_AIRCLEANER_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_AIRCLEANER_MODE): cv.string, - } -) +SET_AIRCLEANER_SCHEMA = { + vol.Required(ATTR_AIRCLEANER_MODE): cv.string, +} -SET_HUMIDITY_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_HUMIDITY): vol.All( - vol.Coerce(int), vol.Range(min=35, max=65) - ), - } -) +SET_HUMIDITY_SCHEMA = { + vol.Required(ATTR_HUMIDITY): vol.All(vol.Coerce(int), vol.Range(min=35, max=65)), +} # From 9ce00018be6b00682d40b6b29f3e78fab7181e62 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 18:41:16 +0200 Subject: [PATCH 134/852] Fix Harmony entity service schema (#50025) --- homeassistant/components/harmony/remote.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index f98f5bb33e7..593fbf3cb22 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -15,7 +15,6 @@ from homeassistant.components.remote import ( SUPPORT_ACTIVITY, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv @@ -44,14 +43,9 @@ PARALLEL_UPDATES = 0 ATTR_CHANNEL = "channel" -HARMONY_SYNC_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) - -HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_CHANNEL): cv.positive_int, - } -) +HARMONY_CHANGE_CHANNEL_SCHEMA = { + vol.Required(ATTR_CHANNEL): cv.positive_int, +} async def async_setup_entry( @@ -74,7 +68,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SYNC, - HARMONY_SYNC_SCHEMA, + {}, "sync", ) platform.async_register_entity_service( From d4565c0e27ac2f9f714b1e8face3aab2dde898ed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 18:41:59 +0200 Subject: [PATCH 135/852] Fix Genius Hub entity service schema (#50024) --- homeassistant/components/geniushub/switch.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index 6760ce57e01..2666f3d365b 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -4,7 +4,6 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.switch import DEVICE_CLASS_OUTLET, SwitchEntity -from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.typing import ConfigType @@ -15,15 +14,12 @@ GH_ON_OFF_ZONE = "on / off" SVC_SET_SWITCH_OVERRIDE = "set_switch_override" -SET_SWITCH_OVERRIDE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Optional(ATTR_DURATION): vol.All( - cv.time_period, - vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), - ), - } -) +SET_SWITCH_OVERRIDE_SCHEMA = { + vol.Optional(ATTR_DURATION): vol.All( + cv.time_period, + vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), + ), +} async def async_setup_platform( From 982c12bcc98e1c9edc79248d9741c8afcced8e30 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 18:42:45 +0200 Subject: [PATCH 136/852] Restore dictionary constants in Elgato device info (#50013) --- homeassistant/components/elgato/const.py | 4 ---- homeassistant/components/elgato/light.py | 17 ++++++++++++----- homeassistant/const.py | 10 +++++++++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/elgato/const.py b/homeassistant/components/elgato/const.py index 8b931fc6328..1cfb48390cf 100644 --- a/homeassistant/components/elgato/const.py +++ b/homeassistant/components/elgato/const.py @@ -7,11 +7,7 @@ DOMAIN = "elgato" DATA_ELGATO_CLIENT = "elgato_client" # Attributes -ATTR_IDENTIFIERS = "identifiers" -ATTR_MANUFACTURER = "manufacturer" -ATTR_MODEL = "model" ATTR_ON = "on" -ATTR_SOFTWARE_VERSION = "sw_version" CONF_SERIAL_NUMBER = "serial_number" diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 7f4987b2620..7ec21dd9f20 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -15,6 +15,13 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import async_get_current_platform @@ -146,11 +153,11 @@ class ElgatoLight(LightEntity): def device_info(self) -> DeviceInfo: """Return device information about this Elgato Key Light.""" return { - "identifiers": {(DOMAIN, self._info.serial_number)}, - "name": self._info.product_name, - "manufacturer": "Elgato", - "model": self._info.product_name, - "sw_version": f"{self._info.firmware_version} ({self._info.firmware_build_number})", + ATTR_IDENTIFIERS: {(DOMAIN, self._info.serial_number)}, + ATTR_NAME: self._info.product_name, + ATTR_MANUFACTURER: "Elgato", + ATTR_MODEL: self._info.product_name, + ATTR_SW_VERSION: f"{self._info.firmware_version} ({self._info.firmware_build_number})", } async def async_identify(self) -> None: diff --git a/homeassistant/const.py b/homeassistant/const.py index cb0e435c8d1..8aafc77356d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,4 +1,6 @@ """Constants used by Home Assistant components.""" +from typing import Final + MAJOR_VERSION = 2021 MINOR_VERSION = 6 PATCH_VERSION = "0.dev0" @@ -289,7 +291,7 @@ ATTR_SERVICE_DATA = "service_data" ATTR_ID = "id" # Name -ATTR_NAME = "name" +ATTR_NAME: Final = "name" # Contains one string or a list of strings, each being an entity id ATTR_ENTITY_ID = "entity_id" @@ -306,6 +308,8 @@ ATTR_FRIENDLY_NAME = "friendly_name" # A picture to represent entity ATTR_ENTITY_PICTURE = "entity_picture" +ATTR_IDENTIFIERS: Final = "identifiers" + # Icon to use in the frontend ATTR_ICON = "icon" @@ -323,6 +327,10 @@ ATTR_LOCATION = "location" ATTR_MODE = "mode" +ATTR_MANUFACTURER: Final = "manufacturer" +ATTR_MODEL: Final = "model" +ATTR_SW_VERSION: Final = "sw_version" + ATTR_BATTERY_CHARGING = "battery_charging" ATTR_BATTERY_LEVEL = "battery_level" ATTR_WAKEUP = "wake_up_interval" From 5fd8e7008ea6e6d18bedae0418ac5a0731b11e0b Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Mon, 3 May 2021 17:45:38 +0100 Subject: [PATCH 137/852] Create separate entries for each component in mypy.ini (#50030) --- mypy.ini | 1290 +++++++++++++++++++++++++++++++- script/hassfest/mypy_config.py | 16 +- 2 files changed, 1297 insertions(+), 9 deletions(-) diff --git a/mypy.ini b/mypy.ini index 636bb1589cb..30fe27177a6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -35,7 +35,579 @@ warn_return_any = false warn_unreachable = false warn_unused_ignores = false -[mypy-homeassistant.components,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.bond.*,homeassistant.components.brother.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.elgato.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.knx.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.recorder.purge,homeassistant.components.recorder.repack,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.slack.*,homeassistant.components.sonos.media_player,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.zeroconf.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*] +[mypy-homeassistant.components] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.automation.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.binary_sensor.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.bond.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.brother.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.calendar.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.cover.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.device_automation.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.elgato.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.frontend.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.geo_location.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.group.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.history.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.http.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.huawei_lte.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.hyperion.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.image_processing.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.integration.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.knx.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.light.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.lock.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.mailbox.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.media_player.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.notify.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.number.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.persistent_notification.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.proximity.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.recorder.purge] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.recorder.repack] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.remote.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.scene.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.sensor.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.slack.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.sonos.media_player] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.sun.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.switch.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.systemmonitor.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.tts.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.vacuum.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.water_heater.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.weather.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.websocket_api.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.zeroconf.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.zone.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.zwave_js.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -61,5 +633,719 @@ warn_return_any = false warn_unreachable = false warn_unused_ignores = false -[mypy-homeassistant.components.adguard.*,homeassistant.components.aemet.*,homeassistant.components.airly.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.amcrest.*,homeassistant.components.analytics.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aurora.*,homeassistant.components.awair.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.blueprint.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.bsblan.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.config.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.deconz.*,homeassistant.components.demo.*,homeassistant.components.denonavr.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dhcp.*,homeassistant.components.directv.*,homeassistant.components.doorbird.*,homeassistant.components.dsmr.*,homeassistant.components.dynalite.*,homeassistant.components.eafm.*,homeassistant.components.edl21.*,homeassistant.components.elkm1.*,homeassistant.components.emonitor.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.esphome.*,homeassistant.components.evohome.*,homeassistant.components.fan.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.flo.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.freebox.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.garmin_connect.*,homeassistant.components.geniushub.*,homeassistant.components.gios.*,homeassistant.components.glances.*,homeassistant.components.gogogate2.*,homeassistant.components.google_assistant.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.gpmdp.*,homeassistant.components.gree.*,homeassistant.components.growatt_server.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.hdmi_cec.*,homeassistant.components.here_travel_time.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.honeywell.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.image.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.insteon.*,homeassistant.components.ipp.*,homeassistant.components.isy994.*,homeassistant.components.izone.*,homeassistant.components.kaiterra.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.lifx.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.lovelace.*,homeassistant.components.luftdaten.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lyric.*,homeassistant.components.marytts.*,homeassistant.components.media_source.*,homeassistant.components.melcloud.*,homeassistant.components.meteo_france.*,homeassistant.components.metoffice.*,homeassistant.components.minecraft_server.*,homeassistant.components.mobile_app.*,homeassistant.components.modbus.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mqtt.*,homeassistant.components.mullvad.*,homeassistant.components.mysensors.*,homeassistant.components.n26.*,homeassistant.components.neato.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netio.*,homeassistant.components.nightscout.*,homeassistant.components.nilu.*,homeassistant.components.nmap_tracker.*,homeassistant.components.norway_air.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nuki.*,homeassistant.components.nws.*,homeassistant.components.nzbget.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onvif.*,homeassistant.components.ovo_energy.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_viera.*,homeassistant.components.philips_js.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.plaato.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.point.*,homeassistant.components.profiler.*,homeassistant.components.proxmoxve.*,homeassistant.components.rachio.*,homeassistant.components.rainmachine.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.reddit.*,homeassistant.components.ring.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.roku.*,homeassistant.components.rpi_power.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.sabnzbd.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.search.*,homeassistant.components.sense.*,homeassistant.components.sentry.*,homeassistant.components.sesame.*,homeassistant.components.sharkiq.*,homeassistant.components.shelly.*,homeassistant.components.sma.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.solaredge.*,homeassistant.components.solarlog.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.spotify.*,homeassistant.components.stream.*,homeassistant.components.stt.*,homeassistant.components.surepetcare.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tasmota.*,homeassistant.components.tcp.*,homeassistant.components.telegram_bot.*,homeassistant.components.template.*,homeassistant.components.tesla.*,homeassistant.components.timer.*,homeassistant.components.todoist.*,homeassistant.components.toon.*,homeassistant.components.tplink.*,homeassistant.components.trace.*,homeassistant.components.tradfri.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.unifi.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.velbus.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.vizio.*,homeassistant.components.volumio.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.wink.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wunderground.*,homeassistant.components.xbox.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.yamaha.*,homeassistant.components.yeelight.*,homeassistant.components.zerproc.*,homeassistant.components.zha.*,homeassistant.components.zwave.*] +[mypy-homeassistant.components.adguard.*] +ignore_errors = true + +[mypy-homeassistant.components.aemet.*] +ignore_errors = true + +[mypy-homeassistant.components.airly.*] +ignore_errors = true + +[mypy-homeassistant.components.alarmdecoder.*] +ignore_errors = true + +[mypy-homeassistant.components.alexa.*] +ignore_errors = true + +[mypy-homeassistant.components.almond.*] +ignore_errors = true + +[mypy-homeassistant.components.amcrest.*] +ignore_errors = true + +[mypy-homeassistant.components.analytics.*] +ignore_errors = true + +[mypy-homeassistant.components.asuswrt.*] +ignore_errors = true + +[mypy-homeassistant.components.atag.*] +ignore_errors = true + +[mypy-homeassistant.components.aurora.*] +ignore_errors = true + +[mypy-homeassistant.components.awair.*] +ignore_errors = true + +[mypy-homeassistant.components.azure_devops.*] +ignore_errors = true + +[mypy-homeassistant.components.azure_event_hub.*] +ignore_errors = true + +[mypy-homeassistant.components.blueprint.*] +ignore_errors = true + +[mypy-homeassistant.components.bluetooth_tracker.*] +ignore_errors = true + +[mypy-homeassistant.components.bmw_connected_drive.*] +ignore_errors = true + +[mypy-homeassistant.components.bsblan.*] +ignore_errors = true + +[mypy-homeassistant.components.camera.*] +ignore_errors = true + +[mypy-homeassistant.components.canary.*] +ignore_errors = true + +[mypy-homeassistant.components.cast.*] +ignore_errors = true + +[mypy-homeassistant.components.cert_expiry.*] +ignore_errors = true + +[mypy-homeassistant.components.climacell.*] +ignore_errors = true + +[mypy-homeassistant.components.climate.*] +ignore_errors = true + +[mypy-homeassistant.components.cloud.*] +ignore_errors = true + +[mypy-homeassistant.components.cloudflare.*] +ignore_errors = true + +[mypy-homeassistant.components.config.*] +ignore_errors = true + +[mypy-homeassistant.components.control4.*] +ignore_errors = true + +[mypy-homeassistant.components.conversation.*] +ignore_errors = true + +[mypy-homeassistant.components.deconz.*] +ignore_errors = true + +[mypy-homeassistant.components.demo.*] +ignore_errors = true + +[mypy-homeassistant.components.denonavr.*] +ignore_errors = true + +[mypy-homeassistant.components.device_tracker.*] +ignore_errors = true + +[mypy-homeassistant.components.devolo_home_control.*] +ignore_errors = true + +[mypy-homeassistant.components.dhcp.*] +ignore_errors = true + +[mypy-homeassistant.components.directv.*] +ignore_errors = true + +[mypy-homeassistant.components.doorbird.*] +ignore_errors = true + +[mypy-homeassistant.components.dsmr.*] +ignore_errors = true + +[mypy-homeassistant.components.dynalite.*] +ignore_errors = true + +[mypy-homeassistant.components.eafm.*] +ignore_errors = true + +[mypy-homeassistant.components.edl21.*] +ignore_errors = true + +[mypy-homeassistant.components.elkm1.*] +ignore_errors = true + +[mypy-homeassistant.components.emonitor.*] +ignore_errors = true + +[mypy-homeassistant.components.enphase_envoy.*] +ignore_errors = true + +[mypy-homeassistant.components.entur_public_transport.*] +ignore_errors = true + +[mypy-homeassistant.components.esphome.*] +ignore_errors = true + +[mypy-homeassistant.components.evohome.*] +ignore_errors = true + +[mypy-homeassistant.components.fan.*] +ignore_errors = true + +[mypy-homeassistant.components.filter.*] +ignore_errors = true + +[mypy-homeassistant.components.fints.*] +ignore_errors = true + +[mypy-homeassistant.components.fireservicerota.*] +ignore_errors = true + +[mypy-homeassistant.components.firmata.*] +ignore_errors = true + +[mypy-homeassistant.components.fitbit.*] +ignore_errors = true + +[mypy-homeassistant.components.flo.*] +ignore_errors = true + +[mypy-homeassistant.components.fortios.*] +ignore_errors = true + +[mypy-homeassistant.components.foscam.*] +ignore_errors = true + +[mypy-homeassistant.components.freebox.*] +ignore_errors = true + +[mypy-homeassistant.components.fritz.*] +ignore_errors = true + +[mypy-homeassistant.components.fritzbox.*] +ignore_errors = true + +[mypy-homeassistant.components.garmin_connect.*] +ignore_errors = true + +[mypy-homeassistant.components.geniushub.*] +ignore_errors = true + +[mypy-homeassistant.components.gios.*] +ignore_errors = true + +[mypy-homeassistant.components.glances.*] +ignore_errors = true + +[mypy-homeassistant.components.gogogate2.*] +ignore_errors = true + +[mypy-homeassistant.components.google_assistant.*] +ignore_errors = true + +[mypy-homeassistant.components.google_maps.*] +ignore_errors = true + +[mypy-homeassistant.components.google_pubsub.*] +ignore_errors = true + +[mypy-homeassistant.components.gpmdp.*] +ignore_errors = true + +[mypy-homeassistant.components.gree.*] +ignore_errors = true + +[mypy-homeassistant.components.growatt_server.*] +ignore_errors = true + +[mypy-homeassistant.components.gtfs.*] +ignore_errors = true + +[mypy-homeassistant.components.guardian.*] +ignore_errors = true + +[mypy-homeassistant.components.habitica.*] +ignore_errors = true + +[mypy-homeassistant.components.harmony.*] +ignore_errors = true + +[mypy-homeassistant.components.hassio.*] +ignore_errors = true + +[mypy-homeassistant.components.hdmi_cec.*] +ignore_errors = true + +[mypy-homeassistant.components.here_travel_time.*] +ignore_errors = true + +[mypy-homeassistant.components.hisense_aehw4a1.*] +ignore_errors = true + +[mypy-homeassistant.components.home_connect.*] +ignore_errors = true + +[mypy-homeassistant.components.home_plus_control.*] +ignore_errors = true + +[mypy-homeassistant.components.homeassistant.*] +ignore_errors = true + +[mypy-homeassistant.components.homekit.*] +ignore_errors = true + +[mypy-homeassistant.components.homekit_controller.*] +ignore_errors = true + +[mypy-homeassistant.components.homematicip_cloud.*] +ignore_errors = true + +[mypy-homeassistant.components.honeywell.*] +ignore_errors = true + +[mypy-homeassistant.components.hue.*] +ignore_errors = true + +[mypy-homeassistant.components.huisbaasje.*] +ignore_errors = true + +[mypy-homeassistant.components.humidifier.*] +ignore_errors = true + +[mypy-homeassistant.components.iaqualink.*] +ignore_errors = true + +[mypy-homeassistant.components.icloud.*] +ignore_errors = true + +[mypy-homeassistant.components.image.*] +ignore_errors = true + +[mypy-homeassistant.components.incomfort.*] +ignore_errors = true + +[mypy-homeassistant.components.influxdb.*] +ignore_errors = true + +[mypy-homeassistant.components.input_boolean.*] +ignore_errors = true + +[mypy-homeassistant.components.input_datetime.*] +ignore_errors = true + +[mypy-homeassistant.components.input_number.*] +ignore_errors = true + +[mypy-homeassistant.components.insteon.*] +ignore_errors = true + +[mypy-homeassistant.components.ipp.*] +ignore_errors = true + +[mypy-homeassistant.components.isy994.*] +ignore_errors = true + +[mypy-homeassistant.components.izone.*] +ignore_errors = true + +[mypy-homeassistant.components.kaiterra.*] +ignore_errors = true + +[mypy-homeassistant.components.keenetic_ndms2.*] +ignore_errors = true + +[mypy-homeassistant.components.kodi.*] +ignore_errors = true + +[mypy-homeassistant.components.konnected.*] +ignore_errors = true + +[mypy-homeassistant.components.kostal_plenticore.*] +ignore_errors = true + +[mypy-homeassistant.components.kulersky.*] +ignore_errors = true + +[mypy-homeassistant.components.lifx.*] +ignore_errors = true + +[mypy-homeassistant.components.litejet.*] +ignore_errors = true + +[mypy-homeassistant.components.litterrobot.*] +ignore_errors = true + +[mypy-homeassistant.components.lovelace.*] +ignore_errors = true + +[mypy-homeassistant.components.luftdaten.*] +ignore_errors = true + +[mypy-homeassistant.components.lutron_caseta.*] +ignore_errors = true + +[mypy-homeassistant.components.lyric.*] +ignore_errors = true + +[mypy-homeassistant.components.marytts.*] +ignore_errors = true + +[mypy-homeassistant.components.media_source.*] +ignore_errors = true + +[mypy-homeassistant.components.melcloud.*] +ignore_errors = true + +[mypy-homeassistant.components.meteo_france.*] +ignore_errors = true + +[mypy-homeassistant.components.metoffice.*] +ignore_errors = true + +[mypy-homeassistant.components.minecraft_server.*] +ignore_errors = true + +[mypy-homeassistant.components.mobile_app.*] +ignore_errors = true + +[mypy-homeassistant.components.modbus.*] +ignore_errors = true + +[mypy-homeassistant.components.motion_blinds.*] +ignore_errors = true + +[mypy-homeassistant.components.motioneye.*] +ignore_errors = true + +[mypy-homeassistant.components.mqtt.*] +ignore_errors = true + +[mypy-homeassistant.components.mullvad.*] +ignore_errors = true + +[mypy-homeassistant.components.mysensors.*] +ignore_errors = true + +[mypy-homeassistant.components.n26.*] +ignore_errors = true + +[mypy-homeassistant.components.neato.*] +ignore_errors = true + +[mypy-homeassistant.components.ness_alarm.*] +ignore_errors = true + +[mypy-homeassistant.components.nest.*] +ignore_errors = true + +[mypy-homeassistant.components.netatmo.*] +ignore_errors = true + +[mypy-homeassistant.components.netio.*] +ignore_errors = true + +[mypy-homeassistant.components.nightscout.*] +ignore_errors = true + +[mypy-homeassistant.components.nilu.*] +ignore_errors = true + +[mypy-homeassistant.components.nmap_tracker.*] +ignore_errors = true + +[mypy-homeassistant.components.norway_air.*] +ignore_errors = true + +[mypy-homeassistant.components.notion.*] +ignore_errors = true + +[mypy-homeassistant.components.nsw_fuel_station.*] +ignore_errors = true + +[mypy-homeassistant.components.nuki.*] +ignore_errors = true + +[mypy-homeassistant.components.nws.*] +ignore_errors = true + +[mypy-homeassistant.components.nzbget.*] +ignore_errors = true + +[mypy-homeassistant.components.omnilogic.*] +ignore_errors = true + +[mypy-homeassistant.components.onboarding.*] +ignore_errors = true + +[mypy-homeassistant.components.ondilo_ico.*] +ignore_errors = true + +[mypy-homeassistant.components.onewire.*] +ignore_errors = true + +[mypy-homeassistant.components.onvif.*] +ignore_errors = true + +[mypy-homeassistant.components.ovo_energy.*] +ignore_errors = true + +[mypy-homeassistant.components.ozw.*] +ignore_errors = true + +[mypy-homeassistant.components.panasonic_viera.*] +ignore_errors = true + +[mypy-homeassistant.components.philips_js.*] +ignore_errors = true + +[mypy-homeassistant.components.pilight.*] +ignore_errors = true + +[mypy-homeassistant.components.ping.*] +ignore_errors = true + +[mypy-homeassistant.components.pioneer.*] +ignore_errors = true + +[mypy-homeassistant.components.plaato.*] +ignore_errors = true + +[mypy-homeassistant.components.plex.*] +ignore_errors = true + +[mypy-homeassistant.components.plugwise.*] +ignore_errors = true + +[mypy-homeassistant.components.plum_lightpad.*] +ignore_errors = true + +[mypy-homeassistant.components.point.*] +ignore_errors = true + +[mypy-homeassistant.components.profiler.*] +ignore_errors = true + +[mypy-homeassistant.components.proxmoxve.*] +ignore_errors = true + +[mypy-homeassistant.components.rachio.*] +ignore_errors = true + +[mypy-homeassistant.components.rainmachine.*] +ignore_errors = true + +[mypy-homeassistant.components.recollect_waste.*] +ignore_errors = true + +[mypy-homeassistant.components.recorder.*] +ignore_errors = true + +[mypy-homeassistant.components.reddit.*] +ignore_errors = true + +[mypy-homeassistant.components.ring.*] +ignore_errors = true + +[mypy-homeassistant.components.rituals_perfume_genie.*] +ignore_errors = true + +[mypy-homeassistant.components.roku.*] +ignore_errors = true + +[mypy-homeassistant.components.rpi_power.*] +ignore_errors = true + +[mypy-homeassistant.components.ruckus_unleashed.*] +ignore_errors = true + +[mypy-homeassistant.components.sabnzbd.*] +ignore_errors = true + +[mypy-homeassistant.components.screenlogic.*] +ignore_errors = true + +[mypy-homeassistant.components.script.*] +ignore_errors = true + +[mypy-homeassistant.components.search.*] +ignore_errors = true + +[mypy-homeassistant.components.sense.*] +ignore_errors = true + +[mypy-homeassistant.components.sentry.*] +ignore_errors = true + +[mypy-homeassistant.components.sesame.*] +ignore_errors = true + +[mypy-homeassistant.components.sharkiq.*] +ignore_errors = true + +[mypy-homeassistant.components.shelly.*] +ignore_errors = true + +[mypy-homeassistant.components.sma.*] +ignore_errors = true + +[mypy-homeassistant.components.smart_meter_texas.*] +ignore_errors = true + +[mypy-homeassistant.components.smartthings.*] +ignore_errors = true + +[mypy-homeassistant.components.smarttub.*] +ignore_errors = true + +[mypy-homeassistant.components.smarty.*] +ignore_errors = true + +[mypy-homeassistant.components.smhi.*] +ignore_errors = true + +[mypy-homeassistant.components.solaredge.*] +ignore_errors = true + +[mypy-homeassistant.components.solarlog.*] +ignore_errors = true + +[mypy-homeassistant.components.somfy.*] +ignore_errors = true + +[mypy-homeassistant.components.somfy_mylink.*] +ignore_errors = true + +[mypy-homeassistant.components.sonarr.*] +ignore_errors = true + +[mypy-homeassistant.components.songpal.*] +ignore_errors = true + +[mypy-homeassistant.components.sonos.*] +ignore_errors = true + +[mypy-homeassistant.components.spotify.*] +ignore_errors = true + +[mypy-homeassistant.components.stream.*] +ignore_errors = true + +[mypy-homeassistant.components.stt.*] +ignore_errors = true + +[mypy-homeassistant.components.surepetcare.*] +ignore_errors = true + +[mypy-homeassistant.components.switchbot.*] +ignore_errors = true + +[mypy-homeassistant.components.switcher_kis.*] +ignore_errors = true + +[mypy-homeassistant.components.synology_dsm.*] +ignore_errors = true + +[mypy-homeassistant.components.synology_srm.*] +ignore_errors = true + +[mypy-homeassistant.components.system_health.*] +ignore_errors = true + +[mypy-homeassistant.components.system_log.*] +ignore_errors = true + +[mypy-homeassistant.components.tado.*] +ignore_errors = true + +[mypy-homeassistant.components.tasmota.*] +ignore_errors = true + +[mypy-homeassistant.components.tcp.*] +ignore_errors = true + +[mypy-homeassistant.components.telegram_bot.*] +ignore_errors = true + +[mypy-homeassistant.components.template.*] +ignore_errors = true + +[mypy-homeassistant.components.tesla.*] +ignore_errors = true + +[mypy-homeassistant.components.timer.*] +ignore_errors = true + +[mypy-homeassistant.components.todoist.*] +ignore_errors = true + +[mypy-homeassistant.components.toon.*] +ignore_errors = true + +[mypy-homeassistant.components.tplink.*] +ignore_errors = true + +[mypy-homeassistant.components.trace.*] +ignore_errors = true + +[mypy-homeassistant.components.tradfri.*] +ignore_errors = true + +[mypy-homeassistant.components.tuya.*] +ignore_errors = true + +[mypy-homeassistant.components.twentemilieu.*] +ignore_errors = true + +[mypy-homeassistant.components.unifi.*] +ignore_errors = true + +[mypy-homeassistant.components.upcloud.*] +ignore_errors = true + +[mypy-homeassistant.components.updater.*] +ignore_errors = true + +[mypy-homeassistant.components.upnp.*] +ignore_errors = true + +[mypy-homeassistant.components.velbus.*] +ignore_errors = true + +[mypy-homeassistant.components.vera.*] +ignore_errors = true + +[mypy-homeassistant.components.verisure.*] +ignore_errors = true + +[mypy-homeassistant.components.vizio.*] +ignore_errors = true + +[mypy-homeassistant.components.volumio.*] +ignore_errors = true + +[mypy-homeassistant.components.webostv.*] +ignore_errors = true + +[mypy-homeassistant.components.wemo.*] +ignore_errors = true + +[mypy-homeassistant.components.wink.*] +ignore_errors = true + +[mypy-homeassistant.components.withings.*] +ignore_errors = true + +[mypy-homeassistant.components.wled.*] +ignore_errors = true + +[mypy-homeassistant.components.wunderground.*] +ignore_errors = true + +[mypy-homeassistant.components.xbox.*] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_aqara.*] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.*] +ignore_errors = true + +[mypy-homeassistant.components.yamaha.*] +ignore_errors = true + +[mypy-homeassistant.components.yeelight.*] +ignore_errors = true + +[mypy-homeassistant.components.zerproc.*] +ignore_errors = true + +[mypy-homeassistant.components.zha.*] +ignore_errors = true + +[mypy-homeassistant.components.zwave.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 3f6f46ab894..05763fb6517 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -350,10 +350,11 @@ def generate_and_validate(config: Config) -> str: for key in STRICT_SETTINGS: mypy_config.set(components_section, key, "false") - strict_section = "mypy-" + ",".join(strict_modules) - mypy_config.add_section(strict_section) - for key in STRICT_SETTINGS: - mypy_config.set(strict_section, key, "true") + for strict_module in strict_modules: + strict_section = f"mypy-{strict_module}" + mypy_config.add_section(strict_section) + for key in STRICT_SETTINGS: + mypy_config.set(strict_section, key, "true") # Disable strict checks for tests tests_section = "mypy-tests.*" @@ -361,9 +362,10 @@ def generate_and_validate(config: Config) -> str: for key in STRICT_SETTINGS: mypy_config.set(tests_section, key, "false") - ignored_section = "mypy-" + ",".join(IGNORED_MODULES) - mypy_config.add_section(ignored_section) - mypy_config.set(ignored_section, "ignore_errors", "true") + for ignored_module in IGNORED_MODULES: + ignored_section = f"mypy-{ignored_module}" + mypy_config.add_section(ignored_section) + mypy_config.set(ignored_section, "ignore_errors", "true") with io.StringIO() as fp: mypy_config.write(fp) From c49fa6f1ed7e100b8308b65f8f888055e6e4cb97 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Mon, 3 May 2021 18:51:23 +0200 Subject: [PATCH 138/852] Bump pysmappee to 0.2.25 (#50031) --- homeassistant/components/smappee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index d6e9cc69f6f..b4250332120 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], "requirements": [ - "pysmappee==0.2.24" + "pysmappee==0.2.25" ], "codeowners": [ "@bsmappee" diff --git a/requirements_all.txt b/requirements_all.txt index 649da5c6366..6fac80347f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1720,7 +1720,7 @@ pyskyqhub==0.1.3 pysma==0.4.3 # homeassistant.components.smappee -pysmappee==0.2.24 +pysmappee==0.2.25 # homeassistant.components.smartthings pysmartapp==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ba3edb332d..a1c6a5b27af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ pysignalclirestapi==0.3.4 pysma==0.4.3 # homeassistant.components.smappee -pysmappee==0.2.24 +pysmappee==0.2.25 # homeassistant.components.smartthings pysmartapp==0.3.3 From 302cab185dc40dcb29ec7bc804390cc94e08409a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 May 2021 07:30:22 -1000 Subject: [PATCH 139/852] Add reauth support to flume (#49991) --- homeassistant/components/flume/__init__.py | 53 +++++------ homeassistant/components/flume/config_flow.py | 92 ++++++++++++++----- homeassistant/components/flume/strings.json | 10 +- .../components/flume/translations/en.json | 10 +- tests/components/flume/test_config_flow.py | 83 ++++++++++++++++- 5 files changed, 192 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index c8e652fefd6..9bdc918be9c 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -1,7 +1,4 @@ """The flume integration.""" -from functools import partial -import logging - from pyflume import FlumeAuth, FlumeDeviceList from requests import Session from requests.exceptions import RequestException @@ -14,7 +11,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import ( BASE_TOKEN_FILENAME, @@ -25,12 +22,9 @@ from .const import ( PLATFORMS, ) -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up flume from a config entry.""" +def _setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Config entry set up in executor.""" config = entry.data username = config[CONF_USERNAME] @@ -42,32 +36,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): http_session = Session() try: - flume_auth = await hass.async_add_executor_job( - partial( - FlumeAuth, - username, - password, - client_id, - client_secret, - flume_token_file=flume_token_full_path, - http_session=http_session, - ) - ) - flume_devices = await hass.async_add_executor_job( - partial( - FlumeDeviceList, - flume_auth, - http_session=http_session, - ) + flume_auth = FlumeAuth( + username, + password, + client_id, + client_secret, + flume_token_file=flume_token_full_path, + http_session=http_session, ) + flume_devices = FlumeDeviceList(flume_auth, http_session=http_session) except RequestException as ex: raise ConfigEntryNotReady from ex except Exception as ex: # pylint: disable=broad-except - _LOGGER.error("Invalid credentials for flume: %s", ex) - return False + raise ConfigEntryAuthFailed from ex - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { + return flume_auth, flume_devices, http_session + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up flume from a config entry.""" + + flume_auth, flume_devices, http_session = await hass.async_add_executor_job( + _setup_entry, hass, entry + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { FLUME_DEVICES: flume_devices, FLUME_AUTH: flume_auth, FLUME_HTTP_SESSION: http_session, diff --git a/homeassistant/components/flume/config_flow.py b/homeassistant/components/flume/config_flow.py index 49ae50d8912..1bab8817dbb 100644 --- a/homeassistant/components/flume/config_flow.py +++ b/homeassistant/components/flume/config_flow.py @@ -1,6 +1,6 @@ """Config flow for flume integration.""" -from functools import partial import logging +import os from pyflume import FlumeAuth, FlumeDeviceList from requests.exceptions import RequestException @@ -33,38 +33,46 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +def _validate_input(hass: core.HomeAssistant, data: dict, clear_token_file: bool): + """Validate in the executor.""" + flume_token_full_path = hass.config.path( + f"{BASE_TOKEN_FILENAME}-{data[CONF_USERNAME]}" + ) + if clear_token_file and os.path.exists(flume_token_full_path): + os.unlink(flume_token_full_path) + + return FlumeDeviceList( + FlumeAuth( + data[CONF_USERNAME], + data[CONF_PASSWORD], + data[CONF_CLIENT_ID], + data[CONF_CLIENT_SECRET], + flume_token_file=flume_token_full_path, + ) + ) + + +async def validate_input( + hass: core.HomeAssistant, data: dict, clear_token_file: bool = False +): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ - username = data[CONF_USERNAME] - password = data[CONF_PASSWORD] - client_id = data[CONF_CLIENT_ID] - client_secret = data[CONF_CLIENT_SECRET] - flume_token_full_path = hass.config.path(f"{BASE_TOKEN_FILENAME}-{username}") - try: - flume_auth = await hass.async_add_executor_job( - partial( - FlumeAuth, - username, - password, - client_id, - client_secret, - flume_token_file=flume_token_full_path, - ) + flume_devices = await hass.async_add_executor_job( + _validate_input, hass, data, clear_token_file ) - flume_devices = await hass.async_add_executor_job(FlumeDeviceList, flume_auth) except RequestException as err: raise CannotConnect from err except Exception as err: + _LOGGER.exception("Auth exception") raise InvalidAuth from err if not flume_devices or not flume_devices.device_list: raise CannotConnect # Return info that you want to store in the config entry. - return {"title": username} + return {"title": data[CONF_USERNAME]} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -72,6 +80,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self): + """Init flume config flow.""" + self._reauth_unique_id = None + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} @@ -85,10 +97,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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" + errors[CONF_PASSWORD] = "invalid_auth" return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors @@ -98,6 +107,43 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle import.""" return await self.async_step_user(user_input) + async def async_step_reauth(self, user_input=None): + """Handle reauth.""" + self._reauth_unique_id = self.context["unique_id"] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle reauth input.""" + errors = {} + existing_entry = await self.async_set_unique_id(self._reauth_unique_id) + if user_input is not None: + new_data = {**existing_entry.data, CONF_PASSWORD: user_input[CONF_PASSWORD]} + try: + await validate_input(self.hass, new_data, clear_token_file=True) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors[CONF_PASSWORD] = "invalid_auth" + else: + self.hass.config_entries.async_update_entry( + existing_entry, data=new_data + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + description_placeholders={ + CONF_USERNAME: existing_entry.data[CONF_USERNAME] + }, + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/flume/strings.json b/homeassistant/components/flume/strings.json index 67b4a95d069..5c95cfca22e 100644 --- a/homeassistant/components/flume/strings.json +++ b/homeassistant/components/flume/strings.json @@ -15,9 +15,17 @@ "client_id": "Client ID", "password": "[%key:common::config_flow::data::password%]" } - } + }, + "reauth_confirm": { + "description": "The password for {username} is no longer valid.", + "title": "Reauthenticate your Flume Account", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } }, "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } } diff --git a/homeassistant/components/flume/translations/en.json b/homeassistant/components/flume/translations/en.json index ac7d4335903..e70566f4315 100644 --- a/homeassistant/components/flume/translations/en.json +++ b/homeassistant/components/flume/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,6 +10,13 @@ "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The password for {username} is no longer valid.", + "title": "Reauthenticate your Flume Account" + }, "user": { "data": { "client_id": "Client ID", diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 3a9e3376f05..5c439933b0b 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -12,6 +12,8 @@ from homeassistant.const import ( CONF_USERNAME, ) +from tests.common import MockConfigEntry + def _get_mocked_flume_device_list(): flume_device_list_mock = MagicMock() @@ -124,7 +126,7 @@ async def test_form_invalid_auth(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} + assert result2["errors"] == {"password": "invalid_auth"} async def test_form_cannot_connect(hass): @@ -151,3 +153,82 @@ async def test_form_cannot_connect(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth(hass): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "test@test.org", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + }, + unique_id="test@test.org", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "unique_id": "test@test.org"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"password": "invalid_auth"} + + with patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + side_effect=requests.exceptions.ConnectionError(), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "cannot_connect"} + + mock_flume_device_list = _get_mocked_flume_device_list() + + with patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + return_value=mock_flume_device_list, + ), patch( + "homeassistant.components.flume.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert mock_setup_entry.called + assert result4["type"] == "abort" + assert result4["reason"] == "reauth_successful" From c69eeddc7bd6c6360b2210fe0ad53319ed10c671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 3 May 2021 19:50:39 +0200 Subject: [PATCH 140/852] Upgrade Tibber library, new grid prices for Glitre Energi (#50029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 01a20011bef..ee2b8404405 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.16.2"], + "requirements": ["pyTibber==0.16.3"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 6fac80347f4..75c409cb11b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1250,7 +1250,7 @@ pyRFXtrx==0.26.1 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.16.2 +pyTibber==0.16.3 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1c6a5b27af..7989e6cfbf9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -678,7 +678,7 @@ pyMetno==0.8.3 pyRFXtrx==0.26.1 # homeassistant.components.tibber -pyTibber==0.16.2 +pyTibber==0.16.3 # homeassistant.components.nextbus py_nextbusnext==0.1.4 From f0ec9c38b0e62d1046fca78daf965c90471b49d5 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 3 May 2021 22:45:21 +0200 Subject: [PATCH 141/852] Fix modbus typing (#49938) Add changes needed to please mypy and follow the coding rules of the project. --- homeassistant/components/modbus/binary_sensor.py | 7 ++----- homeassistant/components/modbus/climate.py | 10 +++++----- homeassistant/components/modbus/sensor.py | 6 ++---- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 979888d0a19..d04938c929a 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -35,7 +35,6 @@ from .const import ( DEFAULT_SCAN_INTERVAL, MODBUS_DOMAIN, ) -from .modbus import ModbusHub _LOGGER = logging.getLogger(__name__) @@ -84,14 +83,12 @@ async def async_setup_platform( CONF_NAME: "no name", CONF_BINARY_SENSORS: config[CONF_INPUTS], } - config = None for entry in discovery_info[CONF_BINARY_SENSORS]: if CONF_HUB in entry: - # from old config! - hub: ModbusHub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] + hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] else: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] if CONF_SCAN_INTERVAL not in entry: entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL sensors.append( diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index d98cda3ed43..cc8f74577c7 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -242,7 +242,7 @@ class ModbusThermostat(ClimateEntity): ) if result is None: self._available = False - return + return -1 byte_string = b"".join( [x.to_bytes(2, byteorder="big") for x in result.registers] @@ -255,11 +255,11 @@ class ModbusThermostat(ClimateEntity): ) return -1 - val = val[0] + val2 = val[0] register_value = format( - (self._scale * val) + self._offset, f".{self._precision}f" + (self._scale * val2) + self._offset, f".{self._precision}f" ) - register_value = float(register_value) + register_value2 = float(register_value) self._available = True - return register_value + return register_value2 diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index c1a33c41f6d..91f80864f73 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -58,7 +58,6 @@ from .const import ( DEFAULT_STRUCT_FORMAT, MODBUS_DOMAIN, ) -from .modbus import ModbusHub _LOGGER = logging.getLogger(__name__) @@ -120,7 +119,6 @@ async def async_setup_platform( entry[CONF_INPUT_TYPE] = entry[CONF_REGISTER_TYPE] del entry[CONF_REGISTER] del entry[CONF_REGISTER_TYPE] - config = None for entry in discovery_info[CONF_SENSORS]: if entry[CONF_DATA_TYPE] == DATA_TYPE_STRING: @@ -175,9 +173,9 @@ async def async_setup_platform( continue if CONF_HUB in entry: # from old config! - hub: ModbusHub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] + hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] else: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] if CONF_SCAN_INTERVAL not in entry: entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL sensors.append( diff --git a/mypy.ini b/mypy.ini index 30fe27177a6..42e5c0101f2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -993,9 +993,6 @@ ignore_errors = true [mypy-homeassistant.components.mobile_app.*] ignore_errors = true -[mypy-homeassistant.components.modbus.*] -ignore_errors = true - [mypy-homeassistant.components.motion_blinds.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 05763fb6517..a840723b20e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -134,7 +134,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.metoffice.*", "homeassistant.components.minecraft_server.*", "homeassistant.components.mobile_app.*", - "homeassistant.components.modbus.*", "homeassistant.components.motion_blinds.*", "homeassistant.components.motioneye.*", "homeassistant.components.mqtt.*", From 0df9454310f8eed8dfee0159fc59a20a7981dd46 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 4 May 2021 00:03:46 +0000 Subject: [PATCH 142/852] [ci skip] Translation update --- .../components/deconz/translations/cs.json | 4 ++ .../devolo_home_control/translations/cs.json | 7 ++++ .../components/emonitor/translations/cs.json | 7 ++++ .../enphase_envoy/translations/cs.json | 16 ++++++++ .../components/flume/translations/ca.json | 10 ++++- .../components/flume/translations/et.json | 10 ++++- .../components/flume/translations/ru.json | 10 ++++- .../components/fritz/translations/cs.json | 37 +++++++++++++++++++ .../components/fritzbox/translations/ca.json | 2 +- .../components/fritzbox/translations/en.json | 2 +- .../components/fritzbox/translations/et.json | 2 +- .../components/fritzbox/translations/ru.json | 2 +- .../fritzbox/translations/zh-Hant.json | 2 +- .../components/goalzero/translations/no.json | 2 +- .../google_travel_time/translations/cs.json | 28 ++++++++++++++ .../google_travel_time/translations/no.json | 1 + .../components/motioneye/translations/cs.json | 21 +++++++++++ .../components/mutesync/translations/cs.json | 15 ++++++++ .../components/myq/translations/ca.json | 10 ++++- .../components/myq/translations/cs.json | 8 +++- .../components/myq/translations/et.json | 10 ++++- .../components/myq/translations/nl.json | 10 ++++- .../components/myq/translations/no.json | 10 ++++- .../components/myq/translations/ru.json | 10 ++++- .../components/myq/translations/zh-Hant.json | 10 ++++- .../components/omnilogic/translations/no.json | 1 + .../components/picnic/translations/cs.json | 20 ++++++++++ .../components/smarttub/translations/cs.json | 3 ++ .../waze_travel_time/translations/cs.json | 7 ++++ .../waze_travel_time/translations/no.json | 1 + .../components/zha/translations/no.json | 13 +++++++ 31 files changed, 275 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/fritz/translations/cs.json create mode 100644 homeassistant/components/google_travel_time/translations/cs.json create mode 100644 homeassistant/components/motioneye/translations/cs.json create mode 100644 homeassistant/components/mutesync/translations/cs.json create mode 100644 homeassistant/components/picnic/translations/cs.json diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index c198068e07e..323b29ddac8 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -42,6 +42,10 @@ "button_2": "Druh\u00e9 tla\u010d\u00edtko", "button_3": "T\u0159et\u00ed tla\u010d\u00edtko", "button_4": "\u010ctvrt\u00e9 tla\u010d\u00edtko", + "button_5": "P\u00e1t\u00e9 tla\u010d\u00edtko", + "button_6": "\u0160est\u00e9 tla\u010d\u00edtko", + "button_7": "Sedm\u00e9 tla\u010d\u00edtko", + "button_8": "Osm\u00e9 tla\u010d\u00edtko", "close": "Zav\u0159\u00edt", "dim_down": "Sn\u00ed\u017eit ztlumen\u00ed", "dim_up": "Zv\u00fd\u0161it ztlumen\u00ed", diff --git a/homeassistant/components/devolo_home_control/translations/cs.json b/homeassistant/components/devolo_home_control/translations/cs.json index efe2c4d2ce5..f41c2dc218f 100644 --- a/homeassistant/components/devolo_home_control/translations/cs.json +++ b/homeassistant/components/devolo_home_control/translations/cs.json @@ -14,6 +14,13 @@ "password": "Heslo", "username": "E-mail / Devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Heslo", + "username": "E-mail / devolo ID" + } } } } diff --git a/homeassistant/components/emonitor/translations/cs.json b/homeassistant/components/emonitor/translations/cs.json index 347c9ee3ae0..42eff97466e 100644 --- a/homeassistant/components/emonitor/translations/cs.json +++ b/homeassistant/components/emonitor/translations/cs.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "flow_title": "SiteSage {name}", "step": { "confirm": { diff --git a/homeassistant/components/enphase_envoy/translations/cs.json b/homeassistant/components/enphase_envoy/translations/cs.json index 08830492748..30c3bb1c012 100644 --- a/homeassistant/components/enphase_envoy/translations/cs.json +++ b/homeassistant/components/enphase_envoy/translations/cs.json @@ -1,7 +1,23 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/flume/translations/ca.json b/homeassistant/components/flume/translations/ca.json index e612b29db67..04a7accf4a5 100644 --- a/homeassistant/components/flume/translations/ca.json +++ b/homeassistant/components/flume/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +10,13 @@ "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La contrasenya de {username} ja no \u00e9s v\u00e0lida.", + "title": "Torna a autenticar el compte Flume" + }, "user": { "data": { "client_id": "ID de client", diff --git a/homeassistant/components/flume/translations/et.json b/homeassistant/components/flume/translations/et.json index e1a080b49a3..d2ef2b98454 100644 --- a/homeassistant/components/flume/translations/et.json +++ b/homeassistant/components/flume/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto on juba seadistatud" + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -9,6 +10,13 @@ "unknown": "Tundmatu viga" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Kasutaja {username} salas\u00f5na ei kehti enam.", + "title": "Taastuvasta oma Flume konto" + }, "user": { "data": { "client_id": "Kliendi ID", diff --git a/homeassistant/components/flume/translations/ru.json b/homeassistant/components/flume/translations/ru.json index 757ec6e5226..1eeba0cdc8e 100644 --- a/homeassistant/components/flume/translations/ru.json +++ b/homeassistant/components/flume/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -9,6 +10,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {username} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f Flume" + }, "user": { "data": { "client_id": "ID \u043a\u043b\u0438\u0435\u043d\u0442\u0430", diff --git a/homeassistant/components/fritz/translations/cs.json b/homeassistant/components/fritz/translations/cs.json new file mode 100644 index 00000000000..75ad51d8a1e --- /dev/null +++ b/homeassistant/components/fritz/translations/cs.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "start_config": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/ca.json b/homeassistant/components/fritzbox/translations/ca.json index f8550b5bc32..9324f91ef18 100644 --- a/homeassistant/components/fritzbox/translations/ca.json +++ b/homeassistant/components/fritzbox/translations/ca.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "AVM FRITZ!SmartHome: {name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/en.json b/homeassistant/components/fritzbox/translations/en.json index 61ca1e957bb..1988dcde1a4 100644 --- a/homeassistant/components/fritzbox/translations/en.json +++ b/homeassistant/components/fritzbox/translations/en.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Invalid authentication" }, - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "AVM FRITZ!SmartHome: {name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/et.json b/homeassistant/components/fritzbox/translations/et.json index 5ee2dc801f4..96c77903f97 100644 --- a/homeassistant/components/fritzbox/translations/et.json +++ b/homeassistant/components/fritzbox/translations/et.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Tuvastamise viga" }, - "flow_title": "", + "flow_title": "AVM FRITZ! SmartHome: {name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json index adbdfa13d6b..5ca83042497 100644 --- a/homeassistant/components/fritzbox/translations/ru.json +++ b/homeassistant/components/fritzbox/translations/ru.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "AVM FRITZ!SmartHome: {name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index 9c901bd92e0..d27d78b8962 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "AVM FRITZ!Box\uff1a{name}", + "flow_title": "AVM FRITZ!SmartHome\uff1a{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/goalzero/translations/no.json b/homeassistant/components/goalzero/translations/no.json index 4ae6f564a99..4dfeadfcf6d 100644 --- a/homeassistant/components/goalzero/translations/no.json +++ b/homeassistant/components/goalzero/translations/no.json @@ -14,7 +14,7 @@ "host": "Vert", "name": "Navn" }, - "description": "F\u00f8rst m\u00e5 du laste ned appen Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n F\u00f8lg instruksjonene for \u00e5 koble Yeti til Wifi-nettverket. S\u00e5 f\u00e5 verts-IP-en fra ruteren din. DHCP m\u00e5 v\u00e6re satt opp i ruteren innstillinger for enheten for \u00e5 sikre at verts-IP ikke endres. Se ruteren din.", + "description": "F\u00f8rst m\u00e5 du laste ned appen Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n F\u00f8lg instruksjonene for \u00e5 koble Yeti til Wifi-nettverket. DHCP-reservasjon m\u00e5 v\u00e6re satt opp i ruteren innstillinger for enheten for \u00e5 sikre at verts-IP ikke endres. Se i brukerh\u00e5ndboken til ruteren.", "title": "" } } diff --git a/homeassistant/components/google_travel_time/translations/cs.json b/homeassistant/components/google_travel_time/translations/cs.json new file mode 100644 index 00000000000..a6c3b361960 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "name": "Jm\u00e9no" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "time": "\u010cas", + "units": "Jednotky" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/no.json b/homeassistant/components/google_travel_time/translations/no.json index 5dfe345af01..f8df651a6fc 100644 --- a/homeassistant/components/google_travel_time/translations/no.json +++ b/homeassistant/components/google_travel_time/translations/no.json @@ -11,6 +11,7 @@ "data": { "api_key": "API-n\u00f8kkel", "destination": "Destinasjon", + "name": "Navn", "origin": "Opprinnelse" }, "description": "N\u00e5r du spesifiserer opprinnelse og destinasjon, kan du oppgi en eller flere steder atskilt med r\u00f8rtegnet, i form av en adresse, breddegrad / lengdegradskoordinat eller en Google-sted-ID. N\u00e5r du spesifiserer stedet ved hjelp av en Google-sted-ID, m\u00e5 ID-en v\u00e6re foran \"place_id:`." diff --git a/homeassistant/components/motioneye/translations/cs.json b/homeassistant/components/motioneye/translations/cs.json new file mode 100644 index 00000000000..311a1d4d965 --- /dev/null +++ b/homeassistant/components/motioneye/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_url": "Neplatn\u00e1 URL adresa", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/cs.json b/homeassistant/components/mutesync/translations/cs.json new file mode 100644 index 00000000000..246a84fa62f --- /dev/null +++ b/homeassistant/components/mutesync/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/ca.json b/homeassistant/components/myq/translations/ca.json index 2b6549586a4..1c61ce60154 100644 --- a/homeassistant/components/myq/translations/ca.json +++ b/homeassistant/components/myq/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +10,13 @@ "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La contrasenya de {username} ja no \u00e9s v\u00e0lida.", + "title": "Torna a autenticar el compte MyQ" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/myq/translations/cs.json b/homeassistant/components/myq/translations/cs.json index 7c0c1ae503c..c13753adccd 100644 --- a/homeassistant/components/myq/translations/cs.json +++ b/homeassistant/components/myq/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Slu\u017eba je ji\u017e nastavena" + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", @@ -9,6 +10,11 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/myq/translations/et.json b/homeassistant/components/myq/translations/et.json index b51044a9e6e..c251d10177e 100644 --- a/homeassistant/components/myq/translations/et.json +++ b/homeassistant/components/myq/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Teenus on juba seadistatud" + "already_configured": "Teenus on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti", @@ -9,6 +10,13 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5ma" + }, + "description": "Kasutaja {username} salas\u00f5na ei kehti enam.", + "title": "Taastuvasta oma MyQ konto" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/myq/translations/nl.json b/homeassistant/components/myq/translations/nl.json index 65df320a544..09a36665414 100644 --- a/homeassistant/components/myq/translations/nl.json +++ b/homeassistant/components/myq/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Service is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,13 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Het wachtwoord voor {username} is niet meer geldig.", + "title": "Verifieer uw MyQ account opnieuw" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/myq/translations/no.json b/homeassistant/components/myq/translations/no.json index c639088917a..b43115f6b93 100644 --- a/homeassistant/components/myq/translations/no.json +++ b/homeassistant/components/myq/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert" + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -9,6 +10,13 @@ "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Passordet for {username} er ikke lenger gyldig.", + "title": "Godkjenn MyQ-kontoen din p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/myq/translations/ru.json b/homeassistant/components/myq/translations/ru.json index c88db7d6960..d9a8e156ce0 100644 --- a/homeassistant/components/myq/translations/ru.json +++ b/homeassistant/components/myq/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -9,6 +10,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {username} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f MyQ" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/myq/translations/zh-Hant.json b/homeassistant/components/myq/translations/zh-Hant.json index d50ff1810e3..fca168fdafe 100644 --- a/homeassistant/components/myq/translations/zh-Hant.json +++ b/homeassistant/components/myq/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,6 +10,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "{username} \u5bc6\u78bc\u4e0d\u518d\u6709\u6548\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49 MyQ \u5e33\u865f" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/omnilogic/translations/no.json b/homeassistant/components/omnilogic/translations/no.json index 96c072082e1..15b44be91a8 100644 --- a/homeassistant/components/omnilogic/translations/no.json +++ b/homeassistant/components/omnilogic/translations/no.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH-forskyvning (positiv eller negativ)", "polling_interval": "Avstemningsintervall (i sekunder)" } } diff --git a/homeassistant/components/picnic/translations/cs.json b/homeassistant/components/picnic/translations/cs.json new file mode 100644 index 00000000000..dc27752e935 --- /dev/null +++ b/homeassistant/components/picnic/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/cs.json b/homeassistant/components/smarttub/translations/cs.json index 6be2df92286..383b69460e1 100644 --- a/homeassistant/components/smarttub/translations/cs.json +++ b/homeassistant/components/smarttub/translations/cs.json @@ -9,6 +9,9 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "email": "E-mail", diff --git a/homeassistant/components/waze_travel_time/translations/cs.json b/homeassistant/components/waze_travel_time/translations/cs.json index 3f6b731b9bf..35932c10fd4 100644 --- a/homeassistant/components/waze_travel_time/translations/cs.json +++ b/homeassistant/components/waze_travel_time/translations/cs.json @@ -5,6 +5,13 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "name": "Jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/no.json b/homeassistant/components/waze_travel_time/translations/no.json index 7ae2bf8d418..c9baef06743 100644 --- a/homeassistant/components/waze_travel_time/translations/no.json +++ b/homeassistant/components/waze_travel_time/translations/no.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Destinasjon", + "name": "Navn", "origin": "Opprinnelse", "region": "Region" }, diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 3917ebd103b..bbf7ae8d229 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Kode kreves for tilkobling", + "alarm_failed_tries": "Antall p\u00e5f\u00f8lgende mislykkede kodeoppf\u00f8ringer for \u00e5 utl\u00f8se en alarm", + "alarm_master_code": "Hovedkode for alarmens kontrollpanel(er)", + "title": "Alternativer for alarmkontrollpanel" + }, + "zha_options": { + "default_light_transition": "Standard lysovergangstid (sekunder)", + "enable_identify_on_join": "Aktiver identifiseringseffekt n\u00e5r enheter blir med i nettverket", + "title": "Globale alternativer" + } + }, "device_automation": { "action_type": { "squawk": "Squawk", From 55c96ae86f7743c9912e0119af3b8ef95566ac2f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 4 May 2021 05:11:21 +0200 Subject: [PATCH 143/852] Create Fritz device and connectivity sensor (#49699) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/fritz/__init__.py | 3 +- .../components/fritz/binary_sensor.py | 87 +++++++++++++++++++ homeassistant/components/fritz/common.py | 73 ++++++++-------- homeassistant/components/fritz/config_flow.py | 6 +- homeassistant/components/fritz/const.py | 2 +- .../components/fritz/device_tracker.py | 9 +- tests/components/fritz/__init__.py | 2 +- 8 files changed, 138 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/fritz/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index deadd4e0b19..d692794c6e4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -330,6 +330,7 @@ omit = homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py homeassistant/components/fritz/__init__.py + homeassistant/components/fritz/binary_sensor.py homeassistant/components/fritz/common.py homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index afa3229c585..5cbaa23c1b5 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -13,7 +13,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.typing import ConfigType from .common import FritzBoxTools, FritzData from .const import DATA_FRITZ, DOMAIN, PLATFORMS @@ -59,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload FRITZ!Box Tools config entry.""" fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] fritzbox.async_unload() diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py new file mode 100644 index 00000000000..493f1bc0d42 --- /dev/null +++ b/homeassistant/components/fritz/binary_sensor.py @@ -0,0 +1,87 @@ +"""AVM FRITZ!Box connectivitiy sensor.""" +import logging + +from fritzconnection.core.exceptions import FritzConnectionException + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .common import FritzBoxBaseEntity, FritzBoxTools +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up entry.""" + _LOGGER.debug("Setting up FRITZ!Box binary sensors") + fritzbox_tools = hass.data[DOMAIN][entry.entry_id] + + if "WANIPConn1" in fritzbox_tools.connection.services: + # Only routers are supported at the moment + async_add_entities( + [FritzBoxConnectivitySensor(fritzbox_tools, entry.title)], True + ) + + +class FritzBoxConnectivitySensor(FritzBoxBaseEntity, BinarySensorEntity): + """Define FRITZ!Box connectivity class.""" + + def __init__(self, fritzbox_tools: FritzBoxTools, device_friendlyname: str) -> None: + """Init FRITZ!Box connectivity class.""" + self._unique_id = f"{fritzbox_tools.unique_id}-connectivity" + self._name = f"{device_friendlyname} Connectivity" + self._is_on = True + self._is_available = True + super().__init__(fritzbox_tools, device_friendlyname) + + @property + def name(self): + """Return name.""" + return self._name + + @property + def device_class(self): + """Return device class.""" + return DEVICE_CLASS_CONNECTIVITY + + @property + def is_on(self) -> bool: + """Return status.""" + return self._is_on + + @property + def unique_id(self): + """Return unique id.""" + return self._unique_id + + @property + def available(self) -> bool: + """Return availability.""" + return self._is_available + + def update(self) -> None: + """Update data.""" + _LOGGER.debug("Updating FRITZ!Box binary sensors") + self._is_on = True + try: + if "WANCommonInterfaceConfig1" in self._fritzbox_tools.connection.services: + link_props = self._fritzbox_tools.connection.call_action( + "WANCommonInterfaceConfig1", "GetCommonLinkProperties" + ) + is_up = link_props["NewPhysicalLinkStatus"] + self._is_on = is_up == "Up" + else: + self._is_on = self._fritzbox_tools.fritzstatus.is_connected + + self._is_available = True + + except FritzConnectionException: + _LOGGER.error("Error getting the state from the FRITZ!Box", exc_info=True) + self._is_available = False diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 6a6f0b4a7d9..3a6bf132fb6 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -12,6 +12,7 @@ from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util @@ -49,7 +50,6 @@ class FritzBoxTools: ): """Initialize FritzboxTools class.""" self._cancel_scan = None - self._device_info = None self._devices: dict[str, Any] = {} self._unique_id = None self.connection = None @@ -60,6 +60,9 @@ class FritzBoxTools: self.password = password self.port = port self.username = username + self.mac = None + self.model = None + self.sw_version = None async def async_setup(self): """Wrap up FritzboxTools class setup.""" @@ -76,12 +79,13 @@ class FritzBoxTools: ) self.fritzstatus = FritzStatus(fc=self.connection) + info = self.connection.call_action("DeviceInfo:1", "GetInfo") if self._unique_id is None: - self._unique_id = self.connection.call_action("DeviceInfo:1", "GetInfo")[ - "NewSerialNumber" - ] + self._unique_id = info["NewSerialNumber"] - self._device_info = self._fetch_device_info() + self.model = info.get("NewModelName") + self.sw_version = info.get("NewSoftwareVersion") + self.mac = self.unique_id async def async_start(self): """Start FritzHosts connection.""" @@ -106,16 +110,6 @@ class FritzBoxTools: """Return unique id.""" return self._unique_id - @property - def fritzbox_model(self): - """Return model.""" - return self._device_info["model"].replace("FRITZ!Box ", "") - - @property - def device_info(self): - """Return device info.""" - return self._device_info - @property def devices(self) -> dict[str, Any]: """Return devices.""" @@ -163,33 +157,13 @@ class FritzBoxTools: if new_device: async_dispatcher_send(self.hass, self.signal_device_new) - def _fetch_device_info(self): - """Fetch device info.""" - info = self.connection.call_action("DeviceInfo:1", "GetInfo") - - dev_info = {} - dev_info["identifiers"] = { - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.unique_id) - } - dev_info["manufacturer"] = "AVM" - - if dev_name := info.get("NewName"): - dev_info["name"] = dev_name - if dev_model := info.get("NewModelName"): - dev_info["model"] = dev_model - if dev_sw_ver := info.get("NewSoftwareVersion"): - dev_info["sw_version"] = dev_sw_ver - - return dev_info - class FritzData: """Storage class for platform global data.""" def __init__(self) -> None: """Initialize the data.""" - self.tracked = {} + self.tracked: dict = {} class FritzDevice: @@ -241,3 +215,30 @@ class FritzDevice: def last_activity(self): """Return device last activity.""" return self._last_activity + + +class FritzBoxBaseEntity: + """Fritz host entity base class.""" + + def __init__(self, fritzbox_tools: FritzBoxTools, device_name: str) -> None: + """Init device info class.""" + self._fritzbox_tools = fritzbox_tools + self._device_name = device_name + + @property + def mac_address(self) -> str: + """Return the mac address of the main device.""" + return self._fritzbox_tools.mac + + @property + def device_info(self): + """Return the device information.""" + + return { + "connections": {(CONNECTION_NETWORK_MAC, self.mac_address)}, + "identifiers": {(DOMAIN, self._fritzbox_tools.unique_id)}, + "name": self._device_name, + "manufacturer": "AVM", + "model": self._fritzbox_tools.model, + "sw_version": self._fritzbox_tools.sw_version, + } diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index a8afff6e41e..23e713f7966 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the FRITZ!Box Tools integration.""" +from __future__ import annotations + import logging from urllib.parse import urlparse @@ -65,7 +67,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return None - async def async_check_configured_entry(self) -> ConfigEntry: + async def async_check_configured_entry(self) -> ConfigEntry | None: """Check if entry is configured.""" for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_HOST] == self._host: @@ -170,7 +172,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._password = user_input[CONF_PASSWORD] if not (error := await self.fritz_tools_init()): - self._name = self.fritz_tools.device_info["model"] + self._name = self.fritz_tools.model if await self.async_check_configured_entry(): error = "already_configured" diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 1a3b176deb7..fff55a276e1 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -2,7 +2,7 @@ DOMAIN = "fritz" -PLATFORMS = ["device_tracker"] +PLATFORMS = ["binary_sensor", "device_tracker"] DATA_FRITZ = "fritz_data" diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 23657429f68..646a8cc986e 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -17,7 +17,6 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from .common import FritzBoxTools @@ -116,7 +115,7 @@ class FritzBoxTracker(ScannerEntity): self._mac = device.mac_address self._name = device.hostname or DEFAULT_DEVICE_NAME self._active = False - self._attrs = {} + self._attrs: dict = {} @property def is_connected(self): @@ -154,7 +153,7 @@ class FritzBoxTracker(ScannerEntity): return SOURCE_TYPE_ROUTER @property - def device_info(self) -> DeviceInfo: + def device_info(self): """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, @@ -162,6 +161,10 @@ class FritzBoxTracker(ScannerEntity): "name": self.name, "manufacturer": "AVM", "model": "FRITZ!Box Tracked device", + "via_device": ( + DOMAIN, + self._router.unique_id, + ), } @property diff --git a/tests/components/fritz/__init__.py b/tests/components/fritz/__init__.py index 5a9b6cb1652..27ec391c092 100644 --- a/tests/components/fritz/__init__.py +++ b/tests/components/fritz/__init__.py @@ -49,7 +49,7 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods "NewBytesReceived": 12045, }, ("DeviceInfo:1", "GetInfo"): { - "NewSerialNumber": 1234, + "NewSerialNumber": "abcdefgh", "NewName": "TheName", "NewModelName": "FRITZ!Box 7490", }, From 809c1394d48b70da80bea2877bb8f28aab26885a Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 3 May 2021 23:19:41 -0700 Subject: [PATCH 144/852] Enable mypy for motionEye (aye aye!) (#49738) --- .../components/motioneye/__init__.py | 12 +++++------ homeassistant/components/motioneye/camera.py | 8 ++++---- .../components/motioneye/config_flow.py | 20 ++++++++++--------- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - tests/common.py | 8 +++++--- tests/components/motioneye/test_camera.py | 5 ++++- .../components/motioneye/test_config_flow.py | 2 +- 8 files changed, 31 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 3d8c775f140..f766bb86be2 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -65,10 +65,10 @@ def get_motioneye_entity_unique_id( def get_camera_from_cameras( - camera_id: int, data: dict[str, Any] + camera_id: int, data: dict[str, Any] | None ) -> dict[str, Any] | None: """Get an individual camera dict from a multiple cameras data response.""" - for camera in data.get(KEY_CAMERAS) or []: + for camera in data.get(KEY_CAMERAS, []) if data else []: if camera.get(KEY_ID) == camera_id: val: dict[str, Any] = camera return val @@ -105,7 +105,7 @@ def _add_camera( entry: ConfigEntry, camera_id: int, camera: dict[str, Any], - device_identifier: tuple[str, str, int], + device_identifier: tuple[str, str], ) -> None: """Add a motionEye camera to hass.""" @@ -164,14 +164,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CONF_COORDINATOR: coordinator, } - current_cameras: set[tuple[str, str, int]] = set() + current_cameras: set[tuple[str, str]] = set() device_registry = await dr.async_get_registry(hass) @callback def _async_process_motioneye_cameras() -> None: """Process motionEye camera additions and removals.""" - inbound_camera: set[tuple[str, str, int]] = set() - if KEY_CAMERAS not in coordinator.data: + inbound_camera: set[tuple[str, str]] = set() + if coordinator.data is None or KEY_CAMERAS not in coordinator.data: return for camera in coordinator.data[KEY_CAMERAS]: diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index 80c51753858..e3cad73dfc5 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, Dict, Optional import aiohttp from motioneye_client.client import MotionEyeClient @@ -86,7 +86,7 @@ async def async_setup_entry( listen_for_new_cameras(hass, entry, camera_add) -class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity): +class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity[Optional[Dict[str, Any]]]): """motionEye mjpeg camera.""" def __init__( @@ -96,7 +96,7 @@ class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity): password: str, camera: dict[str, Any], client: MotionEyeClient, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[str, Any] | None], ) -> None: """Initialize a MJPEG camera.""" self._surveillance_username = username @@ -191,7 +191,7 @@ class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity): self._motion_detection_enabled = camera.get(KEY_MOTION_DETECTION, False) available = True self._available = available - CoordinatorEntity._handle_coordinator_update(self) + super()._handle_coordinator_update() @property def brand(self) -> str: diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 76fb766a67a..acba3e16bb2 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, Dict, cast from motioneye_client.client import ( MotionEyeClientConnectionError, @@ -13,8 +13,8 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow from homeassistant.const import CONF_SOURCE, CONF_URL +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType from . import create_motioneye_client from .const import ( @@ -34,13 +34,13 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 async def async_step_user( - self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" def _get_form( - user_input: ConfigType, errors: dict[str, str] | None = None - ) -> dict[str, Any]: + user_input: dict[str, Any], errors: dict[str, str] | None = None + ) -> FlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -77,7 +77,9 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): ) if user_input is None: - return _get_form(reauth_entry.data if reauth_entry else {}) + return _get_form( + cast(Dict[str, Any], reauth_entry.data) if reauth_entry else {} + ) try: # Cannot use cv.url validation in the schema itself, so @@ -130,7 +132,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth( self, - config_data: ConfigType | None = None, - ) -> dict[str, Any]: + config_data: dict[str, Any] | None = None, + ) -> FlowResult: """Handle a reauthentication flow.""" return await self.async_step_user(config_data) diff --git a/mypy.ini b/mypy.ini index 42e5c0101f2..68afb4d9786 100644 --- a/mypy.ini +++ b/mypy.ini @@ -996,9 +996,6 @@ ignore_errors = true [mypy-homeassistant.components.motion_blinds.*] ignore_errors = true -[mypy-homeassistant.components.motioneye.*] -ignore_errors = true - [mypy-homeassistant.components.mqtt.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a840723b20e..91752c0acdf 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -135,7 +135,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.minecraft_server.*", "homeassistant.components.mobile_app.*", "homeassistant.components.motion_blinds.*", - "homeassistant.components.motioneye.*", "homeassistant.components.mqtt.*", "homeassistant.components.mullvad.*", "homeassistant.components.mysensors.*", diff --git a/tests/common.py b/tests/common.py index ee969f7ca11..e3a4a714edf 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,7 +5,7 @@ import asyncio import collections from collections import OrderedDict from contextlib import contextmanager -from datetime import timedelta +from datetime import datetime, timedelta import functools as ft from io import StringIO import json @@ -44,7 +44,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import BLOCK_LOG_TIMEOUT, State +from homeassistant.core import BLOCK_LOG_TIMEOUT, HomeAssistant, State from homeassistant.helpers import ( area_registry, device_registry, @@ -361,7 +361,9 @@ fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) @ha.callback -def async_fire_time_changed(hass, datetime_, fire_all=False): +def async_fire_time_changed( + hass: HomeAssistant, datetime_: datetime, fire_all: bool = False +) -> None: """Fire a time changes event.""" hass.bus.async_fire(EVENT_TIME_CHANGED, {"now": date_util.as_utc(datetime_)}) diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 921dc9df920..f1ddcea4386 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -4,7 +4,7 @@ import logging from typing import Any from unittest.mock import AsyncMock, Mock -from aiohttp import web # type: ignore +from aiohttp import web from aiohttp.web_exceptions import HTTPBadGateway from motioneye_client.client import ( MotionEyeClientError, @@ -165,6 +165,7 @@ async def test_setup_camera_new_data_error(hass: HomeAssistant) -> None: async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state assert entity_state.state == "unavailable" @@ -173,6 +174,7 @@ async def test_setup_camera_new_data_without_streaming(hass: HomeAssistant) -> N client = create_mock_motioneye_client() await setup_mock_motioneye_config_entry(hass, client=client) entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state assert entity_state.state == "idle" cameras = copy.deepcopy(TEST_CAMERAS) @@ -181,6 +183,7 @@ async def test_setup_camera_new_data_without_streaming(hass: HomeAssistant) -> N async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state assert entity_state.state == "unavailable" diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index d8700e162c4..f193c79f3ef 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -235,7 +235,7 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" - assert config_entry.data == new_data + assert dict(config_entry.data) == new_data assert len(mock_setup_entry.mock_calls) == 1 assert mock_client.async_client_close.called From 2eae87fb1b64b3e8ae7f5c78094d46ca1e01acf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 4 May 2021 08:28:45 +0200 Subject: [PATCH 145/852] Add SyncThru binary sensors (#48114) Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + homeassistant/components/syncthru/__init__.py | 53 ++++-- .../components/syncthru/binary_sensor.py | 110 ++++++++++++ .../components/syncthru/manifest.json | 2 +- homeassistant/components/syncthru/sensor.py | 170 ++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 247 insertions(+), 94 deletions(-) create mode 100644 homeassistant/components/syncthru/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index d692794c6e4..6c210d986bc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -967,6 +967,8 @@ omit = homeassistant/components/switchbot/switch.py homeassistant/components/switcher_kis/switch.py homeassistant/components/switchmate/switch.py + homeassistant/components/syncthru/__init__.py + homeassistant/components/syncthru/binary_sensor.py homeassistant/components/syncthru/sensor.py homeassistant/components/synology_chat/notify.py homeassistant/components/synology_dsm/__init__.py diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index a5dbd2a9a35..5c28e6f029a 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -1,22 +1,26 @@ """The syncthru component.""" from __future__ import annotations +from datetime import timedelta import logging +import async_timeout from pysyncthru import SyncThru +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry as dr +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [SENSOR_DOMAIN] +PLATFORMS = [BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -24,21 +28,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = aiohttp_client.async_get_clientsession(hass) hass.data.setdefault(DOMAIN, {}) - printer = hass.data[DOMAIN][entry.entry_id] = SyncThru( - entry.data[CONF_URL], session - ) + printer = SyncThru(entry.data[CONF_URL], session) - try: - await printer.update() - except ValueError: - _LOGGER.error( - "Device at %s not appear to be a SyncThru printer, aborting setup", - printer.url, - ) - return False - else: - if printer.is_unknown_state(): - raise ConfigEntryNotReady + async def async_update_data() -> SyncThru: + """Fetch data from the printer.""" + try: + async with async_timeout.timeout(10): + await printer.update() + except ValueError as value_error: + # if an exception is thrown, printer does not support syncthru + raise UpdateFailed( + f"Configured printer at {printer.url} does not respond. " + "Please make sure it supports SyncThru and check your configuration." + ) from value_error + else: + if printer.is_unknown_state(): + raise ConfigEntryNotReady + return printer + + coordinator: DataUpdateCoordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update_data, + update_interval=timedelta(seconds=30), + ) + hass.data[DOMAIN][entry.entry_id] = coordinator + await coordinator.async_config_entry_first_refresh() device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( @@ -60,9 +76,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -def device_identifiers(printer: SyncThru) -> set[tuple[str, ...]]: +def device_identifiers(printer: SyncThru) -> set[tuple[str, ...]] | None: """Get device identifiers for device registry.""" - return {(DOMAIN, printer.serial_number())} + serial = printer.serial_number() + if serial is None: + return None + return {(DOMAIN, serial)} def device_connections(printer: SyncThru) -> set[tuple[str, str]]: diff --git a/homeassistant/components/syncthru/binary_sensor.py b/homeassistant/components/syncthru/binary_sensor.py new file mode 100644 index 00000000000..66bf76b31a5 --- /dev/null +++ b/homeassistant/components/syncthru/binary_sensor.py @@ -0,0 +1,110 @@ +"""Support for Samsung Printers with SyncThru web interface.""" + +import logging + +from pysyncthru import SyncThru, SyncthruState + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) +from homeassistant.const import CONF_NAME +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import device_identifiers +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SYNCTHRU_STATE_PROBLEM = { + SyncthruState.INVALID: True, + SyncthruState.OFFLINE: None, + SyncthruState.NORMAL: False, + SyncthruState.UNKNOWN: True, + SyncthruState.WARNING: True, + SyncthruState.TESTING: False, + SyncthruState.ERROR: True, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up from config entry.""" + + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + name = config_entry.data[CONF_NAME] + entities = [ + SyncThruOnlineSensor(coordinator, name), + SyncThruProblemSensor(coordinator, name), + ] + + async_add_entities(entities) + + +class SyncThruBinarySensor(CoordinatorEntity, BinarySensorEntity): + """Implementation of an abstract Samsung Printer binary sensor platform.""" + + def __init__(self, coordinator, name): + """Initialize the sensor.""" + super().__init__(coordinator) + self.syncthru: SyncThru = coordinator.data + self._name = name + self._id_suffix = "" + + @property + def unique_id(self): + """Return unique ID for the sensor.""" + serial = self.syncthru.serial_number() + return f"{serial}{self._id_suffix}" if serial else None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_info(self): + """Return device information.""" + return {"identifiers": device_identifiers(self.syncthru)} + + +class SyncThruOnlineSensor(SyncThruBinarySensor): + """Implementation of a sensor that checks whether is turned on/online.""" + + def __init__(self, syncthru, name): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._id_suffix = "_online" + + @property + def device_class(self): + """Class of the sensor.""" + return DEVICE_CLASS_CONNECTIVITY + + @property + def is_on(self): + """Set the state to whether the printer is online.""" + return self.syncthru.is_online() + + +class SyncThruProblemSensor(SyncThruBinarySensor): + """Implementation of a sensor that checks whether the printer works correctly.""" + + def __init__(self, syncthru, name): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._id_suffix = "_problem" + + @property + def device_class(self): + """Class of the sensor.""" + return DEVICE_CLASS_PROBLEM + + @property + def is_on(self): + """Set the state to whether there is a problem with the printer.""" + return SYNCTHRU_STATE_PROBLEM[self.syncthru.device_status()] diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index e84a52b514e..9fd3c2afe06 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -3,7 +3,7 @@ "name": "Samsung SyncThru Printer", "documentation": "https://www.home-assistant.io/integrations/syncthru", "config_flow": true, - "requirements": ["pysyncthru==0.7.0", "url-normalize==1.4.1"], + "requirements": ["pysyncthru==0.7.3", "url-normalize==1.4.1"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:Printer:1", diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 8277bd69467..2b559e0a15f 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -2,13 +2,17 @@ import logging -from pysyncthru import SYNCTHRU_STATE_HUMAN, SyncThru +from pysyncthru import SyncThru, SyncthruState import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_RESOURCE, CONF_URL, PERCENTAGE import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import device_identifiers from .const import DEFAULT_MODEL, DEFAULT_NAME_TEMPLATE, DOMAIN @@ -26,6 +30,16 @@ DEFAULT_MONITORED_CONDITIONS.extend([f"drum_{key}" for key in DRUM_COLORS]) DEFAULT_MONITORED_CONDITIONS.extend([f"tray_{key}" for key in TRAYS]) DEFAULT_MONITORED_CONDITIONS.extend([f"output_tray_{key}" for key in OUTPUT_TRAYS]) +SYNCTHRU_STATE_HUMAN = { + SyncthruState.INVALID: "invalid", + SyncthruState.OFFLINE: "unreachable", + SyncthruState.NORMAL: "normal", + SyncthruState.UNKNOWN: "unknown", + SyncthruState.WARNING: "warning", + SyncthruState.TESTING: "testing", + SyncthruState.ERROR: "error", +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, @@ -58,7 +72,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up from config entry.""" - printer = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + printer: SyncThru = coordinator.data supp_toner = printer.toner_status(filter_supported=True) supp_drum = printer.drum_status(filter_supported=True) @@ -66,28 +81,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities): supp_output_tray = printer.output_tray_status() name = config_entry.data[CONF_NAME] - devices = [SyncThruMainSensor(printer, name)] + entities = [SyncThruMainSensor(coordinator, name)] for key in supp_toner: - devices.append(SyncThruTonerSensor(printer, name, key)) + entities.append(SyncThruTonerSensor(coordinator, name, key)) for key in supp_drum: - devices.append(SyncThruDrumSensor(printer, name, key)) + entities.append(SyncThruDrumSensor(coordinator, name, key)) for key in supp_tray: - devices.append(SyncThruInputTraySensor(printer, name, key)) + entities.append(SyncThruInputTraySensor(coordinator, name, key)) for key in supp_output_tray: - devices.append(SyncThruOutputTraySensor(printer, name, key)) + entities.append(SyncThruOutputTraySensor(coordinator, name, key)) - async_add_entities(devices, True) + async_add_entities(entities) -class SyncThruSensor(SensorEntity): +class SyncThruSensor(CoordinatorEntity, SensorEntity): """Implementation of an abstract Samsung Printer sensor platform.""" - def __init__(self, syncthru, name): + def __init__(self, coordinator, name): """Initialize the sensor.""" - self.syncthru: SyncThru = syncthru - self._attributes = {} - self._state = None + super().__init__(coordinator) + self.syncthru: SyncThru = coordinator.data self._name = name self._icon = "mdi:printer" self._unit_of_measurement = None @@ -97,18 +111,13 @@ class SyncThruSensor(SensorEntity): def unique_id(self): """Return unique ID for the sensor.""" serial = self.syncthru.serial_number() - return serial + self._id_suffix if serial else super().unique_id + return f"{serial}{self._id_suffix}" if serial else None @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 - @property def icon(self): """Return the icon of the device.""" @@ -119,11 +128,6 @@ class SyncThruSensor(SensorEntity): """Return the unit of measuremnt.""" return self._unit_of_measurement - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return self._attributes - @property def device_info(self): """Return device information.""" @@ -131,50 +135,56 @@ class SyncThruSensor(SensorEntity): class SyncThruMainSensor(SyncThruSensor): - """Implementation of the main sensor, conducting the actual polling.""" + """ + Implementation of the main sensor, conducting the actual polling. - def __init__(self, syncthru, name): + It also shows the detailed state and presents + the displayed current status message. + """ + + def __init__(self, coordinator, name): """Initialize the sensor.""" - super().__init__(syncthru, name) + super().__init__(coordinator, name) self._id_suffix = "_main" - self._active = True - async def async_update(self): - """Get the latest data from SyncThru and update the state.""" - if not self._active: - return - try: - await self.syncthru.update() - except ValueError: - # if an exception is thrown, printer does not support syncthru - _LOGGER.warning( - "Configured printer at %s does not support SyncThru. " - "Consider changing your configuration", - self.syncthru.url, - ) - self._active = False - self._state = SYNCTHRU_STATE_HUMAN[self.syncthru.device_status()] - self._attributes = {"display_text": self.syncthru.device_status_details()} + @property + def state(self): + """Set state to human readable version of syncthru status.""" + return SYNCTHRU_STATE_HUMAN[self.syncthru.device_status()] + + @property + def extra_state_attributes(self): + """Show current printer display text.""" + return { + "display_text": self.syncthru.device_status_details(), + } + + @property + def entity_registry_enabled_default(self) -> bool: + """Disable entity by default.""" + return False class SyncThruTonerSensor(SyncThruSensor): """Implementation of a Samsung Printer toner sensor platform.""" - def __init__(self, syncthru, name, color): + def __init__(self, coordinator, name, color): """Initialize the sensor.""" - super().__init__(syncthru, name) + super().__init__(coordinator, name) self._name = f"{name} Toner {color}" self._color = color self._unit_of_measurement = PERCENTAGE self._id_suffix = f"_toner_{color}" - def update(self): - """Get the latest data from SyncThru and update the state.""" - # Data fetching is taken care of through the Main sensor + @property + def extra_state_attributes(self): + """Show all data returned for this toner.""" + return self.syncthru.toner_status().get(self._color, {}) - if self.syncthru.is_online(): - self._attributes = self.syncthru.toner_status().get(self._color, {}) - self._state = self._attributes.get("remaining") + @property + def state(self): + """Show amount of remaining toner.""" + return self.syncthru.toner_status().get(self._color, {}).get("remaining") class SyncThruDrumSensor(SyncThruSensor): @@ -188,13 +198,15 @@ class SyncThruDrumSensor(SyncThruSensor): self._unit_of_measurement = PERCENTAGE self._id_suffix = f"_drum_{color}" - def update(self): - """Get the latest data from SyncThru and update the state.""" - # Data fetching is taken care of through the Main sensor + @property + def extra_state_attributes(self): + """Show all data returned for this drum.""" + return self.syncthru.drum_status().get(self._color, {}) - if self.syncthru.is_online(): - self._attributes = self.syncthru.drum_status().get(self._color, {}) - self._state = self._attributes.get("remaining") + @property + def state(self): + """Show amount of remaining drum.""" + return self.syncthru.drum_status().get(self._color, {}).get("remaining") class SyncThruInputTraySensor(SyncThruSensor): @@ -207,15 +219,20 @@ class SyncThruInputTraySensor(SyncThruSensor): self._number = number self._id_suffix = f"_tray_{number}" - def update(self): - """Get the latest data from SyncThru and update the state.""" - # Data fetching is taken care of through the Main sensor + @property + def extra_state_attributes(self): + """Show all data returned for this input tray.""" + return self.syncthru.input_tray_status().get(self._number, {}) - if self.syncthru.is_online(): - self._attributes = self.syncthru.input_tray_status().get(self._number, {}) - self._state = self._attributes.get("newError") - if self._state == "": - self._state = "Ready" + @property + def state(self): + """Display ready unless there is some error, then display error.""" + tray_state = ( + self.syncthru.input_tray_status().get(self._number, {}).get("newError") + ) + if tray_state == "": + tray_state = "Ready" + return tray_state class SyncThruOutputTraySensor(SyncThruSensor): @@ -228,12 +245,17 @@ class SyncThruOutputTraySensor(SyncThruSensor): self._number = number self._id_suffix = f"_output_tray_{number}" - def update(self): - """Get the latest data from SyncThru and update the state.""" - # Data fetching is taken care of through the Main sensor + @property + def extra_state_attributes(self): + """Show all data returned for this output tray.""" + return self.syncthru.output_tray_status().get(self._number, {}) - if self.syncthru.is_online(): - self._attributes = self.syncthru.output_tray_status().get(self._number, {}) - self._state = self._attributes.get("status") - if self._state == "": - self._state = "Ready" + @property + def state(self): + """Display ready unless there is some error, then display error.""" + tray_state = ( + self.syncthru.output_tray_status().get(self._number, {}).get("status") + ) + if tray_state == "": + tray_state = "Ready" + return tray_state diff --git a/requirements_all.txt b/requirements_all.txt index 75c409cb11b..c377e04a40b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1756,7 +1756,7 @@ pystiebeleltron==0.0.1.dev2 pysuez==0.1.19 # homeassistant.components.syncthru -pysyncthru==0.7.0 +pysyncthru==0.7.3 # homeassistant.components.tankerkoenig pytankerkoenig==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7989e6cfbf9..3a879156951 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -968,7 +968,7 @@ pyspcwebgw==0.4.0 pysqueezebox==0.5.5 # homeassistant.components.syncthru -pysyncthru==0.7.0 +pysyncthru==0.7.3 # homeassistant.components.ecobee python-ecobee-api==0.2.11 From 016a4433d24caeb971bb66b22e1a105311e736a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 May 2021 20:54:31 -1000 Subject: [PATCH 146/852] Handle missing transport_state on media update in sonos (#50051) --- homeassistant/components/sonos/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index c9e6612fc06..583291ce203 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -488,7 +488,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Update information about currently playing media.""" variables = event and event.variables - if variables: + if variables and "transport_state" in variables: + # If the transport has an error then transport_state will + # not be set new_status = variables["transport_state"] else: transport_info = self.soco.get_current_transport_info() From d579e3427f795c423bd2491dbf53a81635bf48fe Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 4 May 2021 08:54:45 +0200 Subject: [PATCH 147/852] Use last_step marker on UniFi options flow (#50053) --- homeassistant/components/unifi/config_flow.py | 4 ++++ tests/components/unifi/test_config_flow.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index a64c6fcef5f..8f90e13c9fa 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -299,6 +299,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): ): cv.multi_select(clients_to_block), } ), + last_step=True, ) async def async_step_device_tracker(self, user_input=None): @@ -354,6 +355,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): ): bool, } ), + last_step=False, ) async def async_step_client_control(self, user_input=None): @@ -391,6 +393,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): } ), errors=errors, + last_step=False, ) async def async_step_statistics_sensors(self, user_input=None): @@ -413,6 +416,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): ): bool, } ), + last_step=True, ) async def _update_options(self): diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 1967369e22b..8f88b6adb4c 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -448,6 +448,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "device_tracker" + assert not result["last_step"] assert set( result["data_schema"].schema[CONF_SSID_FILTER].options.keys() ).intersection(("SSID 1", "SSID 2", "SSID 2_IOT", "SSID 3")) @@ -465,6 +466,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "client_control" + assert not result["last_step"] result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -477,6 +479,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "statistics_sensors" + assert result["last_step"] result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -519,6 +522,7 @@ async def test_simple_option_flow(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "simple_options" + assert result["last_step"] result = await hass.config_entries.options.async_configure( result["flow_id"], From 693147868835b914e3b0e8b43cfe52238351280d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 May 2021 10:57:44 +0200 Subject: [PATCH 148/852] Bump codecov/codecov-action from v1.4.1 to v1.5.0 (#50061) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.4.1 to v1.5.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1.4.1...a1ed4b322b4b38cb846afb5a0ebfa17086917d27) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ae341f9aff1..50b346b7843 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -740,4 +740,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.4.1 + uses: codecov/codecov-action@v1.5.0 From c063f14c24cce39feb4b2265a4ef61098844a682 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Tue, 4 May 2021 13:49:16 +0200 Subject: [PATCH 149/852] Add configuration flow for Buienradar integration (#37796) * Add configuration flow for Buienradar integration * Update buienradar camera tests to work with config flow * Update buienradar weather tests to work with config flow * Update buienradar sensor tests to work with config flow * Remove buienradar config_flow tests to pass tests * Add config flow tests for buienradar integration * Increase test coverage for buienradar config_flow tests * Move data into domain * Remove forecast option * Move data to options * Remove options from config flow * Adjust tests * Adjust string * Fix pylint issues * Rework review comments * Handle import * Change config flow to setup camera or weather * Fix tests * Remove translated file * Fix pylint * Fix flake8 * Fix unload * Minor name changes * Update homeassistant/components/buienradar/config_flow.py Co-authored-by: Ties de Kock * Remove asynctest * Add translation * Disable sensors by default * Remove integration name from translations * Remove import method * Drop selection between platforms, disable camera by default * Minor fix in configured_instances * Bugfix in weather * Rework import * Change unique ids of camera * Fix in import * Fix camera tests * Fix sensor test * Fix sensor test 2 * Fix config flow tests * Add option flow * Add tests for option flow * Add import tests * Some cleanups * Apply suggestions from code review Apply code suggestions Co-authored-by: Franck Nijhof * Fix isort,black,mypy * Small tweaks and added typing to new parts * Fix review comments (1) * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Fix review comments (2) * Fix issues * Fix unique id * Improve tests * Extend tests * Fix issue with unload * Address review comments * Add warning when loading platform * Add load/unload test Co-authored-by: Ties de Kock Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- CODEOWNERS | 2 +- .../components/buienradar/__init__.py | 142 +++++++++++++- homeassistant/components/buienradar/camera.py | 57 ++++-- .../components/buienradar/config_flow.py | 129 +++++++++++++ homeassistant/components/buienradar/const.py | 18 ++ .../components/buienradar/manifest.json | 3 +- homeassistant/components/buienradar/sensor.py | 46 +++-- .../components/buienradar/strings.json | 29 +++ .../buienradar/translations/en.json | 29 +++ .../components/buienradar/weather.py | 71 ++++--- homeassistant/generated/config_flows.py | 1 + tests/components/buienradar/test_camera.py | 176 +++++++++--------- .../components/buienradar/test_config_flow.py | 131 +++++++++++++ tests/components/buienradar/test_init.py | 120 ++++++++++++ tests/components/buienradar/test_sensor.py | 35 ++-- tests/components/buienradar/test_weather.py | 27 ++- 16 files changed, 840 insertions(+), 176 deletions(-) create mode 100644 homeassistant/components/buienradar/config_flow.py create mode 100644 homeassistant/components/buienradar/strings.json create mode 100644 homeassistant/components/buienradar/translations/en.json create mode 100644 tests/components/buienradar/test_config_flow.py create mode 100644 tests/components/buienradar/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 241b9c8ce5d..f69e9b3ecfe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -76,7 +76,7 @@ homeassistant/components/brother/* @bieniu homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bsblan/* @liudger homeassistant/components/bt_smarthub/* @jxwolstenholme -homeassistant/components/buienradar/* @mjj4791 @ties +homeassistant/components/buienradar/* @mjj4791 @ties @Robbie1221 homeassistant/components/cast/* @emontnemery homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren homeassistant/components/circuit/* @braam diff --git a/homeassistant/components/buienradar/__init__.py b/homeassistant/components/buienradar/__init__.py index 680351f9b81..0474876bf2f 100644 --- a/homeassistant/components/buienradar/__init__.py +++ b/homeassistant/components/buienradar/__init__.py @@ -1 +1,141 @@ -"""The buienradar component.""" +"""The buienradar integration.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.helpers.typing import ConfigType + +from .const import ( + CONF_COUNTRY, + CONF_DELTA, + CONF_DIMENSION, + CONF_TIMEFRAME, + DEFAULT_COUNTRY, + DEFAULT_DELTA, + DEFAULT_DIMENSION, + DEFAULT_TIMEFRAME, + DOMAIN, +) + +PLATFORMS = ["camera", "sensor", "weather"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the buienradar component.""" + hass.data.setdefault(DOMAIN, {}) + + weather_configs = _filter_domain_configs(config, "weather", DOMAIN) + sensor_configs = _filter_domain_configs(config, "sensor", DOMAIN) + camera_configs = _filter_domain_configs(config, "camera", DOMAIN) + + _import_configs(hass, weather_configs, sensor_configs, camera_configs) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up buienradar from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_update_options)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + return unload_ok + + +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +def _import_configs( + hass: HomeAssistant, + weather_configs: list[ConfigType], + sensor_configs: list[ConfigType], + camera_configs: list[ConfigType], +) -> None: + camera_config = {} + if camera_configs: + camera_config = camera_configs[0] + + for config in sensor_configs: + # Remove weather configurations which share lat/lon with sensor configurations + matching_weather_config = None + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + for weather_config in weather_configs: + weather_latitude = config.get(CONF_LATITUDE, hass.config.latitude) + weather_longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + if latitude == weather_latitude and longitude == weather_longitude: + matching_weather_config = weather_config + break + + if matching_weather_config is not None: + weather_configs.remove(matching_weather_config) + + configs = weather_configs + sensor_configs + + if not configs and camera_configs: + config = { + CONF_LATITUDE: hass.config.latitude, + CONF_LONGITUDE: hass.config.longitude, + } + configs.append(config) + + if configs: + _try_update_unique_id(hass, configs[0], camera_config) + + for config in configs: + data = { + CONF_LATITUDE: config.get(CONF_LATITUDE, hass.config.latitude), + CONF_LONGITUDE: config.get(CONF_LONGITUDE, hass.config.longitude), + CONF_TIMEFRAME: config.get(CONF_TIMEFRAME, DEFAULT_TIMEFRAME), + CONF_COUNTRY: camera_config.get(CONF_COUNTRY, DEFAULT_COUNTRY), + CONF_DELTA: camera_config.get(CONF_DELTA, DEFAULT_DELTA), + CONF_NAME: config.get(CONF_NAME, "Buienradar"), + } + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=data, + ) + ) + + +def _try_update_unique_id( + hass: HomeAssistant, config: ConfigType, camera_config: ConfigType +) -> None: + dimension = camera_config.get(CONF_DIMENSION, DEFAULT_DIMENSION) + country = camera_config.get(CONF_COUNTRY, DEFAULT_COUNTRY) + + registry = entity_registry.async_get(hass) + entity_id = registry.async_get_entity_id("camera", DOMAIN, f"{dimension}_{country}") + + if entity_id is not None: + latitude = config[CONF_LATITUDE] + longitude = config[CONF_LONGITUDE] + + new_unique_id = f"{latitude:2.6f}{longitude:2.6f}" + registry.async_update_entity(entity_id, new_unique_id=new_unique_id) + + +def _filter_domain_configs( + config: ConfigType, domain: str, platform: str +) -> list[ConfigType]: + configs = [] + for entry in config: + if entry.startswith(domain): + configs += [x for x in config[entry] if x["platform"] == platform] + return configs diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 92f25b7ffc6..1a2d6d4d0be 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -9,14 +9,22 @@ import aiohttp import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util -CONF_DIMENSION = "dimension" -CONF_DELTA = "delta" -CONF_COUNTRY = "country_code" +from .const import ( + CONF_COUNTRY, + CONF_DELTA, + CONF_DIMENSION, + DEFAULT_COUNTRY, + DEFAULT_DELTA, + DEFAULT_DIMENSION, +) _LOGGER = logging.getLogger(__name__) @@ -41,13 +49,27 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up buienradar radar-loop camera component.""" - dimension = config[CONF_DIMENSION] - delta = config[CONF_DELTA] - name = config[CONF_NAME] - country = config[CONF_COUNTRY] + """Set up buienradar camera platform.""" + _LOGGER.warning( + "Platform configuration is deprecated, will be removed in a future release" + ) - async_add_entities([BuienradarCam(name, dimension, delta, country)]) + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up buienradar radar-loop camera component.""" + config = entry.data + options = entry.options + + country = options.get(CONF_COUNTRY, config.get(CONF_COUNTRY, DEFAULT_COUNTRY)) + + delta = options.get(CONF_DELTA, config.get(CONF_DELTA, DEFAULT_DELTA)) + + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + + async_add_entities([BuienradarCam(latitude, longitude, delta, country)]) class BuienradarCam(Camera): @@ -59,7 +81,9 @@ class BuienradarCam(Camera): [0]: https://www.buienradar.nl/overbuienradar/gratis-weerdata """ - def __init__(self, name: str, dimension: int, delta: float, country: str): + def __init__( + self, latitude: float, longitude: float, delta: float, country: str + ) -> None: """ Initialize the component. @@ -67,10 +91,10 @@ class BuienradarCam(Camera): """ super().__init__() - self._name = name + self._name = "Buienradar" # dimension (x and y) of returned radar image - self._dimension = dimension + self._dimension = DEFAULT_DIMENSION # time a cached image stays valid for self._delta = delta @@ -94,7 +118,7 @@ class BuienradarCam(Camera): # deadline for image refresh - self.delta after last successful load self._deadline: datetime | None = None - self._unique_id = f"{self._dimension}_{self._country}" + self._unique_id = f"{latitude:2.6f}{longitude:2.6f}" @property def name(self) -> str: @@ -192,3 +216,8 @@ class BuienradarCam(Camera): def unique_id(self): """Return the unique id.""" return self._unique_id + + @property + def entity_registry_enabled_default(self) -> bool: + """Disable entity by default.""" + return False diff --git a/homeassistant/components/buienradar/config_flow.py b/homeassistant/components/buienradar/config_flow.py new file mode 100644 index 00000000000..e773b39027e --- /dev/null +++ b/homeassistant/components/buienradar/config_flow.py @@ -0,0 +1,129 @@ +"""Config flow for buienradar integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_COUNTRY, + CONF_DELTA, + CONF_TIMEFRAME, + DEFAULT_COUNTRY, + DEFAULT_DELTA, + DEFAULT_TIMEFRAME, + DOMAIN, + SUPPORTED_COUNTRY_CODES, +) + +_LOGGER = logging.getLogger(__name__) + + +class BuienradarFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for buienradar.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> BuienradarOptionFlowHandler: + """Get the options flow for this handler.""" + return BuienradarOptionFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + lat = user_input.get(CONF_LATITUDE) + lon = user_input.get(CONF_LONGITUDE) + + await self.async_set_unique_id(f"{lat}-{lon}") + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=f"{lat},{lon}", data=user_input) + + data_schema = vol.Schema( + { + vol.Required( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + } + ) + + return self.async_show_form( + step_id="user", + data_schema=data_schema, + errors={}, + ) + + async def async_step_import(self, import_input: dict[str, Any]) -> FlowResult: + """Import a config entry.""" + latitude = import_input[CONF_LATITUDE] + longitude = import_input[CONF_LONGITUDE] + + await self.async_set_unique_id(f"{latitude}-{longitude}") + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=f"{latitude},{longitude}", data=import_input + ) + + +class BuienradarOptionFlowHandler(config_entries.OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_COUNTRY, + default=self.config_entry.options.get( + CONF_COUNTRY, + self.config_entry.data.get(CONF_COUNTRY, DEFAULT_COUNTRY), + ), + ): vol.In(SUPPORTED_COUNTRY_CODES), + vol.Optional( + CONF_DELTA, + default=self.config_entry.options.get( + CONF_DELTA, + self.config_entry.data.get(CONF_DELTA, DEFAULT_DELTA), + ), + ): vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.Optional( + CONF_TIMEFRAME, + default=self.config_entry.options.get( + CONF_TIMEFRAME, + self.config_entry.data.get( + CONF_TIMEFRAME, DEFAULT_TIMEFRAME + ), + ), + ): vol.All(vol.Coerce(int), vol.Range(min=5, max=120)), + } + ), + ) diff --git a/homeassistant/components/buienradar/const.py b/homeassistant/components/buienradar/const.py index b91d2497d77..cc785512f9b 100644 --- a/homeassistant/components/buienradar/const.py +++ b/homeassistant/components/buienradar/const.py @@ -1,6 +1,24 @@ """Constants for buienradar component.""" + +DOMAIN = "buienradar" + DEFAULT_TIMEFRAME = 60 +DEFAULT_DIMENSION = 700 +DEFAULT_DELTA = 600 + +CONF_DIMENSION = "dimension" +CONF_DELTA = "delta" +CONF_COUNTRY = "country_code" +CONF_TIMEFRAME = "timeframe" + +"""Range according to the docs""" +CAMERA_DIM_MIN = 120 +CAMERA_DIM_MAX = 700 + +SUPPORTED_COUNTRY_CODES = ["NL", "BE"] +DEFAULT_COUNTRY = "NL" + """Schedule next call after (minutes).""" SCHEDULE_OK = 10 """When an error occurred, new call after (minutes).""" diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index bdaa4e166ee..d7759aa9b8d 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -1,8 +1,9 @@ { "domain": "buienradar", "name": "Buienradar", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/buienradar", "requirements": ["buienradar==1.0.4"], - "codeowners": ["@mjj4791", "@ties"], + "codeowners": ["@mjj4791", "@ties", "@Robbie1221"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 5ff15a50978..e4a317cface 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -21,6 +21,7 @@ from buienradar.constants import ( import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_LATITUDE, @@ -37,11 +38,12 @@ from homeassistant.const import ( SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from .const import DEFAULT_TIMEFRAME +from .const import CONF_TIMEFRAME, DEFAULT_TIMEFRAME from .util import BrData _LOGGER = logging.getLogger(__name__) @@ -186,8 +188,6 @@ SENSOR_TYPES = { "symbol_5d": ["Symbol 5d", None, None], } -CONF_TIMEFRAME = "timeframe" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional( @@ -208,14 +208,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up buienradar sensor platform.""" + _LOGGER.warning( + "Platform configuration is deprecated, will be removed in a future release" + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Create the buienradar sensor.""" + config = entry.data + options = entry.options + latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - timeframe = config[CONF_TIMEFRAME] + + timeframe = options.get( + CONF_TIMEFRAME, config.get(CONF_TIMEFRAME, DEFAULT_TIMEFRAME) + ) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False + return coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} @@ -225,12 +240,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= timeframe, ) - dev = [] - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - dev.append(BrSensor(sensor_type, config.get(CONF_NAME), coordinates)) - async_add_entities(dev) + entities = [ + BrSensor(sensor_type, config.get(CONF_NAME, "Buienradar"), coordinates) + for sensor_type in SENSOR_TYPES + ] - data = BrData(hass, coordinates, timeframe, dev) + async_add_entities(entities) + + data = BrData(hass, coordinates, timeframe, entities) # schedule the first update in 1 minute from now: await data.schedule_update(1) @@ -380,7 +397,7 @@ class BrSensor(SensorEntity): self._state = nested.get(self.type[len(PRECIPITATION_FORECAST) + 1 :]) return True - if self.type == WINDSPEED or self.type == WINDGUST: + if self.type in [WINDSPEED, WINDGUST]: # hass wants windspeeds in km/h not m/s, so convert: self._state = data.get(self.type) if self._state is not None: @@ -463,3 +480,8 @@ class BrSensor(SensorEntity): def force_update(self): """Return true for continuous sensors, false for discrete sensors.""" return self._force_update + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False diff --git a/homeassistant/components/buienradar/strings.json b/homeassistant/components/buienradar/strings.json new file mode 100644 index 00000000000..740068a952b --- /dev/null +++ b/homeassistant/components/buienradar/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } + } + }, + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Country code of the country to display camera images.", + "delta": "Time interval in seconds between camera image updates", + "timeframe": "Minutes to look ahead for precipitation forecast" + } + } + } + } +} diff --git a/homeassistant/components/buienradar/translations/en.json b/homeassistant/components/buienradar/translations/en.json new file mode 100644 index 00000000000..1965ab05ed9 --- /dev/null +++ b/homeassistant/components/buienradar/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "error": { + "already_configured": "Location is already configured" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Country code of the country to display camera images.", + "delta": "Time interval in seconds between camera image updates", + "timeframe": "Minutes to look ahead for precipitation forecast" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index 2ff638a2550..0aa57efc5f9 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -38,36 +38,42 @@ from homeassistant.components.weather import ( PLATFORM_SCHEMA, WeatherEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback # Reuse data and API logic from the sensor implementation -from .const import DEFAULT_TIMEFRAME +from .const import DEFAULT_TIMEFRAME, DOMAIN from .util import BrData _LOGGER = logging.getLogger(__name__) -DATA_CONDITION = "buienradar_condition" - - CONF_FORECAST = "forecast" +DATA_CONDITION = "buienradar_condition" CONDITION_CLASSES = { - ATTR_CONDITION_CLOUDY: ["c", "p"], - ATTR_CONDITION_FOG: ["d", "n"], - ATTR_CONDITION_HAIL: [], - ATTR_CONDITION_LIGHTNING: ["g"], - ATTR_CONDITION_LIGHTNING_RAINY: ["s"], - ATTR_CONDITION_PARTLYCLOUDY: ["b", "j", "o", "r"], - ATTR_CONDITION_POURING: ["l", "q"], - ATTR_CONDITION_RAINY: ["f", "h", "k", "m"], - ATTR_CONDITION_SNOWY: ["u", "i", "v", "t"], - ATTR_CONDITION_SNOWY_RAINY: ["w"], - ATTR_CONDITION_SUNNY: ["a"], - ATTR_CONDITION_WINDY: [], - ATTR_CONDITION_WINDY_VARIANT: [], - ATTR_CONDITION_EXCEPTIONAL: [], + ATTR_CONDITION_CLOUDY: ("c", "p"), + ATTR_CONDITION_FOG: ("d", "n"), + ATTR_CONDITION_HAIL: (), + ATTR_CONDITION_LIGHTNING: ("g",), + ATTR_CONDITION_LIGHTNING_RAINY: ("s",), + ATTR_CONDITION_PARTLYCLOUDY: ( + "b", + "j", + "o", + "r", + ), + ATTR_CONDITION_POURING: ("l", "q"), + ATTR_CONDITION_RAINY: ("f", "h", "k", "m"), + ATTR_CONDITION_SNOWY: ("u", "i", "v", "t"), + ATTR_CONDITION_SNOWY_RAINY: ("w",), + ATTR_CONDITION_SUNNY: ("a",), + ATTR_CONDITION_WINDY: (), + ATTR_CONDITION_WINDY_VARIANT: (), + ATTR_CONDITION_EXCEPTIONAL: (), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -81,13 +87,24 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up buienradar weather platform.""" + _LOGGER.warning( + "Platform configuration is deprecated, will be removed in a future release" + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the buienradar platform.""" + config = entry.data + latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False + return coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} @@ -97,12 +114,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.debug("Initializing buienradar weather: coordinates %s", coordinates) # create condition helper - if DATA_CONDITION not in hass.data: + if DATA_CONDITION not in hass.data[DOMAIN]: cond_keys = [str(chr(x)) for x in range(97, 123)] - hass.data[DATA_CONDITION] = dict.fromkeys(cond_keys) + hass.data[DOMAIN][DATA_CONDITION] = dict.fromkeys(cond_keys) for cond, condlst in CONDITION_CLASSES.items(): for condi in condlst: - hass.data[DATA_CONDITION][condi] = cond + hass.data[DOMAIN][DATA_CONDITION][condi] = cond async_add_entities([BrWeather(data, config, coordinates)]) @@ -115,8 +132,7 @@ class BrWeather(WeatherEntity): def __init__(self, data, config, coordinates): """Initialise the platform with a data instance and station name.""" - self._stationname = config.get(CONF_NAME) - self._forecast = config[CONF_FORECAST] + self._stationname = config.get(CONF_NAME, "Buienradar") self._data = data self._unique_id = "{:2.6f}{:2.6f}".format( @@ -141,7 +157,7 @@ class BrWeather(WeatherEntity): if self._data and self._data.condition: ccode = self._data.condition.get(CONDCODE) if ccode: - conditions = self.hass.data.get(DATA_CONDITION) + conditions = self.hass.data[DOMAIN].get(DATA_CONDITION) if conditions: return conditions.get(ccode) @@ -187,11 +203,8 @@ class BrWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - if not self._forecast: - return None - fcdata_out = [] - cond = self.hass.data[DATA_CONDITION] + cond = self.hass.data[DOMAIN][DATA_CONDITION] if not self._data.forecast: return None diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3b408860d59..ef577726b99 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -37,6 +37,7 @@ FLOWS = [ "broadlink", "brother", "bsblan", + "buienradar", "canary", "cast", "cert_expiry", diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index c9c6d7b4793..3d0c63d972b 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -1,34 +1,63 @@ """The tests for generic camera component.""" import asyncio from contextlib import suppress +import copy from aiohttp.client_exceptions import ClientResponseError -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR -from homeassistant.setup import async_setup_component +from homeassistant.components.buienradar.const import CONF_COUNTRY, CONF_DELTA, DOMAIN +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + HTTP_INTERNAL_SERVER_ERROR, +) +from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry + # An infinitesimally small time-delta. EPSILON_DELTA = 0.0000000001 +TEST_LATITUDE = 51.5288504 +TEST_LONGITUDE = 5.4002156 -def radar_map_url(dim: int = 512, country_code: str = "NL") -> str: - """Build map url, defaulting to 512 wide (as in component).""" - return f"https://api.buienradar.nl/image/1.0/RadarMap{country_code}?w={dim}&h={dim}" +TEST_CFG_DATA = {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE} + + +def radar_map_url(country_code: str = "NL") -> str: + """Build map URL.""" + return f"https://api.buienradar.nl/image/1.0/RadarMap{country_code}?w=700&h=700" + + +async def _setup_config_entry(hass, entry): + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + domain="camera", + platform="buienradar", + unique_id=f"{TEST_LATITUDE:2.6f}{TEST_LONGITUDE:2.6f}", + config_entry=entry, + original_name="Buienradar", + ) + await hass.async_block_till_done() + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client): """Test that it fetches the given url.""" aioclient_mock.get(radar_map_url(), text="hello world") - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert resp.status == 200 assert aioclient_mock.call_count == 1 @@ -38,7 +67,7 @@ async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client): # default delta is 600s -> should be the same when calling immediately # afterwards. - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 @@ -46,22 +75,19 @@ async def test_expire_delta(aioclient_mock, hass, hass_client): """Test that the cache expires after delta.""" aioclient_mock.get(radar_map_url(), text="hello world") - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "buienradar", - "delta": EPSILON_DELTA, - } - }, + options = {CONF_DELTA: EPSILON_DELTA} + + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA, options=options ) - await hass.async_block_till_done() + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert resp.status == 200 assert aioclient_mock.call_count == 1 @@ -70,7 +96,7 @@ async def test_expire_delta(aioclient_mock, hass, hass_client): await asyncio.sleep(EPSILON_DELTA) # tiny delta has passed -> should immediately call again - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 2 @@ -78,15 +104,16 @@ async def test_only_one_fetch_at_a_time(aioclient_mock, hass, hass_client): """Test that it fetches with only one request at the same time.""" aioclient_mock.get(radar_map_url(), text="hello world") - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp_1 = client.get("/api/camera_proxy/camera.config_test") - resp_2 = client.get("/api/camera_proxy/camera.config_test") + resp_1 = client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") + resp_2 = client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") resp = await resp_1 resp_2 = await resp_2 @@ -96,44 +123,22 @@ async def test_only_one_fetch_at_a_time(aioclient_mock, hass, hass_client): assert aioclient_mock.call_count == 1 -async def test_dimension(aioclient_mock, hass, hass_client): - """Test that it actually adheres to the dimension.""" - aioclient_mock.get(radar_map_url(700), text="hello world") - - await async_setup_component( - hass, - "camera", - {"camera": {"name": "config_test", "platform": "buienradar", "dimension": 700}}, - ) - await hass.async_block_till_done() - - client = await hass_client() - - await client.get("/api/camera_proxy/camera.config_test") - - assert aioclient_mock.call_count == 1 - - async def test_belgium_country(aioclient_mock, hass, hass_client): """Test that it actually adheres to another country like Belgium.""" aioclient_mock.get(radar_map_url(country_code="BE"), text="hello world") - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "buienradar", - "country_code": "BE", - } - }, - ) - await hass.async_block_till_done() + data = copy.deepcopy(TEST_CFG_DATA) + data[CONF_COUNTRY] = "BE" + + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=data) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - await client.get("/api/camera_proxy/camera.config_test") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 @@ -142,15 +147,16 @@ async def test_failure_response_not_cached(aioclient_mock, hass, hass_client): """Test that it does not cache a failure response.""" aioclient_mock.get(radar_map_url(), text="hello world", status=401) - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - await client.get("/api/camera_proxy/camera.config_test") - await client.get("/api/camera_proxy/camera.config_test") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 2 @@ -168,22 +174,19 @@ async def test_last_modified_updates(aioclient_mock, hass, hass_client): headers={"Last-Modified": last_modified}, ) - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "buienradar", - "delta": EPSILON_DELTA, - } - }, + options = {CONF_DELTA: EPSILON_DELTA} + + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA, options=options ) - await hass.async_block_till_done() + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp_1 = await client.get("/api/camera_proxy/camera.config_test") + resp_1 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") # It is not possible to check if header was sent. assert aioclient_mock.call_count == 1 @@ -197,7 +200,7 @@ async def test_last_modified_updates(aioclient_mock, hass, hass_client): aioclient_mock.get(radar_map_url(), text=None, status=304) - resp_2 = await client.get("/api/camera_proxy/camera.config_test") + resp_2 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 assert (await resp_1.read()) == (await resp_2.read()) @@ -205,10 +208,11 @@ async def test_last_modified_updates(aioclient_mock, hass, hass_client): async def test_retries_after_error(aioclient_mock, hass, hass_client): """Test that it does retry after an error instead of caching.""" - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() @@ -216,7 +220,7 @@ async def test_retries_after_error(aioclient_mock, hass, hass_client): # A 404 should not return data and throw: with suppress(ClientResponseError): - await client.get("/api/camera_proxy/camera.config_test") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 @@ -227,7 +231,7 @@ async def test_retries_after_error(aioclient_mock, hass, hass_client): assert aioclient_mock.call_count == 0 # http error should not be cached, immediate retry. - resp_2 = await client.get("/api/camera_proxy/camera.config_test") + resp_2 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 # Binary text can not be added as body to `aioclient_mock.get(text=...)`, diff --git a/tests/components/buienradar/test_config_flow.py b/tests/components/buienradar/test_config_flow.py new file mode 100644 index 00000000000..b8abefec70a --- /dev/null +++ b/tests/components/buienradar/test_config_flow.py @@ -0,0 +1,131 @@ +"""Test the buienradar2 config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import MockConfigEntry + +TEST_LATITUDE = 51.5288504 +TEST_LONGITUDE = 5.4002156 + + +async def test_config_flow_setup_(hass): + """Test setup of camera.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == f"{TEST_LATITUDE},{TEST_LONGITUDE}" + assert result["data"] == { + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + } + + +async def test_config_flow_already_configured_weather(hass): + """Test already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + unique_id=f"{TEST_LATITUDE}-{TEST_LONGITUDE}", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_import_camera(hass): + """Test import of camera.""" + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == f"{TEST_LATITUDE},{TEST_LONGITUDE}" + assert result["data"] == { + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_options_flow(hass): + """Test options flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"country_code": "BE", "delta": 450, "timeframe": 30}, + ) + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.buienradar.async_unload_entry", return_value=True + ): + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.options == {"country_code": "BE", "delta": 450, "timeframe": 30} diff --git a/tests/components/buienradar/test_init.py b/tests/components/buienradar/test_init.py new file mode 100644 index 00000000000..e3ac8c025e1 --- /dev/null +++ b/tests/components/buienradar/test_init.py @@ -0,0 +1,120 @@ +"""Tests for the buienradar component.""" +from unittest.mock import patch + +from homeassistant.components.buienradar import async_setup +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.entity_registry import async_get_registry + +from tests.common import MockConfigEntry + +TEST_LATITUDE = 51.5288504 +TEST_LONGITUDE = 5.4002156 + + +async def test_import_all(hass): + """Test import of all platforms.""" + config = { + "weather 1": [{"platform": "buienradar", "name": "test1"}], + "sensor 1": [{"platform": "buienradar", "timeframe": 30, "name": "test2"}], + "camera 1": [ + { + "platform": "buienradar", + "country_code": "BE", + "delta": 300, + "name": "test3", + } + ], + } + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + await async_setup(hass, config) + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(conf_entries) == 1 + + entry = conf_entries[0] + + assert entry.state == "loaded" + assert entry.data == { + "latitude": hass.config.latitude, + "longitude": hass.config.longitude, + "timeframe": 30, + "country_code": "BE", + "delta": 300, + "name": "test2", + } + + +async def test_import_camera(hass): + """Test import of camera platform.""" + entity_registry = await async_get_registry(hass) + entity_registry.async_get_or_create( + domain="camera", + platform="buienradar", + unique_id="512_NL", + original_name="test_name", + ) + await hass.async_block_till_done() + + config = { + "camera 1": [{"platform": "buienradar", "country_code": "NL", "dimension": 512}] + } + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + await async_setup(hass, config) + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(conf_entries) == 1 + + entry = conf_entries[0] + + assert entry.state == "loaded" + assert entry.data == { + "latitude": hass.config.latitude, + "longitude": hass.config.longitude, + "timeframe": 60, + "country_code": "NL", + "delta": 600, + "name": "Buienradar", + } + + entity_id = entity_registry.async_get_entity_id( + "camera", + "buienradar", + f"{hass.config.latitude:2.6f}{hass.config.longitude:2.6f}", + ) + assert entity_id + entity = entity_registry.async_get(entity_id) + assert entity.original_name == "test_name" + + +async def test_load_unload(hass): + """Test options flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == "loaded" + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == "not_loaded" diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py index 801f5706a08..f0a24b6beb3 100644 --- a/tests/components/buienradar/test_sensor.py +++ b/tests/components/buienradar/test_sensor.py @@ -1,26 +1,29 @@ """The tests for the Buienradar sensor platform.""" -from homeassistant.components import sensor -from homeassistant.setup import async_setup_component +from unittest.mock import patch + +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import MockConfigEntry CONDITIONS = ["stationname", "temperature"] -BASE_CONFIG = { - "sensor": [ - { - "platform": "buienradar", - "name": "volkel", - "latitude": 51.65, - "longitude": 5.7, - "monitored_conditions": CONDITIONS, - } - ] -} +TEST_CFG_DATA = {CONF_LATITUDE: 51.5288504, CONF_LONGITUDE: 5.4002156} async def test_smoke_test_setup_component(hass): """Smoke test for successfully set-up with default config.""" - assert await async_setup_component(hass, sensor.DOMAIN, BASE_CONFIG) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.buienradar.sensor.BrSensor.entity_registry_enabled_default" + ) as enabled_by_default_mock: + enabled_by_default_mock.return_value = True + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() for cond in CONDITIONS: - state = hass.states.get(f"sensor.volkel_{cond}") + state = hass.states.get(f"sensor.buienradar_{cond}") assert state.state == "unknown" diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py index db0a6ce3984..9d16b531ad0 100644 --- a/tests/components/buienradar/test_weather.py +++ b/tests/components/buienradar/test_weather.py @@ -1,25 +1,20 @@ """The tests for the buienradar weather component.""" -from homeassistant.components import weather -from homeassistant.setup import async_setup_component +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -# Example config snippet from documentation. -BASE_CONFIG = { - "weather": [ - { - "platform": "buienradar", - "name": "volkel", - "latitude": 51.65, - "longitude": 5.7, - "forecast": True, - } - ] -} +from tests.common import MockConfigEntry + +TEST_CFG_DATA = {CONF_LATITUDE: 51.5288504, CONF_LONGITUDE: 5.4002156} async def test_smoke_test_setup_component(hass): """Smoke test for successfully set-up with default config.""" - assert await async_setup_component(hass, weather.DOMAIN, BASE_CONFIG) + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("weather.volkel") + state = hass.states.get("weather.buienradar") assert state.state == "unknown" From a0feee083cc17dfc47e194af146f726e89999166 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 4 May 2021 14:47:17 +0200 Subject: [PATCH 150/852] Fix and enable type checks in Rituals Perfume Genie (#49947) --- .../rituals_perfume_genie/__init__.py | 4 ++-- .../rituals_perfume_genie/binary_sensor.py | 6 +++-- .../rituals_perfume_genie/entity.py | 13 +++++++---- .../rituals_perfume_genie/sensor.py | 22 ++++++++++++++----- .../rituals_perfume_genie/switch.py | 6 +++-- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 7 files changed, 35 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index a06c9acdd7d..1906478cbd2 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): for device in account_devices: hublot = device.hub_data[HUBLOT] - coordinator = RitualsPerufmeGenieDataUpdateCoordinator(hass, device) + coordinator = RitualsDataUpdateCoordinator(hass, device) await coordinator.async_refresh() hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device @@ -61,7 +61,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -class RitualsPerufmeGenieDataUpdateCoordinator(DataUpdateCoordinator): +class RitualsDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Rituals Perufme Genie device data from single endpoint.""" def __init__(self, hass: HomeAssistant, device: Diffuser): diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index a73595dfdcf..ffeceb079bb 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -11,8 +11,8 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import RitualsDataUpdateCoordinator from .const import COORDINATORS, DEVICES, DOMAIN from .entity import DiffuserEntity @@ -38,7 +38,9 @@ async def async_setup_entry( class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): """Representation of a diffuser battery charging binary sensor.""" - def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: """Initialize the battery charging binary sensor.""" super().__init__(diffuser, coordinator, CHARGING_SUFFIX) diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index 1253a38e962..1c1f3912c68 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -1,12 +1,12 @@ """Base class for Rituals Perfume Genie diffuser entity.""" from __future__ import annotations -from typing import Any - from pyrituals import Diffuser +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import RitualsDataUpdateCoordinator from .const import ATTRIBUTES, DOMAIN, HUBLOT, SENSORS MANUFACTURER = "Rituals Cosmetics" @@ -23,8 +23,13 @@ AVAILABLE_STATE = 1 class DiffuserEntity(CoordinatorEntity): """Representation of a diffuser entity.""" + coordinator: RitualsDataUpdateCoordinator + def __init__( - self, diffuser: Diffuser, coordinator: CoordinatorEntity, entity_suffix: str + self, + diffuser: Diffuser, + coordinator: RitualsDataUpdateCoordinator, + entity_suffix: str, ) -> None: """Init from config, hookup diffuser and coordinator.""" super().__init__(coordinator) @@ -49,7 +54,7 @@ class DiffuserEntity(CoordinatorEntity): return super().available and self._diffuser.hub_data[STATUS] == AVAILABLE_STATE @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return information about the device.""" return { "name": self._hubname, diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 1c12b305d48..5982678e9c6 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -1,4 +1,6 @@ """Support for Rituals Perfume Genie sensors.""" +from __future__ import annotations + from typing import Callable from pyrituals import Diffuser @@ -10,8 +12,8 @@ from homeassistant.const import ( PERCENTAGE, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import RitualsDataUpdateCoordinator from .const import COORDINATORS, DEVICES, DOMAIN, ID, SENSORS from .entity import DiffuserEntity @@ -38,7 +40,7 @@ async def async_setup_entry( """Set up the diffuser sensors.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] - entities = [] + entities: list[DiffuserEntity] = [] for hublot, diffuser in diffusers.items(): coordinator = coordinators[hublot] entities.append(DiffuserPerfumeSensor(diffuser, coordinator)) @@ -53,7 +55,9 @@ async def async_setup_entry( class DiffuserPerfumeSensor(DiffuserEntity): """Representation of a diffuser perfume sensor.""" - def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: """Initialize the perfume sensor.""" super().__init__(diffuser, coordinator, PERFUME_SUFFIX) @@ -73,7 +77,9 @@ class DiffuserPerfumeSensor(DiffuserEntity): class DiffuserFillSensor(DiffuserEntity): """Representation of a diffuser fill sensor.""" - def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: """Initialize the fill sensor.""" super().__init__(diffuser, coordinator, FILL_SUFFIX) @@ -93,7 +99,9 @@ class DiffuserFillSensor(DiffuserEntity): class DiffuserBatterySensor(DiffuserEntity): """Representation of a diffuser battery sensor.""" - def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: """Initialize the battery sensor.""" super().__init__(diffuser, coordinator, BATTERY_SUFFIX) @@ -116,7 +124,9 @@ class DiffuserBatterySensor(DiffuserEntity): class DiffuserWifiSensor(DiffuserEntity): """Representation of a diffuser wifi sensor.""" - def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: """Initialize the wifi sensor.""" super().__init__(diffuser, coordinator, WIFI_SUFFIX) diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index a564a04c698..83e392f7bad 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -8,8 +8,8 @@ from pyrituals import Diffuser from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import RitualsDataUpdateCoordinator from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN from .entity import DiffuserEntity @@ -37,7 +37,9 @@ async def async_setup_entry( class DiffuserSwitch(SwitchEntity, DiffuserEntity): """Representation of a diffuser switch.""" - def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: """Initialize the diffuser switch.""" super().__init__(diffuser, coordinator, "") self._is_on = self._diffuser.is_on diff --git a/mypy.ini b/mypy.ini index 68afb4d9786..948b2de0e11 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1125,9 +1125,6 @@ ignore_errors = true [mypy-homeassistant.components.ring.*] ignore_errors = true -[mypy-homeassistant.components.rituals_perfume_genie.*] -ignore_errors = true - [mypy-homeassistant.components.roku.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 91752c0acdf..dc4485864f9 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -178,7 +178,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.recorder.*", "homeassistant.components.reddit.*", "homeassistant.components.ring.*", - "homeassistant.components.rituals_perfume_genie.*", "homeassistant.components.roku.*", "homeassistant.components.rpi_power.*", "homeassistant.components.ruckus_unleashed.*", From 786c5db5be0edfd52786dcb457ff8ad29e5f84d2 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Tue, 4 May 2021 13:50:06 +0100 Subject: [PATCH 151/852] Use AddEntitiesCallback type, pt.4 (#49955) --- homeassistant/components/here_travel_time/sensor.py | 4 ++-- homeassistant/components/knx/binary_sensor.py | 7 +++---- homeassistant/components/knx/climate.py | 7 +++---- homeassistant/components/knx/cover.py | 5 ++--- homeassistant/components/knx/fan.py | 7 +++---- homeassistant/components/knx/light.py | 7 +++---- homeassistant/components/knx/scene.py | 7 +++---- homeassistant/components/knx/sensor.py | 7 +++---- homeassistant/components/knx/switch.py | 7 +++---- homeassistant/components/knx/weather.py | 7 ++----- homeassistant/components/sma/sensor.py | 3 ++- homeassistant/components/switch/light.py | 7 +++---- homeassistant/components/switcher_kis/switch.py | 5 ++--- homeassistant/components/systemmonitor/sensor.py | 5 +++-- 14 files changed, 37 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 4b8f765d08a..c02456b2a3f 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import Callable import herepy import voluptuous as vol @@ -26,6 +25,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import location import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import DiscoveryInfoType import homeassistant.util.dt as dt @@ -145,7 +145,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform( hass: HomeAssistant, config: dict[str, str | bool], - async_add_entities: Callable, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the HERE travel time platform.""" diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index d81a86970a4..9a271ec965f 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,14 +1,13 @@ """Support for KNX/IP binary sensors.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Any, Callable +from typing import Any from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt @@ -19,7 +18,7 @@ from .knx_entity import KnxEntity async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up binary sensor(s) for KNX platform.""" diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index b059c86e8a0..de9b71a2287 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,8 +1,7 @@ """Support for KNX/IP climate devices.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Any, Callable +from typing import Any from xknx.devices import Climate as XknxClimate from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode @@ -19,7 +18,7 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONTROLLER_MODES, DOMAIN, PRESET_MODES @@ -33,7 +32,7 @@ PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()} async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up climate(s) for KNX platform.""" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index d8983089a93..b0b69c83a31 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -1,7 +1,6 @@ """Support for KNX/IP covers.""" from __future__ import annotations -from collections.abc import Iterable from datetime import datetime from typing import Any, Callable @@ -25,7 +24,7 @@ from homeassistant.components.cover import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -37,7 +36,7 @@ from .schema import CoverSchema async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up cover(s) for KNX platform.""" diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index b526f727f7a..2bc9ab8e654 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -1,15 +1,14 @@ """Support for KNX/IP fans.""" from __future__ import annotations -from collections.abc import Iterable import math -from typing import Any, Callable +from typing import Any from xknx.devices import Fan as XknxFan from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.percentage import ( int_states_in_range, @@ -26,7 +25,7 @@ DEFAULT_PERCENTAGE = 50 async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up fans for KNX platform.""" diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index d3086cacd0f..46768e97b96 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -1,8 +1,7 @@ """Support for KNX/IP lights.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Any, Callable, cast +from typing import Any, cast from xknx.devices import Light as XknxLight from xknx.telegram.address import parse_device_group_address @@ -21,7 +20,7 @@ from homeassistant.components.light import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.color as color_util @@ -33,7 +32,7 @@ from .schema import LightSchema async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up lights for KNX platform.""" diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 8aa55917973..23f375972e6 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -1,14 +1,13 @@ """Support for KNX scenes.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Any, Callable +from typing import Any from xknx.devices import Scene as XknxScene from homeassistant.components.scene import Scene from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN @@ -18,7 +17,7 @@ from .knx_entity import KnxEntity async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the scenes for KNX platform.""" diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 190e0feb4b3..fa4de79cb03 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,14 +1,13 @@ """Support for KNX/IP sensors.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Any, Callable +from typing import Any from xknx.devices import Sensor as XknxSensor from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util import dt @@ -19,7 +18,7 @@ from .knx_entity import KnxEntity async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up sensor(s) for KNX platform.""" diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 6006b45c60c..c6fbb32b15d 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -1,8 +1,7 @@ """Support for KNX/IP switches.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Any, Callable +from typing import Any from xknx import XKNX from xknx.devices import Switch as XknxSwitch @@ -10,7 +9,7 @@ from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN, KNX_ADDRESS @@ -21,7 +20,7 @@ from .schema import SwitchSchema async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up switch(es) for KNX platform.""" diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 21cb6ddf55c..18cb217105c 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -1,15 +1,12 @@ """Support for KNX/IP weather station.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Callable - from xknx.devices import Weather as XknxWeather from homeassistant.components.weather import WeatherEntity from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN @@ -19,7 +16,7 @@ from .knx_entity import KnxEntity async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up weather entities for KNX platform.""" diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index ad530367904..cc5c92d2159 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -98,7 +99,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform( hass: HomeAssistant, config: ConfigEntry, - async_add_entities: Callable[[], Coroutine], + async_add_entities: AddEntitiesCallback, discovery_info=None, ) -> None: """Import the platform into a config entry.""" diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 039090d3124..12fb847f86b 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,8 +1,7 @@ """Light support for switch entities.""" from __future__ import annotations -from collections.abc import Sequence -from typing import Any, Callable, cast +from typing import Any, cast import voluptuous as vol @@ -17,7 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -36,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable[[Sequence[Entity]], None], + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Initialize Light Switch platform.""" diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index de47f11748f..8f7332162a9 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -1,8 +1,6 @@ """Home Assistant Switcher Component Switch platform.""" from __future__ import annotations -from typing import Callable - from aioswitcher.api import SwitcherV2Api from aioswitcher.api.messages import SwitcherV2ControlResponseMSG from aioswitcher.consts import ( @@ -19,6 +17,7 @@ from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( ATTR_AUTO_OFF_SET, @@ -55,7 +54,7 @@ SERVICE_TURN_ON_WITH_TIMER_SCHEMA = { async def async_setup_platform( hass: HomeAssistant, config: dict, - async_add_entities: Callable, + async_add_entities: AddEntitiesCallback, discovery_info: dict, ) -> None: """Set up the switcher platform for the switch component.""" diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 70fd8275bd7..6cec3f1d81b 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -9,7 +9,7 @@ import logging import os import socket import sys -from typing import Any, Callable, cast +from typing import Any, cast import psutil import voluptuous as vol @@ -36,6 +36,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -198,7 +199,7 @@ class SensorData: async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: Callable, + async_add_entities: AddEntitiesCallback, discovery_info: Any | None = None, ) -> None: """Set up the system monitor sensors.""" From c21add195a780a98e50c5e5f00d9c05da148d033 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 4 May 2021 19:10:28 +0300 Subject: [PATCH 152/852] Catch Shelly set state exceptions when device is inaccessible (#50064) --- homeassistant/components/shelly/cover.py | 8 ++--- homeassistant/components/shelly/entity.py | 20 +++++++++++- homeassistant/components/shelly/light.py | 37 ++++++++++++++++++++--- homeassistant/components/shelly/switch.py | 4 +-- 4 files changed, 57 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index 0438e5fe6b7..18f13479c30 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -83,24 +83,24 @@ class ShellyCover(ShellyBlockEntity, CoverEntity): async def async_close_cover(self, **kwargs): """Close cover.""" - self.control_result = await self.block.set_state(go="close") + self.control_result = await self.set_state(go="close") self.async_write_ha_state() async def async_open_cover(self, **kwargs): """Open cover.""" - self.control_result = await self.block.set_state(go="open") + self.control_result = await self.set_state(go="open") self.async_write_ha_state() async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - self.control_result = await self.block.set_state( + self.control_result = await self.set_state( go="to_pos", roller_pos=kwargs[ATTR_POSITION] ) self.async_write_ha_state() async def async_stop_cover(self, **_kwargs): """Stop the cover.""" - self.control_result = await self.block.set_state(go="stop") + self.control_result = await self.set_state(go="stop") self.async_write_ha_state() @callback diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 44ef41b82b6..675eead2155 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -1,11 +1,13 @@ """Shelly entity helper.""" from __future__ import annotations +import asyncio from dataclasses import dataclass import logging from typing import Any, Callable import aioshelly +import async_timeout from homeassistant.core import callback from homeassistant.helpers import ( @@ -17,7 +19,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.restore_state import RestoreEntity from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper -from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, COAP, DATA_CONFIG_ENTRY, DOMAIN, REST from .utils import async_remove_shelly_entity, get_entity_name _LOGGER = logging.getLogger(__name__) @@ -218,6 +220,22 @@ class ShellyBlockEntity(entity.Entity): """Handle device update.""" self.async_write_ha_state() + async def set_state(self, **kwargs): + """Set block state (HTTP request).""" + _LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + return await self.block.set_state(**kwargs) + except (asyncio.TimeoutError, OSError) as err: + _LOGGER.error( + "Setting state for entity %s failed, state: %s, error: %s", + self.name, + kwargs, + repr(err), + ) + self.wrapper.last_update_success = False + return None + class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): """Helper class to represent a block attribute.""" diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index f3e80ee87b0..adca498c3f9 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -1,7 +1,11 @@ """Light for Shelly.""" from __future__ import annotations +import asyncio +import logging + from aioshelly import Block +import async_timeout from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -24,6 +28,7 @@ from homeassistant.util.color import ( from . import ShellyDeviceWrapper from .const import ( + AIOSHELLY_DEVICE_TIMEOUT_SEC, COAP, DATA_CONFIG_ENTRY, DOMAIN, @@ -34,6 +39,8 @@ from .const import ( from .entity import ShellyBlockEntity from .utils import async_remove_shelly_entity +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up lights for device.""" @@ -199,7 +206,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): async def async_turn_on(self, **kwargs) -> None: """Turn on light.""" if self.block.type == "relay": - self.control_result = await self.block.set_state(turn="on") + self.control_result = await self.set_state(turn="on") self.async_write_ha_state() return @@ -233,17 +240,37 @@ class ShellyLight(ShellyBlockEntity, LightEntity): ATTR_RGBW_COLOR ] - if set_mode and self.mode != set_mode: - self.mode_result = await self.wrapper.device.switch_light_mode(set_mode) + if await self.set_light_mode(set_mode): + self.control_result = await self.set_state(**params) - self.control_result = await self.block.set_state(**params) self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn off light.""" - self.control_result = await self.block.set_state(turn="off") + self.control_result = await self.set_state(turn="off") self.async_write_ha_state() + async def set_light_mode(self, set_mode): + """Change device mode color/white if mode has changed.""" + if set_mode is None or self.mode == set_mode: + return True + + _LOGGER.debug("Setting light mode for entity %s, mode: %s", self.name, set_mode) + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + self.mode_result = await self.wrapper.device.switch_light_mode(set_mode) + except (asyncio.TimeoutError, OSError) as err: + _LOGGER.error( + "Setting light mode for entity %s failed, state: %s, error: %s", + self.name, + set_mode, + repr(err), + ) + self.wrapper.last_update_success = False + return False + + return True + @callback def _update_callback(self): """When device updates, clear control & mode result that overrides state.""" diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index c86487072c6..6f3dd0b0136 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -62,12 +62,12 @@ class RelaySwitch(ShellyBlockEntity, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn on relay.""" - self.control_result = await self.block.set_state(turn="on") + self.control_result = await self.set_state(turn="on") self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn off relay.""" - self.control_result = await self.block.set_state(turn="off") + self.control_result = await self.set_state(turn="off") self.async_write_ha_state() @callback From ee5f955fd859e79f2b659f2b0534781004f48138 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 May 2021 20:06:54 +0200 Subject: [PATCH 153/852] Clean up stale config schema from deCONZ (#50081) --- homeassistant/components/deconz/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index eb659b870c1..8b47363c7ba 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,6 +1,4 @@ """Support for deCONZ devices.""" -import voluptuous as vol - from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -15,10 +13,6 @@ from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN from .gateway import DeconzGateway from .services import async_setup_services, async_unload_services -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({}, extra=vol.ALLOW_EXTRA)}, extra=vol.ALLOW_EXTRA -) - async def async_setup_entry(hass, config_entry): """Set up a deCONZ bridge for a config entry. From 96f69fb9fb5b96f5d02ed92b893363a855aaa4ed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 May 2021 20:08:51 +0200 Subject: [PATCH 154/852] Finalize clean up connection classes (#49895) --- homeassistant/config_entries.py | 13 +++++++------ homeassistant/helpers/config_entry_oauth2_flow.py | 1 - .../config_flow/integration/config_flow.py | 2 -- .../config_flow_oauth2/integration/config_flow.py | 3 --- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 76e523a6d89..7275d3101a9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -92,6 +92,13 @@ RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure" EVENT_FLOW_DISCOVERED = "config_entry_discovered" +DISABLED_USER = "user" + +RELOAD_AFTER_UPDATE_DELAY = 30 + +# Deprecated: Connection classes +# These aren't used anymore since 2021.6.0 +# Mainly here not to break custom integrations. CONN_CLASS_CLOUD_PUSH = "cloud_push" CONN_CLASS_CLOUD_POLL = "cloud_poll" CONN_CLASS_LOCAL_PUSH = "local_push" @@ -99,10 +106,6 @@ CONN_CLASS_LOCAL_POLL = "local_poll" CONN_CLASS_ASSUMED = "assumed" CONN_CLASS_UNKNOWN = "unknown" -DISABLED_USER = "user" - -RELOAD_AFTER_UPDATE_DELAY = 30 - class ConfigError(HomeAssistantError): """Error while configuring an account.""" @@ -1068,8 +1071,6 @@ class ConfigFlow(data_entry_flow.FlowHandler): if domain is not None: HANDLERS.register(domain)(cls) - CONNECTION_CLASS = CONN_CLASS_UNKNOWN - @property def unique_id(self) -> str | None: """Return unique ID if available.""" diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index ede345ce7de..be9afe385ca 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -212,7 +212,6 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): DOMAIN = "" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN def __init__(self) -> None: """Instantiate config flow.""" diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py index 886f3223655..f88390599e7 100644 --- a/script/scaffold/templates/config_flow/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -65,8 +65,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for NEW_NAME.""" VERSION = 1 - # 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: dict[str, Any] | None = None diff --git a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py index 8670c8a7b43..a035a65dbb3 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py @@ -1,7 +1,6 @@ """Config flow for NEW_NAME.""" import logging -from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN @@ -13,8 +12,6 @@ class OAuth2FlowHandler( """Config flow to handle NEW_NAME OAuth2 authentication.""" DOMAIN = DOMAIN - # TODO Pick one from config_entries.CONN_CLASS_* - CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN @property def logger(self) -> logging.Logger: From 13ba4d7572a5f71737e510b40bc17eb02ee132cf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 May 2021 20:43:41 +0200 Subject: [PATCH 155/852] Upgrade pytest to 6.2.4 (#50077) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index dd24366e24b..efa1a140482 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -20,7 +20,7 @@ pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==1.4.2 pytest-xdist==2.2.1 -pytest==6.2.3 +pytest==6.2.4 requests_mock==1.8.0 responses==0.12.0 respx==0.17.0 From fb2cb469e284408d08a372d8298ec1e63faed9e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 May 2021 21:10:17 +0200 Subject: [PATCH 156/852] Remove YAML configuration from DoorBird (#50082) --- homeassistant/components/doorbird/__init__.py | 26 +--- .../components/doorbird/config_flow.py | 12 -- tests/components/doorbird/test_config_flow.py | 119 ------------------ 3 files changed, 2 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 9a21c1b3439..30c2613f0d5 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -7,10 +7,9 @@ import requests import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PASSWORD, @@ -56,17 +55,7 @@ DEVICE_SCHEMA = vol.Schema( } ) -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - {vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])} - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup(hass: HomeAssistant, config: dict): @@ -76,17 +65,6 @@ async def async_setup(hass: HomeAssistant, config: dict): # Provide an endpoint for the doorstations to call to trigger events hass.http.register_view(DoorBirdRequestView) - if DOMAIN in config and CONF_DEVICES in config[DOMAIN]: - for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]): - if CONF_NAME not in doorstation_config: - doorstation_config[CONF_NAME] = f"DoorBird {index + 1}" - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=doorstation_config - ) - ) - def _reset_device_favorites_handler(event): """Handle clearing favorites on device.""" token = event.data.get("token") diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 2558d1a3869..5d207fbbbce 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -124,18 +124,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() - async def async_step_import(self, user_input): - """Handle import.""" - if user_input: - info, errors = await self._async_validate_or_error(user_input) - if not errors: - await self.async_set_unique_id( - info["mac_addr"], raise_on_progress=False - ) - self._abort_if_unique_id_configured() - return self.async_create_entry(title=info["title"], data=user_input) - return await self.async_step_user(user_input) - async def _async_validate_or_error(self, user_input): """Validate doorbird or error.""" errors = {} diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 9fa9752dc65..915955da652 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -5,7 +5,6 @@ import pytest import requests from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -76,124 +75,6 @@ async def test_user_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_import(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - import_config = VALID_CONFIG.copy() - import_config[CONF_EVENTS] = ["event1", "event2", "event3"] - import_config[CONF_TOKEN] = "imported_token" - import_config[ - CONF_CUSTOM_URL - ] = "http://legacy.custom.url/should/only/come/in/from/yaml" - - doorbirdapi = _get_mock_doorbirdapi_return_values( - ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} - ) - with patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( - "homeassistant.components.doorbird.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.doorbird.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=import_config, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "1.2.3.4" - assert result["data"] == { - "host": "1.2.3.4", - "name": "mydoorbird", - "password": "password", - "username": "friend", - "events": ["event1", "event2", "event3"], - "token": "imported_token", - # This will go away once we convert to cloud hooks - "hass_url_override": "http://legacy.custom.url/should/only/come/in/from/yaml", - } - # It is not possible to import options at this time - # so they end up in the config entry data and are - # used a fallback when they are not in options - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_import_with_zeroconf_already_discovered(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - doorbirdapi = _get_mock_doorbirdapi_return_values( - ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"} - ) - # Running the zeroconf init will make the unique id - # in progress - with patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ): - zero_conf = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"macaddress": "1CCAE3DOORBIRD"}, - "name": "Doorstation - abc123._axis-video._tcp.local.", - "host": "192.168.1.5", - }, - ) - await hass.async_block_till_done() - assert zero_conf["type"] == data_entry_flow.RESULT_TYPE_FORM - assert zero_conf["step_id"] == "user" - assert zero_conf["errors"] == {} - - import_config = VALID_CONFIG.copy() - import_config[CONF_EVENTS] = ["event1", "event2", "event3"] - import_config[CONF_TOKEN] = "imported_token" - import_config[ - CONF_CUSTOM_URL - ] = "http://legacy.custom.url/should/only/come/in/from/yaml" - - with patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( - "homeassistant.components.doorbird.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.doorbird.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=import_config, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "1.2.3.4" - assert result["data"] == { - "host": "1.2.3.4", - "name": "mydoorbird", - "password": "password", - "username": "friend", - "events": ["event1", "event2", "event3"], - "token": "imported_token", - # This will go away once we convert to cloud hooks - "hass_url_override": "http://legacy.custom.url/should/only/come/in/from/yaml", - } - # It is not possible to import options at this time - # so they end up in the config entry data and are - # used a fallback when they are not in options - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_zeroconf_wrong_oui(hass): """Test we abort when we get the wrong OUI via zeroconf.""" await setup.async_setup_component(hass, "persistent_notification", {}) From e5ef171077a17f2e940200cfc113688594c77f00 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 4 May 2021 16:06:14 -0400 Subject: [PATCH 157/852] Bump zigpy-znp from 0.4.0 to 0.5.1 (#50086) * Bump zigpy-znp from 0.4.0 to 0.5.0 * Add zigpy-znp to ZHA debug logging config * Bump zigpy-znp from 0.5.0 to 0.5.1 --- homeassistant/components/zha/core/const.py | 2 ++ homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c4c18c4304b..2110cc9546d 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -168,6 +168,7 @@ DEBUG_COMP_BELLOWS = "bellows" DEBUG_COMP_ZHA = "homeassistant.components.zha" DEBUG_COMP_ZIGPY = "zigpy" DEBUG_COMP_ZIGPY_CC = "zigpy_cc" +DEBUG_COMP_ZIGPY_ZNP = "zigpy_znp" DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz" DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee" DEBUG_COMP_ZIGPY_ZIGATE = "zigpy_zigate" @@ -178,6 +179,7 @@ DEBUG_LEVELS = { DEBUG_COMP_ZHA: logging.DEBUG, DEBUG_COMP_ZIGPY: logging.DEBUG, DEBUG_COMP_ZIGPY_CC: logging.DEBUG, + DEBUG_COMP_ZIGPY_ZNP: logging.DEBUG, DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG, DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, DEBUG_COMP_ZIGPY_ZIGATE: logging.DEBUG, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 42859f301b7..ec231ccc0e4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -13,7 +13,7 @@ "zigpy==0.33.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.4.0" + "zigpy-znp==0.5.1" ], "codeowners": ["@dmulcahey", "@adminiuga"], "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index c377e04a40b..f810324678c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2415,7 +2415,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.4.0 +zigpy-znp==0.5.1 # homeassistant.components.zha zigpy==0.33.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a879156951..42908397592 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1285,7 +1285,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.4.0 +zigpy-znp==0.5.1 # homeassistant.components.zha zigpy==0.33.0 From 4af6e505b3dd3adf048da6d522f09a5498207c92 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 May 2021 22:18:22 +0200 Subject: [PATCH 158/852] Deprecate speedtest.net YAML config (#50072) --- .../components/speedtestdotnet/__init__.py | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 9c76b351f33..b8f80de1b52 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -29,20 +29,25 @@ from .const import ( _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_SERVER_ID): cv.positive_int, - vol.Optional( - CONF_SCAN_INTERVAL, default=timedelta(minutes=DEFAULT_SCAN_INTERVAL) - ): cv.positive_time_period, - vol.Optional(CONF_MANUAL, default=False): cv.boolean, - vol.Optional( - CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES) - ): vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), - } - ) - }, + vol.All( + # Deprecated in Home Assistant 2021.6 + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_SERVER_ID): cv.positive_int, + vol.Optional( + CONF_SCAN_INTERVAL, + default=timedelta(minutes=DEFAULT_SCAN_INTERVAL), + ): cv.positive_time_period, + vol.Optional(CONF_MANUAL, default=False): cv.boolean, + vol.Optional( + CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES) + ): vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From 98ef1e1e8210eae7ba7263d6206dede44460f5c1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 May 2021 22:18:46 +0200 Subject: [PATCH 159/852] Remove stale config schema from ESPHome integration (#50083) --- homeassistant/components/esphome/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 66b16cf3fe3..a3b3f187906 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -51,9 +51,6 @@ _LOGGER = logging.getLogger(__name__) STORAGE_VERSION = 1 -# No config schema - only configuration entry -CONFIG_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the esphome component.""" From 4cf910affc162586f7b0347f2ab7cd76271b4055 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 May 2021 14:23:22 -0700 Subject: [PATCH 160/852] Guard logbook assuming entity ID is a string (#50047) --- homeassistant/components/logbook/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 8d216b5c6f0..de0f901be3b 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -647,7 +647,7 @@ def _augment_data_with_context( return attr_entity_id = event_data.get(ATTR_ENTITY_ID) - if not attr_entity_id or ( + if not isinstance(attr_entity_id, str) or ( event_type in SCRIPT_AUTOMATION_EVENTS and attr_entity_id == entity_id ): return From 490776436739bdadc701094594cf10fe2133e63b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 May 2021 23:23:59 +0200 Subject: [PATCH 161/852] Remove YAML configuration from Daikin (#50080) --- homeassistant/components/daikin/__init__.py | 48 +++---------------- .../components/daikin/config_flow.py | 7 --- tests/components/daikin/test_config_flow.py | 23 +-------- 3 files changed, 7 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index fb38c38db0a..9d0d189248f 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -6,10 +6,9 @@ import logging from aiohttp import ClientConnectionError from async_timeout import timeout from pydaikin.daikin_base import Appliance -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_HOSTS, CONF_PASSWORD +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv @@ -25,52 +24,16 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) PLATFORMS = ["climate", "sensor", "switch"] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_HOSTS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ) - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Establish connection with Daikin.""" - if DOMAIN not in config: - return True - - hosts = config[DOMAIN][CONF_HOSTS] - if not hosts: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT} - ) - ) - for host in hosts: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_HOST: host} - ) - ) - return True +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Establish connection with Daikin.""" conf = entry.data # For backwards compat, set unique ID - if entry.unique_id is None: - hass.config_entries.async_update_entry(entry, unique_id=conf[KEY_MAC]) - elif ".local" in entry.unique_id: + if entry.unique_id is None or ".local" in entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=conf[KEY_MAC]) + daikin_api = await daikin_api_setup( hass, conf[CONF_HOST], @@ -80,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) if not daikin_api: return False + hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 9e980bed196..ea0709e5557 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -115,13 +115,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input.get(CONF_PASSWORD), ) - async def async_step_import(self, user_input): - """Import a config entry.""" - host = user_input.get(CONF_HOST) - if not host: - return await self.async_step_user() - return await self._create_device(host) - async def async_step_zeroconf(self, discovery_info): """Prepare configuration for a discovered Daikin device.""" _LOGGER.debug("Zeroconf user_input: %s", discovery_info) diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 076f9f54878..624268d7ee3 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -8,7 +8,7 @@ from aiohttp.web_exceptions import HTTPForbidden import pytest from homeassistant.components.daikin.const import KEY_MAC -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -80,27 +80,6 @@ async def test_abort_if_already_setup(hass, mock_daikin): assert result["reason"] == "already_configured" -async def test_import(hass, mock_daikin): - """Test import step.""" - result = await hass.config_entries.flow.async_init( - "daikin", - context={"source": SOURCE_IMPORT}, - data={}, - ) - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_init( - "daikin", - context={"source": SOURCE_IMPORT}, - data={CONF_HOST: HOST}, - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][KEY_MAC] == MAC - - @pytest.mark.parametrize( "s_effect,reason", [ From 34eb1627e0be5325cf13b92428781fa7f3c85da5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 May 2021 23:25:59 +0200 Subject: [PATCH 162/852] Remove deprecated LIFX Legacy integration (#50069) --- .coveragerc | 1 - .../components/lifx_legacy/__init__.py | 1 - homeassistant/components/lifx_legacy/light.py | 278 ------------------ .../components/lifx_legacy/manifest.json | 8 - requirements_all.txt | 3 - 5 files changed, 291 deletions(-) delete mode 100644 homeassistant/components/lifx_legacy/__init__.py delete mode 100644 homeassistant/components/lifx_legacy/light.py delete mode 100644 homeassistant/components/lifx_legacy/manifest.json diff --git a/.coveragerc b/.coveragerc index 6c210d986bc..977cf11a752 100644 --- a/.coveragerc +++ b/.coveragerc @@ -547,7 +547,6 @@ omit = homeassistant/components/life360/* homeassistant/components/lifx/* homeassistant/components/lifx_cloud/scene.py - homeassistant/components/lifx_legacy/light.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py homeassistant/components/linksys_smart/device_tracker.py diff --git a/homeassistant/components/lifx_legacy/__init__.py b/homeassistant/components/lifx_legacy/__init__.py deleted file mode 100644 index 83d5a0e5048..00000000000 --- a/homeassistant/components/lifx_legacy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The lifx_legacy component.""" diff --git a/homeassistant/components/lifx_legacy/light.py b/homeassistant/components/lifx_legacy/light.py deleted file mode 100644 index 795f3e17793..00000000000 --- a/homeassistant/components/lifx_legacy/light.py +++ /dev/null @@ -1,278 +0,0 @@ -""" -Support for the LIFX platform that implements lights. - -This is a legacy platform, included because the current lifx platform does -not yet support Windows. -""" -import logging - -import liffylights -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ATTR_HS_COLOR, - ATTR_TRANSITION, - PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_TRANSITION, - LightEntity, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_time_change -from homeassistant.util.color import ( - color_temperature_kelvin_to_mired, - color_temperature_mired_to_kelvin, -) - -_LOGGER = logging.getLogger(__name__) - -BYTE_MAX = 255 - -CONF_BROADCAST = "broadcast" -CONF_SERVER = "server" - -SHORT_MAX = 65535 - -TEMP_MAX = 9000 -TEMP_MAX_HASS = 500 -TEMP_MIN = 2500 -TEMP_MIN_HASS = 154 - -SUPPORT_LIFX = ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_TRANSITION -) - -PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_SERVER), - cv.deprecated(CONF_BROADCAST), - PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_SERVER): cv.string, vol.Optional(CONF_BROADCAST): cv.string} - ), -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the LIFX platform.""" - _LOGGER.warning( - "The LIFX Legacy platform is deprecated and will be removed in " - "Home Assistant Core 2021.6.0; Use the LIFX integration instead" - ) - - server_addr = config.get(CONF_SERVER) - broadcast_addr = config.get(CONF_BROADCAST) - - lifx_library = LIFX(add_entities, server_addr, broadcast_addr) - - # Register our poll service - track_time_change(hass, lifx_library.poll, second=[10, 40]) - - lifx_library.probe() - - -class LIFX: - """Representation of a LIFX light.""" - - def __init__(self, add_entities_callback, server_addr=None, broadcast_addr=None): - """Initialize the light.""" - self._devices = [] - - self._add_entities_callback = add_entities_callback - - self._liffylights = liffylights.LiffyLights( - self.on_device, self.on_power, self.on_color, server_addr, broadcast_addr - ) - - def find_bulb(self, ipaddr): - """Search for bulbs.""" - bulb = None - for device in self._devices: - if device.ipaddr == ipaddr: - bulb = device - break - return bulb - - def on_device(self, ipaddr, name, power, hue, sat, bri, kel): - """Initialize the light.""" - bulb = self.find_bulb(ipaddr) - - if bulb is None: - _LOGGER.debug( - "new bulb %s %s %d %d %d %d %d", ipaddr, name, power, hue, sat, bri, kel - ) - bulb = LIFXLight(self._liffylights, ipaddr, name, power, hue, sat, bri, kel) - self._devices.append(bulb) - self._add_entities_callback([bulb]) - else: - _LOGGER.debug( - "update bulb %s %s %d %d %d %d %d", - ipaddr, - name, - power, - hue, - sat, - bri, - kel, - ) - bulb.set_power(power) - bulb.set_color(hue, sat, bri, kel) - bulb.schedule_update_ha_state() - - def on_color(self, ipaddr, hue, sat, bri, kel): - """Initialize the light.""" - bulb = self.find_bulb(ipaddr) - - if bulb is not None: - bulb.set_color(hue, sat, bri, kel) - bulb.schedule_update_ha_state() - - def on_power(self, ipaddr, power): - """Initialize the light.""" - bulb = self.find_bulb(ipaddr) - - if bulb is not None: - bulb.set_power(power) - bulb.schedule_update_ha_state() - - def poll(self, now): - """Set up polling for the light.""" - self.probe() - - def probe(self, address=None): - """Probe the light.""" - self._liffylights.probe(address) - - -class LIFXLight(LightEntity): - """Representation of a LIFX light.""" - - def __init__(self, liffy, ipaddr, name, power, hue, saturation, brightness, kelvin): - """Initialize the light.""" - _LOGGER.debug("LIFXLight: %s %s", ipaddr, name) - - self._liffylights = liffy - self._ip = ipaddr - self.set_name(name) - self.set_power(power) - self.set_color(hue, saturation, brightness, kelvin) - - @property - def should_poll(self): - """No polling needed for LIFX light.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def ipaddr(self): - """Return the IP address of the device.""" - return self._ip - - @property - def hs_color(self): - """Return the hs value.""" - return (self._hue / 65535 * 360, self._sat / 65535 * 100) - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - brightness = int(self._bri / (BYTE_MAX + 1)) - _LOGGER.debug("brightness: %d", brightness) - return brightness - - @property - def color_temp(self): - """Return the color temperature.""" - temperature = color_temperature_kelvin_to_mired(self._kel) - - _LOGGER.debug("color_temp: %d", temperature) - return temperature - - @property - def is_on(self): - """Return true if device is on.""" - _LOGGER.debug("is_on: %d", self._power) - return self._power != 0 - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_LIFX - - def turn_on(self, **kwargs): - """Turn the device on.""" - if ATTR_TRANSITION in kwargs: - fade = int(kwargs[ATTR_TRANSITION] * 1000) - else: - fade = 0 - - if ATTR_HS_COLOR in kwargs: - hue, saturation = kwargs[ATTR_HS_COLOR] - hue = hue / 360 * 65535 - saturation = saturation / 100 * 65535 - else: - hue = self._hue - saturation = self._sat - - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1) - else: - brightness = self._bri - - if ATTR_COLOR_TEMP in kwargs: - kelvin = int(color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])) - else: - kelvin = self._kel - - _LOGGER.debug( - "turn_on: %s (%d) %d %d %d %d %d", - self._ip, - self._power, - hue, - saturation, - brightness, - kelvin, - fade, - ) - - if self._power == 0: - self._liffylights.set_color( - self._ip, hue, saturation, brightness, kelvin, 0 - ) - self._liffylights.set_power(self._ip, 65535, fade) - else: - self._liffylights.set_color( - self._ip, hue, saturation, brightness, kelvin, fade - ) - - def turn_off(self, **kwargs): - """Turn the device off.""" - if ATTR_TRANSITION in kwargs: - fade = int(kwargs[ATTR_TRANSITION] * 1000) - else: - fade = 0 - - _LOGGER.debug("turn_off: %s %d", self._ip, fade) - self._liffylights.set_power(self._ip, 0, fade) - - def set_name(self, name): - """Set name of the light.""" - self._name = name - - def set_power(self, power): - """Set power state value.""" - _LOGGER.debug("set_power: %d", power) - self._power = power != 0 - - def set_color(self, hue, sat, bri, kel): - """Set color state values.""" - self._hue = hue - self._sat = sat - self._bri = bri - self._kel = kel diff --git a/homeassistant/components/lifx_legacy/manifest.json b/homeassistant/components/lifx_legacy/manifest.json deleted file mode 100644 index 8bd5a471bf6..00000000000 --- a/homeassistant/components/lifx_legacy/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "lifx_legacy", - "name": "LIFX Legacy", - "documentation": "https://www.home-assistant.io/integrations/lifx_legacy", - "requirements": ["liffylights==0.9.4"], - "codeowners": [], - "iot_class": "local_push" -} diff --git a/requirements_all.txt b/requirements_all.txt index f810324678c..acfff356b16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -872,9 +872,6 @@ libsoundtouch==0.8 # homeassistant.components.life360 life360==4.1.1 -# homeassistant.components.lifx_legacy -liffylights==0.9.4 - # homeassistant.components.osramlightify lightify==1.0.7.3 From 2273bda44ae08eec7faceec82f249ebc409f2ec4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 May 2021 23:26:21 +0200 Subject: [PATCH 163/852] Deprecate Abode YAML configuration (#50075) --- homeassistant/components/abode/__init__.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 22e22efd82e..156dbae2804 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -45,15 +45,19 @@ ATTR_EVENT_BY = "event_by" ATTR_VALUE = "value" CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_POLLING, default=False): cv.boolean, - } - ) - }, + vol.All( + # Deprecated in Home Assistant 2021.6 + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_POLLING, default=False): cv.boolean, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From 4d939486a9ef9583b5ed45414ed4850adb4fe53f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 May 2021 23:26:48 +0200 Subject: [PATCH 164/852] Fix updating owner user/auth (#50087) Check if `is_active` is in update msg --- homeassistant/components/config/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index c1d43a5d4a9..54d992466f9 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -112,7 +112,7 @@ async def websocket_update(hass, connection, msg): ) return - if user.is_owner and msg["is_active"] is False: + if user.is_owner and msg.get("is_active") is False: connection.send_message( websocket_api.error_message( msg["id"], From 004fa63dbeaf3a2ce285e917c0986ec7174e8b19 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Tue, 4 May 2021 22:36:48 +0100 Subject: [PATCH 165/852] Use AddEntitiesCallback type, pt.3 (#49953) --- .../components/canary/alarm_control_panel.py | 6 ++---- homeassistant/components/canary/camera.py | 5 ++--- homeassistant/components/canary/sensor.py | 6 ++---- homeassistant/components/dynalite/cover.py | 6 ++++-- homeassistant/components/dynalite/dynalitebase.py | 3 ++- homeassistant/components/dynalite/light.py | 6 ++++-- homeassistant/components/dynalite/switch.py | 6 ++++-- homeassistant/components/elgato/light.py | 11 +++++++---- homeassistant/components/hyperion/light.py | 5 ++++- homeassistant/components/hyperion/switch.py | 7 +++++-- homeassistant/components/litterrobot/sensor.py | 6 ++---- homeassistant/components/litterrobot/switch.py | 6 +++--- homeassistant/components/litterrobot/vacuum.py | 6 +++--- .../components/rituals_perfume_genie/binary_sensor.py | 7 ++++--- .../components/rituals_perfume_genie/sensor.py | 7 ++++--- .../components/rituals_perfume_genie/switch.py | 7 +++++-- homeassistant/components/sma/sensor.py | 5 ++--- homeassistant/components/twentemilieu/sensor.py | 7 +++---- homeassistant/components/vera/binary_sensor.py | 6 ++---- homeassistant/components/vera/climate.py | 6 +++--- homeassistant/components/vera/cover.py | 6 +++--- homeassistant/components/vera/light.py | 6 +++--- homeassistant/components/vera/lock.py | 6 +++--- homeassistant/components/vera/scene.py | 6 +++--- homeassistant/components/vera/sensor.py | 6 +++--- homeassistant/components/vera/switch.py | 6 +++--- .../components/verisure/alarm_control_panel.py | 7 +++---- homeassistant/components/verisure/binary_sensor.py | 6 ++---- homeassistant/components/verisure/camera.py | 11 ++++++----- homeassistant/components/verisure/lock.py | 11 ++++++----- homeassistant/components/verisure/sensor.py | 6 ++---- homeassistant/components/verisure/switch.py | 7 +++---- homeassistant/components/vizio/media_player.py | 7 ++++--- homeassistant/components/waze_travel_time/sensor.py | 4 ++-- 34 files changed, 113 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 3e964c186fb..f0d6deb477b 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -1,8 +1,6 @@ """Support for Canary alarm.""" from __future__ import annotations -from typing import Callable - from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity @@ -19,7 +17,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN @@ -29,7 +27,7 @@ from .coordinator import CanaryDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Canary alarm control panels based on a config entry.""" coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 1ead5dcd44e..ada9d168942 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from datetime import timedelta -from typing import Callable from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame @@ -15,7 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import Throttle @@ -46,7 +45,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Canary sensors based on a config entry.""" coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 9da8ad42986..0378d34a989 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,8 +1,6 @@ """Support for Canary sensors.""" from __future__ import annotations -from typing import Callable - from canary.api import SensorType from homeassistant.components.sensor import SensorEntity @@ -17,7 +15,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER @@ -57,7 +55,7 @@ STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal" async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Canary sensors based on a config entry.""" coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ diff --git a/homeassistant/components/dynalite/cover.py b/homeassistant/components/dynalite/cover.py index 0673e07fc0d..3e6c738f066 100644 --- a/homeassistant/components/dynalite/cover.py +++ b/homeassistant/components/dynalite/cover.py @@ -1,5 +1,4 @@ """Support for the Dynalite channels as covers.""" -from typing import Callable from homeassistant.components.cover import ( DEVICE_CLASS_SHUTTER, @@ -8,6 +7,7 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .dynalitebase import DynaliteBase, async_setup_entry_base @@ -15,7 +15,9 @@ DEFAULT_COVER_CLASS = DEVICE_CLASS_SHUTTER async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Record the async_add_entities function to add them later when received from Dynalite.""" diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index 371f2aa8508..ebb1dd23795 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -8,6 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, LOGGER @@ -15,7 +16,7 @@ from .const import DOMAIN, LOGGER def async_setup_entry_base( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable, + async_add_entities: AddEntitiesCallback, platform: str, entity_from_device: Callable, ) -> None: diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index bb9569358be..ee91df1ae98 100644 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -1,15 +1,17 @@ """Support for Dynalite channels as lights.""" -from typing import Callable from homeassistant.components.light import SUPPORT_BRIGHTNESS, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .dynalitebase import DynaliteBase, async_setup_entry_base async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Record the async_add_entities function to add them later when received from Dynalite.""" async_setup_entry_base( diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py index a482228183c..c98a1ce0ec4 100644 --- a/homeassistant/components/dynalite/switch.py +++ b/homeassistant/components/dynalite/switch.py @@ -1,15 +1,17 @@ """Support for the Dynalite channels and presets as switches.""" -from typing import Callable from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .dynalitebase import DynaliteBase, async_setup_entry_base async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Record the async_add_entities function to add them later when received from Dynalite.""" async_setup_entry_base( diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 7ec21dd9f20..a81f5f214ba 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, Callable +from typing import Any from elgato import Elgato, ElgatoError, Info, State @@ -23,8 +23,11 @@ from homeassistant.const import ( ATTR_SW_VERSION, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.entity_platform import async_get_current_platform +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from .const import DATA_ELGATO_CLIENT, DOMAIN, SERVICE_IDENTIFY @@ -37,7 +40,7 @@ SCAN_INTERVAL = timedelta(seconds=10) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Elgato Key Light based on a config entry.""" elgato: Elgato = hass.data[DOMAIN][entry.entry_id][DATA_ELGATO_CLIENT] diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index f8a384731b3..63e90a89068 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -25,6 +25,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from . import ( @@ -81,7 +82,9 @@ ICON_EXTERNAL_SOURCE = "mdi:television-ambient-light" async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> bool: """Set up a Hyperion platform from config entry.""" diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index e24a6ac7dda..d36fcd79836 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -2,7 +2,7 @@ from __future__ import annotations import functools -from typing import Any, Callable +from typing import Any from hyperion import client from hyperion.const import ( @@ -32,6 +32,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from . import ( @@ -82,7 +83,9 @@ def _component_to_switch_name(component: str, instance_name: str) -> str: async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> bool: """Set up a Hyperion platform from config entry.""" entry_data = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 022a372ac68..15ea68f8342 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,15 +1,13 @@ """Support for Litter-Robot sensors.""" from __future__ import annotations -from typing import Callable - from pylitterbot.robot import Robot from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .entity import LitterRobotEntity @@ -83,7 +81,7 @@ ROBOT_SENSORS: list[tuple[type[LitterRobotPropertySensor], str, str]] = [ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot sensors using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 2896458acff..3c4e4bb9937 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -1,12 +1,12 @@ """Support for Litter-Robot switches.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .entity import LitterRobotControlEntity @@ -66,7 +66,7 @@ ROBOT_SWITCHES: list[tuple[type[LitterRobotControlEntity], str]] = [ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot switches using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index fc398e4ad12..bd5fb9b92df 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,7 +1,7 @@ """Support for Litter-Robot "Vacuum".""" from __future__ import annotations -from typing import Any, Callable +from typing import Any from pylitterbot.enums import LitterBoxStatus from pylitterbot.robot import VALID_WAIT_TIMES @@ -23,7 +23,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .entity import LitterRobotControlEntity @@ -42,7 +42,7 @@ SERVICE_SET_WAIT_TIME = "set_wait_time" async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index ffeceb079bb..2d82982388d 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -1,8 +1,6 @@ """Support for Rituals Perfume Genie binary sensors.""" from __future__ import annotations -from typing import Callable - from pyrituals import Diffuser from homeassistant.components.binary_sensor import ( @@ -11,6 +9,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator from .const import COORDINATORS, DEVICES, DOMAIN @@ -21,7 +20,9 @@ BATTERY_CHARGING_ID = 21 async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the diffuser binary sensors.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 5982678e9c6..31a04bb5b8f 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -1,8 +1,6 @@ """Support for Rituals Perfume Genie sensors.""" from __future__ import annotations -from typing import Callable - from pyrituals import Diffuser from homeassistant.config_entries import ConfigEntry @@ -12,6 +10,7 @@ from homeassistant.const import ( PERCENTAGE, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator from .const import COORDINATORS, DEVICES, DOMAIN, ID, SENSORS @@ -35,7 +34,9 @@ ATTR_SIGNAL_STRENGTH = "signal_strength" async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the diffuser sensors.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index 83e392f7bad..a2ca89dc2ac 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -1,13 +1,14 @@ """Support for Rituals Perfume Genie switches.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any from pyrituals import Diffuser from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN @@ -21,7 +22,9 @@ ON_STATE = "1" async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the diffuser switch.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index cc5c92d2159..04bfb7644a3 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -1,9 +1,8 @@ """SMA Solar Webconnect interface.""" from __future__ import annotations -from collections.abc import Coroutine import logging -from typing import Any, Callable +from typing import Any import pysma import voluptuous as vol @@ -118,7 +117,7 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[], Coroutine], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up SMA sensors.""" sma_data = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 29a3012c29f..c0174263d23 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -1,8 +1,6 @@ """Support for Twente Milieu sensors.""" from __future__ import annotations -from typing import Callable - from twentemilieu import ( WASTE_TYPE_NON_RECYCLABLE, WASTE_TYPE_ORGANIC, @@ -18,7 +16,8 @@ from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_UPDATE, DOMAIN @@ -28,7 +27,7 @@ PARALLEL_UPDATES = 1 async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Twente Milieu sensor based on a config entry.""" twentemilieu = hass.data[DOMAIN][entry.data[CONF_ID]] diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 816234bb602..1eca4f208f4 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -1,8 +1,6 @@ """Support for Vera binary sensors.""" from __future__ import annotations -from typing import Callable - import pyvera as veraApi from homeassistant.components.binary_sensor import ( @@ -12,7 +10,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VeraDevice from .common import ControllerData, get_controller_data @@ -21,7 +19,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 5027becb71f..c4c7cae8f85 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -1,7 +1,7 @@ """Support for Vera thermostats.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi @@ -23,7 +23,7 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import convert from . import VeraDevice @@ -38,7 +38,7 @@ SUPPORT_HVAC = [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_O async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index cf3dd4a3d13..248465ed842 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -1,7 +1,7 @@ """Support for Vera cover - curtains, rollershutters etc.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi @@ -13,7 +13,7 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VeraDevice from .common import ControllerData, get_controller_data @@ -22,7 +22,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index 7fcb726efcc..5cc19d78164 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -1,7 +1,7 @@ """Support for Vera lights.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi @@ -16,7 +16,7 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from . import VeraDevice @@ -26,7 +26,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index eada4b20550..2c656cba5de 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -1,7 +1,7 @@ """Support for Vera locks.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi @@ -13,7 +13,7 @@ from homeassistant.components.lock import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VeraDevice from .common import ControllerData, get_controller_data @@ -25,7 +25,7 @@ ATTR_LOW_BATTERY = "low_battery" async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index c6eb983a8f7..543ba3b517b 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -1,14 +1,14 @@ """Support for Vera scenes.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from .common import ControllerData, get_controller_data @@ -18,7 +18,7 @@ from .const import VERA_ID_FORMAT async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 516801b57c6..e1e95820a2e 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta -from typing import Callable, cast +from typing import cast import pyvera as veraApi @@ -14,7 +14,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import convert from . import VeraDevice @@ -26,7 +26,7 @@ SCAN_INTERVAL = timedelta(seconds=5) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index c779a3c8cfc..7531819d90d 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -1,7 +1,7 @@ """Support for Vera switches.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi @@ -12,7 +12,7 @@ from homeassistant.components.switch import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import convert from . import VeraDevice @@ -22,7 +22,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 0367a8d81b1..64ee024dfd7 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -2,8 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable -from typing import Callable from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -15,7 +13,8 @@ from homeassistant.components.alarm_control_panel.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ALARM_STATE_TO_HA, CONF_GIID, DOMAIN, LOGGER @@ -25,7 +24,7 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure alarm control panel from a config entry.""" async_add_entities([VerisureAlarm(coordinator=hass.data[DOMAIN][entry.entry_id])]) diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 7f61eb194fa..ab79c6f45fe 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -1,9 +1,6 @@ """Support for Verisure binary sensors.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Callable - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_OPENING, @@ -12,6 +9,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DOMAIN @@ -21,7 +19,7 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure binary sensors based on a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index bf244188a5e..f52a7a38e59 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -1,10 +1,8 @@ """Support for Verisure cameras.""" from __future__ import annotations -from collections.abc import Iterable import errno import os -from typing import Callable from verisure import Error as VerisureError @@ -12,8 +10,11 @@ from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.entity_platform import async_get_current_platform +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DOMAIN, LOGGER, SERVICE_CAPTURE_SMARTCAM @@ -23,7 +24,7 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure sensors based on a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 9c41053a34e..c33ccda208a 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -2,8 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable -from typing import Callable from verisure import Error as VerisureError @@ -11,8 +9,11 @@ from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.entity_platform import async_get_current_platform +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -31,7 +32,7 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure alarm control panel from a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index ac79cc16091..028e5877e51 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -1,9 +1,6 @@ """Support for Verisure sensors.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Callable - from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -13,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DEVICE_TYPE_NAME, DOMAIN @@ -22,7 +20,7 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure sensors based on a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 85be983e44a..97b2ff0186a 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -1,14 +1,13 @@ """Support for Verisure Smartplugs.""" from __future__ import annotations -from collections.abc import Iterable from time import monotonic -from typing import Callable from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DOMAIN @@ -18,7 +17,7 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure alarm control panel from a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 3e8f0044759..f5071ce146a 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, Callable +from typing import Any from pyvizio import VizioAsync from pyvizio.api.apps import find_app_name @@ -33,7 +33,8 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -65,7 +66,7 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Vizio media player entry.""" host = config_entry.data[CONF_HOST] diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 4277d0bffb5..bd8e41dc31c 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import timedelta import logging import re -from typing import Callable from WazeRouteCalculator import WazeRouteCalculator, WRCError import voluptuous as vol @@ -24,6 +23,7 @@ from homeassistant.const import ( from homeassistant.core import Config, CoreState, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_DESTINATION, @@ -112,7 +112,7 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[SensorEntity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Waze travel time sensor entry.""" defaults = { From 13a27eec9073e77a16f0162c27af66c030c75c9e Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 4 May 2021 23:45:25 +0200 Subject: [PATCH 166/852] Fix KNX climate unque_id (#50054) --- homeassistant/components/knx/climate.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index de9b71a2287..aec587c9d0e 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -67,7 +67,24 @@ def _async_migrate_unique_id( ga_target_temperature_state = parse_device_group_address( entity_config[ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS][0] ) - new_uid = f"{ga_temperature_state}_{ga_target_temperature_state}" + target_temp = entity_config.get(ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS) + ga_target_temperature = ( + parse_device_group_address(target_temp[0]) + if target_temp is not None + else None + ) + setpoint_shift = entity_config.get(ClimateSchema.CONF_SETPOINT_SHIFT_ADDRESS) + ga_setpoint_shift = ( + parse_device_group_address(setpoint_shift[0]) + if setpoint_shift is not None + else None + ) + new_uid = ( + f"{ga_temperature_state}_" + f"{ga_target_temperature_state}_" + f"{ga_target_temperature}_" + f"{ga_setpoint_shift}" + ) entity_registry.async_update_entity(entity_id, new_unique_id=new_uid) @@ -80,7 +97,9 @@ class KNXClimate(KnxEntity, ClimateEntity): super().__init__(device) self._unique_id = ( f"{device.temperature.group_address_state}_" - f"{device.target_temperature.group_address_state}" + f"{device.target_temperature.group_address_state}_" + f"{device.target_temperature.group_address}_" + f"{device._setpoint_shift.group_address}" # pylint: disable=protected-access ) self._unit_of_measurement = TEMP_CELSIUS From 98edd58c558696a4cb38780446dac9ff81124214 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 May 2021 00:11:03 +0200 Subject: [PATCH 167/852] Update frontend to 20210504.0 (#50093) --- 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 bfd602471a7..65927e6da0c 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==20210503.0" + "home-assistant-frontend==20210504.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0d9cbe7990b..67c8436f67a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210503.0 +home-assistant-frontend==20210504.0 httpx==0.18.0 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index acfff356b16..91408b5f954 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210503.0 +home-assistant-frontend==20210504.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42908397592..ba5c9a7215d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210503.0 +home-assistant-frontend==20210504.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 2f89ba24b6682a60b2383f851a44f8d83c5c7367 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 5 May 2021 00:29:18 +0200 Subject: [PATCH 168/852] Cleanup modbus binary_sensor signature (#49809) Co-authored-by: Martin Hjelmare --- .../components/modbus/binary_sensor.py | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index d04938c929a..82b1db6dd1a 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -91,17 +91,7 @@ async def async_setup_platform( hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] if CONF_SCAN_INTERVAL not in entry: entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL - sensors.append( - ModbusBinarySensor( - hub, - entry[CONF_NAME], - entry.get(CONF_SLAVE), - entry[CONF_ADDRESS], - entry.get(CONF_DEVICE_CLASS), - entry[CONF_INPUT_TYPE], - entry[CONF_SCAN_INTERVAL], - ) - ) + sensors.append(ModbusBinarySensor(hub, hass, entry)) async_add_entities(sensors) @@ -109,24 +99,23 @@ async def async_setup_platform( class ModbusBinarySensor(BinarySensorEntity): """Modbus binary sensor.""" - def __init__( - self, hub, name, slave, address, device_class, input_type, scan_interval - ): + def __init__(self, hub, hass, entry): """Initialize the Modbus binary sensor.""" self._hub = hub - self._name = name - self._slave = int(slave) if slave else None - self._address = int(address) - self._device_class = device_class - self._input_type = input_type + self._hass = hass + self._name = entry[CONF_NAME] + self._slave = entry.get(CONF_SLAVE) + self._address = int(entry[CONF_ADDRESS]) + self._device_class = entry.get(CONF_DEVICE_CLASS) + self._input_type = entry[CONF_INPUT_TYPE] self._value = None self._available = True - self._scan_interval = timedelta(seconds=scan_interval) + self._scan_interval = timedelta(seconds=entry[CONF_SCAN_INTERVAL]) async def async_added_to_hass(self): """Handle entity which will be added.""" async_track_time_interval( - self.hass, lambda arg: self.update(), self._scan_interval + self._hass, lambda arg: self.update(), self._scan_interval ) @property From 469d9123fe01285f785b5f14a9066c7a648172df Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 5 May 2021 00:04:27 +0000 Subject: [PATCH 169/852] [ci skip] Translation update --- .../buienradar/translations/ca.json | 29 +++++++++++++++++++ .../buienradar/translations/et.json | 29 +++++++++++++++++++ .../buienradar/translations/nl.json | 29 +++++++++++++++++++ .../buienradar/translations/no.json | 29 +++++++++++++++++++ .../buienradar/translations/ru.json | 29 +++++++++++++++++++ .../components/flume/translations/nl.json | 10 ++++++- .../components/flume/translations/no.json | 10 ++++++- .../flume/translations/zh-Hant.json | 10 ++++++- .../components/fritzbox/translations/no.json | 2 +- 9 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/buienradar/translations/ca.json create mode 100644 homeassistant/components/buienradar/translations/et.json create mode 100644 homeassistant/components/buienradar/translations/nl.json create mode 100644 homeassistant/components/buienradar/translations/no.json create mode 100644 homeassistant/components/buienradar/translations/ru.json diff --git a/homeassistant/components/buienradar/translations/ca.json b/homeassistant/components/buienradar/translations/ca.json new file mode 100644 index 00000000000..22a029a61b5 --- /dev/null +++ b/homeassistant/components/buienradar/translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "error": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Codi de pa\u00eds a mostrar en les imatges.", + "delta": "Interval de temps entre actualitzacions de la imatge, en segons", + "timeframe": "Nombre de minuts d'antelaci\u00f3 per revisar la previsi\u00f3 de precipitacions" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/et.json b/homeassistant/components/buienradar/translations/et.json new file mode 100644 index 00000000000..4be9c850309 --- /dev/null +++ b/homeassistant/components/buienradar/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Asukoht on juba kasutusel" + }, + "error": { + "already_configured": "Asukoht on juba kasutusel" + }, + "step": { + "user": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Riigi kood kaamerapiltide kuvamiseks.", + "delta": "Ajavahemik sekundites kaamera pildi v\u00e4rskenduste vahel", + "timeframe": "Sademete prognoosi vaatamise minutid" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/nl.json b/homeassistant/components/buienradar/translations/nl.json new file mode 100644 index 00000000000..0d022f1c61e --- /dev/null +++ b/homeassistant/components/buienradar/translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "error": { + "already_configured": "Locatie is al geconfigureerd." + }, + "step": { + "user": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Landcode van het land om camerabeelden weer te geven.", + "delta": "Tijdsinterval in seconden tussen updates van camerabeelden", + "timeframe": "Minuten om vooruit te kijken voor neerslagvoorspelling" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/no.json b/homeassistant/components/buienradar/translations/no.json new file mode 100644 index 00000000000..68ab2cb00e1 --- /dev/null +++ b/homeassistant/components/buienradar/translations/no.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "error": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "latitude": "Breddegrad", + "longitude": "Lengdegrad" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Landskoden til landet for \u00e5 vise kamerabilder.", + "delta": "Tidsintervall i sekunder mellom kamerabildeoppdateringer", + "timeframe": "Minutter for \u00e5 se fremover for nedb\u00f8rsvarsel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/ru.json b/homeassistant/components/buienradar/translations/ru.json new file mode 100644 index 00000000000..cda79c91def --- /dev/null +++ b/homeassistant/components/buienradar/translations/ru.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439 \u0441 \u043a\u0430\u043c\u0435\u0440\u044b", + "delta": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043c\u0435\u0436\u0434\u0443 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\u043c\u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0441 \u043a\u0430\u043c\u0435\u0440\u044b (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "timeframe": "\u041e\u0436\u0438\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043e\u0441\u0430\u0434\u043a\u043e\u0432 (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/nl.json b/homeassistant/components/flume/translations/nl.json index 97daf42d11e..de0e225be03 100644 --- a/homeassistant/components/flume/translations/nl.json +++ b/homeassistant/components/flume/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,13 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Het wachtwoord voor {username} is niet meer geldig.", + "title": "Verifieer uw Flume account opnieuw" + }, "user": { "data": { "client_id": "Client-id", diff --git a/homeassistant/components/flume/translations/no.json b/homeassistant/components/flume/translations/no.json index 5f473bfdfef..aeda0eae271 100644 --- a/homeassistant/components/flume/translations/no.json +++ b/homeassistant/components/flume/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -9,6 +10,13 @@ "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Passordet for {username} er ikke lenger gyldig.", + "title": "Godkjenn Flume-kontoen din p\u00e5 nytt" + }, "user": { "data": { "client_id": "Klient ID", diff --git a/homeassistant/components/flume/translations/zh-Hant.json b/homeassistant/components/flume/translations/zh-Hant.json index 7a585b1b618..9aae3792609 100644 --- a/homeassistant/components/flume/translations/zh-Hant.json +++ b/homeassistant/components/flume/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,6 +10,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "{username} \u5bc6\u78bc\u4e0d\u518d\u6709\u6548\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49 Flume \u5e33\u865f" + }, "user": { "data": { "client_id": "\u5ba2\u6236\u7aef ID", diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json index bd64b428bdf..9a64c5b8506 100644 --- a/homeassistant/components/fritzbox/translations/no.json +++ b/homeassistant/components/fritzbox/translations/no.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "", + "flow_title": "AVM FRITZ! SmartHome: {name}", "step": { "confirm": { "data": { From cc21de569d6117fe7a28109bcf47b73c8ab42513 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 07:27:23 +0200 Subject: [PATCH 170/852] Upgrade yamllint to 1.26.1 (#50060) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffe394683ae..cd850ef45f6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,7 @@ repos: - --branch=master - --branch=rc - repo: https://github.com/adrienverge/yamllint.git - rev: v1.24.2 + rev: v1.26.1 hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index b5dac41af36..cff9387e287 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -13,4 +13,4 @@ pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 pyupgrade==2.12.0 -yamllint==1.24.2 +yamllint==1.26.1 From 301d642ad8ef531f70a4083ead6215cb2aae407f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 07:56:50 +0200 Subject: [PATCH 171/852] Clean up deprecation message & config schema from Cloudflare (#50079) --- .../components/cloudflare/__init__.py | 43 ++----------------- tests/components/cloudflare/__init__.py | 7 --- .../components/cloudflare/test_config_flow.py | 4 +- 3 files changed, 4 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index abef32a4c5c..e461e34c9a2 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -10,11 +10,9 @@ from pycfdns.exceptions import ( CloudflareConnectionException, CloudflareException, ) -import voluptuous as vol -from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_EMAIL, CONF_ZONE +from homeassistant.const import CONF_API_TOKEN, CONF_ZONE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -31,43 +29,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.deprecated(CONF_EMAIL), - cv.deprecated(CONF_API_KEY), - cv.deprecated(CONF_ZONE), - cv.deprecated(CONF_RECORDS), - vol.Schema( - { - vol.Optional(CONF_EMAIL): cv.string, - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_ZONE): cv.string, - vol.Optional(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]), - } - ), - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the component.""" - hass.data.setdefault(DOMAIN, {}) - - if len(hass.config_entries.async_entries(DOMAIN)) > 0: - return True - - if DOMAIN in config and CONF_API_KEY in config[DOMAIN]: - persistent_notification.async_create( - hass, - "Cloudflare integration now requires an API Token. Please go to the integrations page to setup.", - "Cloudflare Setup", - "cloudflare_setup", - ) - - return True +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -104,6 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval = timedelta(minutes=DEFAULT_UPDATE_INTERVAL) undo_interval = async_track_time_interval(hass, update_records, update_interval) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_UNDO_UPDATE_INTERVAL: undo_interval, } diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py index 60ce0f055d5..0e4e07b91cc 100644 --- a/tests/components/cloudflare/__init__.py +++ b/tests/components/cloudflare/__init__.py @@ -88,13 +88,6 @@ def _get_mock_cfupdate( return client -def _patch_async_setup(return_value=True): - return patch( - "homeassistant.components.cloudflare.async_setup", - return_value=return_value, - ) - - def _patch_async_setup_entry(return_value=True): return patch( "homeassistant.components.cloudflare.async_setup_entry", diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index abdd69269f5..00dbb5e47df 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -20,7 +20,6 @@ from . import ( USER_INPUT, USER_INPUT_RECORDS, USER_INPUT_ZONE, - _patch_async_setup, _patch_async_setup_entry, ) @@ -58,7 +57,7 @@ async def test_user_form(hass, cfupdate_flow): assert result["step_id"] == "records" assert result["errors"] == {} - with _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + with _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT_RECORDS, @@ -76,7 +75,6 @@ async def test_user_form(hass, cfupdate_flow): assert result["result"] assert result["result"].unique_id == USER_INPUT_ZONE[CONF_ZONE] - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From b0eb2afa65eefd7b428fc9a8085ee422cac4c561 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 4 May 2021 22:58:20 -0700 Subject: [PATCH 172/852] Bump motioneye-client to v0.3.6 . (#50096) --- homeassistant/components/motioneye/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index a4a1e028d53..43cb231c30c 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -4,10 +4,10 @@ "documentation": "https://www.home-assistant.io/integrations/motioneye", "config_flow": true, "requirements": [ - "motioneye-client==0.3.2" + "motioneye-client==0.3.6" ], "codeowners": [ "@dermotduffy" ], "iot_class": "local_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 91408b5f954..cf519b9d0bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -951,7 +951,7 @@ mitemp_bt==0.0.3 motionblinds==0.4.10 # homeassistant.components.motioneye -motioneye-client==0.3.2 +motioneye-client==0.3.6 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba5c9a7215d..622becd3a34 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -514,7 +514,7 @@ minio==4.0.9 motionblinds==0.4.10 # homeassistant.components.motioneye -motioneye-client==0.3.2 +motioneye-client==0.3.6 # homeassistant.components.mullvad mullvad-api==1.0.0 From 44383f25ce45a9e059b963b970b8eadc5db225f5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 08:56:50 +0200 Subject: [PATCH 173/852] Clean up pylint comments (#49334) --- homeassistant/components/notify/__init__.py | 1 - homeassistant/data_entry_flow.py | 4 +++- homeassistant/helpers/entity.py | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 118579fb0c0..41953ddfc75 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -116,7 +116,6 @@ class BaseNotificationService: # While not purely typed, it makes typehinting more useful for us # and removes the need for constant None checks or asserts. - # Ignore types: https://github.com/PyCQA/pylint/issues/3167 hass: HomeAssistant = None # type: ignore # Name => target diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index f442e4662c6..720711e07e3 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -307,7 +307,9 @@ class FlowHandler: # Set by flow manager cur_step: dict[str, str] | None = None - # Ignore types: https://github.com/PyCQA/pylint/issues/3167 + + # While not purely typed, it makes typehinting more useful for us + # and removes the need for constant None checks or asserts. flow_id: str = None # type: ignore hass: HomeAssistant = None # type: ignore handler: str = None # type: ignore diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index dd6d1836857..d9b3b28a9ef 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -138,7 +138,6 @@ class Entity(ABC): # Owning hass instance. Will be set by EntityPlatform # While not purely typed, it makes typehinting more useful for us # and removes the need for constant None checks or asserts. - # Ignore types: https://github.com/PyCQA/pylint/issues/3167 hass: HomeAssistant = None # type: ignore # Owning platform instance. Will be set by EntityPlatform From 2b461073ff6b24cf162d6a266758e92b8b0f3b3c Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Wed, 5 May 2021 09:05:46 +0200 Subject: [PATCH 174/852] Improve buienradar tests (#50101) --- tests/components/buienradar/test_init.py | 8 +++--- tests/components/buienradar/test_sensor.py | 30 +++++++++++++-------- tests/components/buienradar/test_weather.py | 2 +- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/tests/components/buienradar/test_init.py b/tests/components/buienradar/test_init.py index e3ac8c025e1..ea91291a3de 100644 --- a/tests/components/buienradar/test_init.py +++ b/tests/components/buienradar/test_init.py @@ -1,7 +1,7 @@ """Tests for the buienradar component.""" from unittest.mock import patch -from homeassistant.components.buienradar import async_setup +from homeassistant import setup from homeassistant.components.buienradar.const import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers.entity_registry import async_get_registry @@ -30,7 +30,7 @@ async def test_import_all(hass): with patch( "homeassistant.components.buienradar.async_setup_entry", return_value=True ): - await async_setup(hass, config) + await setup.async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() conf_entries = hass.config_entries.async_entries(DOMAIN) @@ -68,7 +68,7 @@ async def test_import_camera(hass): with patch( "homeassistant.components.buienradar.async_setup_entry", return_value=True ): - await async_setup(hass, config) + await setup.async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() conf_entries = hass.config_entries.async_entries(DOMAIN) @@ -97,7 +97,7 @@ async def test_import_camera(hass): assert entity.original_name == "test_name" -async def test_load_unload(hass): +async def test_load_unload(aioclient_mock, hass): """Test options flow.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py index f0a24b6beb3..2b381f980d7 100644 --- a/tests/components/buienradar/test_sensor.py +++ b/tests/components/buienradar/test_sensor.py @@ -1,29 +1,37 @@ """The tests for the Buienradar sensor platform.""" -from unittest.mock import patch - from homeassistant.components.buienradar.const import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.entity_registry import async_get from tests.common import MockConfigEntry +TEST_LONGITUDE = 51.5288504 +TEST_LATITUDE = 5.4002156 + CONDITIONS = ["stationname", "temperature"] -TEST_CFG_DATA = {CONF_LATITUDE: 51.5288504, CONF_LONGITUDE: 5.4002156} +TEST_CFG_DATA = {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE} -async def test_smoke_test_setup_component(hass): +async def test_smoke_test_setup_component(aioclient_mock, hass): """Smoke test for successfully set-up with default config.""" mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) mock_entry.add_to_hass(hass) - with patch( - "homeassistant.components.buienradar.sensor.BrSensor.entity_registry_enabled_default" - ) as enabled_by_default_mock: - enabled_by_default_mock.return_value = True + entity_registry = async_get(hass) + for cond in CONDITIONS: + entity_registry.async_get_or_create( + domain="sensor", + platform="buienradar", + unique_id=f"{TEST_LATITUDE:2.6f}{TEST_LONGITUDE:2.6f}{cond}", + config_entry=mock_entry, + original_name=f"Buienradar {cond}", + ) + await hass.async_block_till_done() - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() for cond in CONDITIONS: - state = hass.states.get(f"sensor.buienradar_{cond}") + state = hass.states.get(f"sensor.buienradar_5_40021651_528850{cond}") assert state.state == "unknown" diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py index 9d16b531ad0..81fd3f4fcb4 100644 --- a/tests/components/buienradar/test_weather.py +++ b/tests/components/buienradar/test_weather.py @@ -7,7 +7,7 @@ from tests.common import MockConfigEntry TEST_CFG_DATA = {CONF_LATITUDE: 51.5288504, CONF_LONGITUDE: 5.4002156} -async def test_smoke_test_setup_component(hass): +async def test_smoke_test_setup_component(aioclient_mock, hass): """Smoke test for successfully set-up with default config.""" mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) From 373236e588a4d5de16c9b9444bef51d59b4eff45 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 09:09:34 +0200 Subject: [PATCH 175/852] Upgrade black to 21.5b0 (#50102) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd850ef45f6..af6984435dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 21.4b2 + rev: 21.5b0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index cff9387e287..672347a2269 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.4b2 +black==21.5b0 codespell==2.0.0 flake8-comprehensions==3.4.0 flake8-docstrings==1.6.0 From f88eea5275897212c6dcd4348b729fa2874fea40 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 09:12:35 +0200 Subject: [PATCH 176/852] Upgrade luftdaten to 0.6.5 (#50103) --- homeassistant/components/luftdaten/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index dad6a1a6934..f296093b556 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -3,7 +3,7 @@ "name": "Luftdaten", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/luftdaten", - "requirements": ["luftdaten==0.6.4"], + "requirements": ["luftdaten==0.6.5"], "codeowners": ["@fabaff"], "quality_scale": "gold", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index cf519b9d0bd..13b693d7480 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -897,7 +897,7 @@ logi_circle==0.2.2 london-tube-status==0.2 # homeassistant.components.luftdaten -luftdaten==0.6.4 +luftdaten==0.6.5 # homeassistant.components.lupusec lupupy==0.0.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 622becd3a34..274f5b1d75d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -487,7 +487,7 @@ libsoundtouch==0.8 logi_circle==0.2.2 # homeassistant.components.luftdaten -luftdaten==0.6.4 +luftdaten==0.6.5 # homeassistant.components.maxcube maxcube-api==0.4.3 From 219ad5cd9e7b3b3d6380f96a70f13bf92ea9b14f Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Wed, 5 May 2021 09:19:51 +0200 Subject: [PATCH 177/852] Fix fitbit RuntimeError: I/O must be done in the executor (#50058) --- homeassistant/components/fitbit/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 8571d31bc8a..263ae24ff34 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -346,7 +346,7 @@ class FitbitAuthCallbackView(HomeAssistantView): self.oauth = oauth @callback - def get(self, request): + async def get(self, request): """Finish OAuth callback request.""" hass = request.app["hass"] data = request.query @@ -359,7 +359,9 @@ class FitbitAuthCallbackView(HomeAssistantView): redirect_uri = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" try: - result = self.oauth.fetch_access_token(data.get("code"), redirect_uri) + result = await hass.async_add_executor_job( + self.oauth.fetch_access_token, data.get("code"), redirect_uri + ) except MissingTokenError as error: _LOGGER.error("Missing token: %s", error) response_message = f"""Something went wrong when From b7cd75b1342c1109d34baa7ccbf3d25e22a3bc32 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 09:28:47 +0200 Subject: [PATCH 178/852] Upgrade pyupgrade to v2.14.0 (#50059) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af6984435dd..15e723feb26 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.12.0 + rev: v2.14.0 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 672347a2269..4b08322b43b 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 -pyupgrade==2.12.0 +pyupgrade==2.14.0 yamllint==1.26.1 From 26b5a067bd03e2361e933ac9e2567d15ec394635 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 09:51:05 +0200 Subject: [PATCH 179/852] Remove YAML configuration from Verisure (#50076) --- homeassistant/components/verisure/__init__.py | 93 +-------- .../components/verisure/config_flow.py | 31 --- homeassistant/components/verisure/const.py | 4 - tests/components/verisure/test_config_flow.py | 195 ------------------ 4 files changed, 4 insertions(+), 319 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index f61208309fc..8abb3e59a9f 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -3,9 +3,6 @@ from __future__ import annotations from contextlib import suppress import os -from typing import Any - -import voluptuous as vol from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, @@ -15,27 +12,14 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_EMAIL, - CONF_PASSWORD, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR -from .const import ( - CONF_CODE_DIGITS, - CONF_DEFAULT_LOCK_CODE, - CONF_GIID, - CONF_LOCK_CODE_DIGITS, - CONF_LOCK_DEFAULT_CODE, - DEFAULT_LOCK_CODE_DIGITS, - DOMAIN, -) +from .const import DOMAIN from .coordinator import VerisureDataUpdateCoordinator PLATFORMS = [ @@ -47,80 +31,11 @@ PLATFORMS = [ SWITCH_DOMAIN, ] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_CODE_DIGITS): cv.positive_int, - vol.Optional(CONF_GIID): cv.string, - vol.Optional(CONF_DEFAULT_LOCK_CODE): cv.string, - }, - extra=vol.ALLOW_EXTRA, - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: - """Set up the Verisure integration.""" - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_EMAIL: config[DOMAIN][CONF_USERNAME], - CONF_PASSWORD: config[DOMAIN][CONF_PASSWORD], - CONF_GIID: config[DOMAIN].get(CONF_GIID), - CONF_LOCK_CODE_DIGITS: config[DOMAIN].get(CONF_CODE_DIGITS), - CONF_LOCK_DEFAULT_CODE: config[DOMAIN].get(CONF_LOCK_DEFAULT_CODE), - }, - ) - ) - - return True +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Verisure from a config entry.""" - # Migrate old YAML settings (hidden in the config entry), - # to config entry options. Can be removed after YAML support is gone. - if CONF_LOCK_CODE_DIGITS in entry.data or CONF_DEFAULT_LOCK_CODE in entry.data: - options = entry.options.copy() - - if ( - CONF_LOCK_CODE_DIGITS in entry.data - and CONF_LOCK_CODE_DIGITS not in entry.options - and entry.data[CONF_LOCK_CODE_DIGITS] != DEFAULT_LOCK_CODE_DIGITS - ): - options.update( - { - CONF_LOCK_CODE_DIGITS: entry.data[CONF_LOCK_CODE_DIGITS], - } - ) - - if ( - CONF_DEFAULT_LOCK_CODE in entry.data - and CONF_DEFAULT_LOCK_CODE not in entry.options - ): - options.update( - { - CONF_DEFAULT_LOCK_CODE: entry.data[CONF_DEFAULT_LOCK_CODE], - } - ) - - data = entry.data.copy() - data.pop(CONF_LOCK_CODE_DIGITS, None) - data.pop(CONF_DEFAULT_LOCK_CODE, None) - hass.config_entries.async_update_entry(entry, data=data, options=options) - - # Continue as normal... coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) if not await coordinator.async_login(): diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 34238d0763d..6c2822896e6 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -36,14 +36,6 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): installations: dict[str, str] password: str - # These can be removed after YAML import has been removed. - giid: str | None = None - settings: dict[str, int | str] - - def __init__(self): - """Initialize.""" - self.settings = {} - @staticmethod @callback def async_get_options_flow(config_entry: ConfigEntry) -> VerisureOptionsFlowHandler: @@ -95,8 +87,6 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Select Verisure installation to add.""" if len(self.installations) == 1: user_input = {CONF_GIID: list(self.installations)[0]} - elif self.giid and self.giid in self.installations: - user_input = {CONF_GIID: self.giid} if user_input is None: return self.async_show_form( @@ -115,7 +105,6 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): CONF_EMAIL: self.email, CONF_PASSWORD: self.password, CONF_GIID: user_input[CONF_GIID], - **self.settings, }, ) @@ -168,26 +157,6 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Import Verisure YAML configuration.""" - if user_input[CONF_GIID]: - self.giid = user_input[CONF_GIID] - await self.async_set_unique_id(self.giid) - self._abort_if_unique_id_configured() - else: - # The old YAML configuration could handle 1 single Verisure instance. - # Therefore, if we don't know the GIID, we can use the discovery - # without a unique ID logic, to prevent re-import/discovery. - await self._async_handle_discovery_without_unique_id() - - # Settings, later to be converted to config entry options - if user_input[CONF_LOCK_CODE_DIGITS]: - self.settings[CONF_LOCK_CODE_DIGITS] = user_input[CONF_LOCK_CODE_DIGITS] - if user_input[CONF_LOCK_DEFAULT_CODE]: - self.settings[CONF_LOCK_DEFAULT_CODE] = user_input[CONF_LOCK_DEFAULT_CODE] - - return await self.async_step_user(user_input) - class VerisureOptionsFlowHandler(OptionsFlow): """Handle Verisure options.""" diff --git a/homeassistant/components/verisure/const.py b/homeassistant/components/verisure/const.py index 030c5a58075..e8720baa1d5 100644 --- a/homeassistant/components/verisure/const.py +++ b/homeassistant/components/verisure/const.py @@ -44,7 +44,3 @@ ALARM_STATE_TO_HA = { "ARMED_AWAY": STATE_ALARM_ARMED_AWAY, "PENDING": STATE_ALARM_PENDING, } - -# Legacy; to remove after YAML removal -CONF_CODE_DIGITS = "code_digits" -CONF_DEFAULT_LOCK_CODE = "default_lock_code" diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index b9af9450132..f850487fe26 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -44,8 +44,6 @@ async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: with patch( "homeassistant.components.verisure.config_flow.Verisure", ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.verisure.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -72,7 +70,6 @@ async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: } assert len(mock_verisure.mock_calls) == 2 - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -107,8 +104,6 @@ async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> Non assert result2["errors"] is None with patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.verisure.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -126,7 +121,6 @@ async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> Non } assert len(mock_verisure.mock_calls) == 2 - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -220,8 +214,6 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: "homeassistant.components.verisure.config_flow.Verisure.login", return_value=True, ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.verisure.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -243,7 +235,6 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: } assert len(mock_verisure.mock_calls) == 1 - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -365,8 +356,6 @@ async def test_options_flow( entry.add_to_hass(hass) with patch( - "homeassistant.components.verisure.async_setup", return_value=True - ), patch( "homeassistant.components.verisure.async_setup_entry", return_value=True, ): @@ -397,8 +386,6 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: entry.add_to_hass(hass) with patch( - "homeassistant.components.verisure.async_setup", return_value=True - ), patch( "homeassistant.components.verisure.async_setup_entry", return_value=True, ): @@ -422,185 +409,3 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "code_format_mismatch"} - - -# -# Below this line are tests that can be removed once the YAML configuration -# has been removed from this integration. -# -@pytest.mark.parametrize( - "giid,installations", - [ - ("12345", TEST_INSTALLATION), - ("12345", TEST_INSTALLATIONS), - (None, TEST_INSTALLATION), - ], -) -async def test_imports( - hass: HomeAssistant, giid: str | None, installations: dict[str, str] -) -> None: - """Test a YAML import with/without known giid on single/multiple installations.""" - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=installations - ) - mock_verisure.login.return_value = True - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: giid, - CONF_LOCK_CODE_DIGITS: 10, - CONF_LOCK_DEFAULT_CODE: "123456", - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "ascending (12345th street)" - assert result["data"] == { - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_LOCK_CODE_DIGITS: 10, - CONF_LOCK_DEFAULT_CODE: "123456", - CONF_PASSWORD: "SuperS3cr3t!", - } - - assert len(mock_verisure.mock_calls) == 2 - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_imports_invalid_login(hass: HomeAssistant) -> None: - """Test a YAML import that results in a invalid login.""" - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureLoginError, - ): - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: None, - CONF_LOCK_CODE_DIGITS: None, - CONF_LOCK_DEFAULT_CODE: None, - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - - assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} - - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=TEST_INSTALLATION - ) - mock_verisure.login.return_value = True - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "ascending (12345th street)" - assert result2["data"] == { - CONF_GIID: "12345", - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_PASSWORD: "SuperS3cr3t!", - } - - assert len(mock_verisure.mock_calls) == 2 - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_imports_needs_user_installation_choice(hass: HomeAssistant) -> None: - """Test a YAML import that needs to use to decide on the installation.""" - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=TEST_INSTALLATIONS - ) - mock_verisure.login.return_value = True - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: None, - CONF_LOCK_CODE_DIGITS: None, - CONF_LOCK_DEFAULT_CODE: None, - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - - assert result["step_id"] == "installation" - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None - - with patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"giid": "12345"} - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "ascending (12345th street)" - assert result2["data"] == { - CONF_GIID: "12345", - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_PASSWORD: "SuperS3cr3t!", - } - - assert len(mock_verisure.mock_calls) == 2 - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize("giid", ["12345", None]) -async def test_import_already_exists(hass: HomeAssistant, giid: str | None) -> None: - """Test that import flow aborts if exists.""" - MockConfigEntry(domain=DOMAIN, data={}, unique_id="12345").add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_PASSWORD: "SuperS3cr3t!", - CONF_GIID: giid, - }, - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" From 084fe1fb68238666bfa7d8bb4de5769ac2137e4a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 09:51:43 +0200 Subject: [PATCH 180/852] Deprecate Glances YAML configuration (#50085) --- homeassistant/components/glances/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 0ccf8509cdd..a2969c032f8 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -54,7 +54,8 @@ GLANCES_SCHEMA = vol.All( ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [GLANCES_SCHEMA])}, extra=vol.ALLOW_EXTRA + vol.All(cv.deprecated(DOMAIN), {DOMAIN: vol.All(cv.ensure_list, [GLANCES_SCHEMA])}), + extra=vol.ALLOW_EXTRA, ) From 6cb5bf2b880f4fb39a370a088b8760ca8c3d051b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 09:52:11 +0200 Subject: [PATCH 181/852] Deprecate Denon HEOS YAML configuration (#50104) --- homeassistant/components/heos/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 56155cb21a2..3a9bedbb376 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -31,7 +31,11 @@ from .const import ( PLATFORMS = [MEDIA_PLAYER_DOMAIN] CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA + vol.All( + cv.deprecated(DOMAIN), + {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, + ), + extra=vol.ALLOW_EXTRA, ) MIN_UPDATE_SOURCES = timedelta(seconds=1) From e55be3c89a1e365aec0adf1b0c513838a517d16e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 09:52:32 +0200 Subject: [PATCH 182/852] Deprecate Freebox YAML configuration (#50084) --- homeassistant/components/freebox/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 40e01db39d1..44816a5c8ae 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -18,7 +18,10 @@ FREEBOX_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [FREEBOX_SCHEMA]))}, + vol.All( + cv.deprecated(DOMAIN), + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [FREEBOX_SCHEMA]))}, + ), extra=vol.ALLOW_EXTRA, ) From 93572bfe029ad7d554db82fc74c6bfdd52bc835c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 May 2021 13:04:37 +0200 Subject: [PATCH 183/852] Add color_mode support to tasmota light (#49599) --- homeassistant/components/tasmota/light.py | 132 +++++++---- tests/components/tasmota/test_light.py | 256 ++++++++++++++++------ 2 files changed, 282 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index efab8dcaae3..440b6f4267d 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -12,20 +12,21 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, SUPPORT_EFFECT, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, LightEntity, + brightness_supported, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -import homeassistant.util.color as color_util from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW @@ -64,14 +65,17 @@ class TasmotaLight( def __init__(self, **kwds): """Initialize Tasmota light.""" self._state = False + self._supported_color_modes = None self._supported_features = 0 self._brightness = None + self._color_mode = None self._color_temp = None self._effect = None - self._hs = None self._white_value = None self._flash_times = None + self._rgb = None + self._rgbw = None super().__init__( **kwds, @@ -87,22 +91,35 @@ class TasmotaLight( def _setup_from_entity(self): """(Re)Setup the entity.""" + self._supported_color_modes = set() supported_features = 0 light_type = self._tasmota_entity.light_type - if light_type != LIGHT_TYPE_NONE: - supported_features |= SUPPORT_BRIGHTNESS + if light_type in [LIGHT_TYPE_RGB, LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]: + # Mark RGB support for RGBW light because we don't have control over the + # white channel, so the base component's RGB->RGBW translation does not work + self._supported_color_modes.add(COLOR_MODE_RGB) + self._color_mode = COLOR_MODE_RGB + + if light_type == LIGHT_TYPE_RGBW: + self._supported_color_modes.add(COLOR_MODE_RGBW) + self._color_mode = COLOR_MODE_RGBW if light_type in [LIGHT_TYPE_COLDWARM, LIGHT_TYPE_RGBCW]: - supported_features |= SUPPORT_COLOR_TEMP + self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + self._color_mode = COLOR_MODE_COLOR_TEMP + + if light_type != LIGHT_TYPE_NONE and not self._supported_color_modes: + self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + self._color_mode = COLOR_MODE_BRIGHTNESS + + if not self._supported_color_modes: + self._supported_color_modes.add(COLOR_MODE_ONOFF) + self._color_mode = COLOR_MODE_ONOFF if light_type in [LIGHT_TYPE_RGB, LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]: - supported_features |= SUPPORT_COLOR supported_features |= SUPPORT_EFFECT - if light_type in [LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]: - supported_features |= SUPPORT_WHITE_VALUE - if self._tasmota_entity.supports_transition: supported_features |= SUPPORT_TRANSITION @@ -119,8 +136,17 @@ class TasmotaLight( percent_bright = brightness / TASMOTA_BRIGHTNESS_MAX self._brightness = percent_bright * 255 if "color" in attributes: - color = attributes["color"] - self._hs = color_util.color_RGB_to_hs(*color) + + def clamp(value): + """Clamp value to the range 0..255.""" + return min(max(value, 0), 255) + + rgb = attributes["color"] + # Tasmota's RGB color is adjusted for brightness, compensate + red_compensated = clamp(round(rgb[0] / self._brightness * 255)) + green_compensated = clamp(round(rgb[1] / self._brightness * 255)) + blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) + self._rgb = [red_compensated, green_compensated, blue_compensated] if "color_temp" in attributes: self._color_temp = attributes["color_temp"] if "effect" in attributes: @@ -129,11 +155,13 @@ class TasmotaLight( white_value = float(attributes["white_value"]) percent_white = white_value / TASMOTA_BRIGHTNESS_MAX self._white_value = percent_white * 255 - if self._white_value == 0: - self._color_temp = None - self._white_value = None - if self._white_value is not None and self._white_value > 0: - self._hs = None + if self._tasmota_entity.light_type == LIGHT_TYPE_RGBCW: + # Tasmota does not support RGBWW mode, set mode to ct or rgb + if self._white_value == 0: + self._color_mode = COLOR_MODE_RGB + else: + self._color_mode = COLOR_MODE_COLOR_TEMP + self.async_write_ha_state() @property @@ -141,6 +169,11 @@ class TasmotaLight( """Return the brightness of this light between 0..255.""" return self._brightness + @property + def color_mode(self): + """Return the color mode of the light.""" + return self._color_mode + @property def color_temp(self): """Return the color temperature in mired.""" @@ -167,20 +200,27 @@ class TasmotaLight( return self._tasmota_entity.effect_list @property - def hs_color(self): - """Return the hs color value.""" - return self._hs + def rgb_color(self): + """Return the rgb color value.""" + return self._rgb @property - def white_value(self): - """Return the white property.""" - return self._white_value + def rgbw_color(self): + """Return the rgbw color value.""" + if self._rgb is None or self._white_value is None: + return None + return [*self._rgb, self._white_value] @property def is_on(self): """Return true if device is on.""" return self._state + @property + def supported_color_modes(self): + """Flag supported color modes.""" + return self._supported_color_modes + @property def supported_features(self): """Flag supported features.""" @@ -188,21 +228,33 @@ class TasmotaLight( async def async_turn_on(self, **kwargs): """Turn the entity on.""" - supported_features = self._supported_features + supported_color_modes = self._supported_color_modes attributes = {} - if ATTR_HS_COLOR in kwargs and supported_features & SUPPORT_COLOR: - hs_color = kwargs[ATTR_HS_COLOR] - attributes["color"] = {} - - rgb = color_util.color_hsv_to_RGB(hs_color[0], hs_color[1], 100) + if ATTR_RGB_COLOR in kwargs and COLOR_MODE_RGB in supported_color_modes: + rgb = kwargs[ATTR_RGB_COLOR] attributes["color"] = [rgb[0], rgb[1], rgb[2]] + if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes: + rgbw = kwargs[ATTR_RGBW_COLOR] + # Tasmota does not support direct RGBW control, the light must be set to + # either white mode or color mode. Set the mode according to max of rgb + # and white channels + if max(rgbw[0:3]) > rgbw[3]: + attributes["color"] = [rgbw[0], rgbw[1], rgbw[2]] + else: + white_value_normalized = rgbw[3] / DEFAULT_BRIGHTNESS_MAX + device_white_value = min( + round(white_value_normalized * TASMOTA_BRIGHTNESS_MAX), + TASMOTA_BRIGHTNESS_MAX, + ) + attributes["white_value"] = device_white_value + if ATTR_TRANSITION in kwargs: attributes["transition"] = kwargs[ATTR_TRANSITION] - if ATTR_BRIGHTNESS in kwargs and supported_features & SUPPORT_BRIGHTNESS: + if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes): brightness_normalized = kwargs[ATTR_BRIGHTNESS] / DEFAULT_BRIGHTNESS_MAX device_brightness = min( round(brightness_normalized * TASMOTA_BRIGHTNESS_MAX), @@ -212,20 +264,12 @@ class TasmotaLight( device_brightness = max(device_brightness, 1) attributes["brightness"] = device_brightness - if ATTR_COLOR_TEMP in kwargs and supported_features & SUPPORT_COLOR_TEMP: + if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in supported_color_modes: attributes["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) if ATTR_EFFECT in kwargs: attributes["effect"] = kwargs[ATTR_EFFECT] - if ATTR_WHITE_VALUE in kwargs: - white_value_normalized = kwargs[ATTR_WHITE_VALUE] / DEFAULT_BRIGHTNESS_MAX - device_white_value = min( - round(white_value_normalized * TASMOTA_BRIGHTNESS_MAX), - TASMOTA_BRIGHTNESS_MAX, - ) - attributes["white_value"] = device_white_value - self._tasmota_entity.set_state(True, attributes) async def async_turn_off(self, **kwargs): diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index a60f167c38f..6b450fa805d 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -11,14 +11,7 @@ from hatasmota.utils import ( ) from homeassistant.components import light -from homeassistant.components.light import ( - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_EFFECT, - SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, -) +from homeassistant.components.light import SUPPORT_EFFECT, SUPPORT_TRANSITION from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON @@ -60,6 +53,8 @@ async def test_attributes_on_off(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None assert state.attributes.get("supported_features") == 0 + assert state.attributes.get("supported_color_modes") == ["onoff"] + assert state.attributes.get("color_mode") == "onoff" async def test_attributes_dimmer_tuya(hass, mqtt_mock, setup_tasmota): @@ -83,7 +78,9 @@ async def test_attributes_dimmer_tuya(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None - assert state.attributes.get("supported_features") == SUPPORT_BRIGHTNESS + assert state.attributes.get("supported_features") == 0 + assert state.attributes.get("supported_color_modes") == ["brightness"] + assert state.attributes.get("color_mode") == "brightness" async def test_attributes_dimmer(hass, mqtt_mock, setup_tasmota): @@ -106,10 +103,9 @@ async def test_attributes_dimmer(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None - assert ( - state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION - ) + assert state.attributes.get("supported_features") == SUPPORT_TRANSITION + assert state.attributes.get("supported_color_modes") == ["brightness"] + assert state.attributes.get("color_mode") == "brightness" async def test_attributes_ct(hass, mqtt_mock, setup_tasmota): @@ -132,10 +128,9 @@ async def test_attributes_ct(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") == 153 assert state.attributes.get("max_mireds") == 500 - assert ( - state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION - ) + assert state.attributes.get("supported_features") == SUPPORT_TRANSITION + assert state.attributes.get("supported_color_modes") == ["color_temp"] + assert state.attributes.get("color_mode") == "color_temp" async def test_attributes_ct_reduced(hass, mqtt_mock, setup_tasmota): @@ -159,10 +154,9 @@ async def test_attributes_ct_reduced(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") == 200 assert state.attributes.get("max_mireds") == 380 - assert ( - state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION - ) + assert state.attributes.get("supported_features") == SUPPORT_TRANSITION + assert state.attributes.get("supported_color_modes") == ["color_temp"] + assert state.attributes.get("color_mode") == "color_temp" async def test_attributes_rgb(hass, mqtt_mock, setup_tasmota): @@ -193,8 +187,10 @@ async def test_attributes_rgb(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("max_mireds") is None assert ( state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT | SUPPORT_TRANSITION + == SUPPORT_EFFECT | SUPPORT_TRANSITION ) + assert state.attributes.get("supported_color_modes") == ["rgb"] + assert state.attributes.get("color_mode") == "rgb" async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): @@ -225,12 +221,10 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("max_mireds") is None assert ( state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_EFFECT - | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE + == SUPPORT_EFFECT | SUPPORT_TRANSITION ) + assert state.attributes.get("supported_color_modes") == ["rgb", "rgbw"] + assert state.attributes.get("color_mode") == "rgbw" async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota): @@ -261,13 +255,10 @@ async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("max_mireds") == 500 assert ( state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_COLOR_TEMP - | SUPPORT_EFFECT - | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE + == SUPPORT_EFFECT | SUPPORT_TRANSITION ) + assert state.attributes.get("supported_color_modes") == ["color_temp", "rgb"] + assert state.attributes.get("color_mode") == "color_temp" async def test_attributes_rgbww_reduced(hass, mqtt_mock, setup_tasmota): @@ -299,13 +290,10 @@ async def test_attributes_rgbww_reduced(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("max_mireds") == 380 assert ( state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_COLOR_TEMP - | SUPPORT_EFFECT - | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE + == SUPPORT_EFFECT | SUPPORT_TRANSITION ) + assert state.attributes.get("supported_color_modes") == ["color_temp", "rgb"] + assert state.attributes.get("color_mode") == "color_temp" async def test_controlling_state_via_mqtt_on_off(hass, mqtt_mock, setup_tasmota): @@ -325,29 +313,35 @@ async def test_controlling_state_via_mqtt_on_off(hass, mqtt_mock, setup_tasmota) state = hass.states.get("light.test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "onoff" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "onoff" async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): @@ -367,19 +361,23 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' @@ -387,6 +385,7 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' @@ -394,6 +393,7 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 + assert state.attributes.get("color_mode") == "color_temp" # Tasmota will send "Color" also for CT light, this should be ignored async_fire_mqtt_message( @@ -403,6 +403,7 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "color_temp" async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): @@ -422,19 +423,23 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' @@ -442,22 +447,27 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128,0"}' + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Color":"128,64,0","White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 128, 0) + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 127.5 + assert "white_value" not in state.attributes # Setting white > 0 should clear the color - assert not state.attributes.get("rgb_color") + assert "rgb_color" not in state.attributes + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' @@ -465,15 +475,18 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' ) state = hass.states.get("light.test") assert state.state == STATE_ON - # Setting white to 0 should clear the white_value and color_temp - assert not state.attributes.get("white_value") - assert not state.attributes.get("color_temp") + # Setting white to 0 should clear the color_temp + assert "white_value" not in state.attributes + assert "color_temp" not in state.attributes + assert state.attributes.get("rgb_color") == (255, 128, 0) + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' @@ -511,19 +524,23 @@ async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmo state = hass.states.get("light.test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' @@ -531,29 +548,33 @@ async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmo state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"FF8000"}' + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"804000","White":0}' ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 128, 0) + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"00FF800000"}' + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"0080400000"}' ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (0, 255, 128) + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 127.5 + assert "white_value" not in state.attributes # Setting white > 0 should clear the color - assert not state.attributes.get("rgb_color") + assert "rgb_color" not in state.attributes + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' @@ -561,6 +582,7 @@ async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmo state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' @@ -570,6 +592,7 @@ async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmo # Setting white to 0 should clear the white_value and color_temp assert not state.attributes.get("white_value") assert not state.attributes.get("color_temp") + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' @@ -607,19 +630,23 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm state = hass.states.get("light.test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' @@ -627,22 +654,27 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128,0"}' + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Color":"128,64,0","White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 128, 0) + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 127.5 + assert "white_value" not in state.attributes # Setting white > 0 should clear the color - assert not state.attributes.get("rgb_color") + assert "rgb_color" not in state.attributes + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' @@ -650,6 +682,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' @@ -659,6 +692,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm # Setting white to 0 should clear the white_value and color_temp assert not state.attributes.get("white_value") assert not state.attributes.get("color_temp") + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' @@ -765,6 +799,102 @@ async def test_sending_mqtt_commands_rgbww_tuya(hass, mqtt_mock, setup_tasmota): ) +async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 4 # 4 channel light (RGBW) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT message is sent + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Tasmota is not optimistic, the state should still be off + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + # Turn the light off and verify MQTT message is sent + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT messages are sent + await common.async_turn_on(hass, "light.test", brightness=192) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set color when setting color + await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set color when setting brighter color than white + await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 16]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set white when setting brighter white than color + await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;White 50", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", white_value=128) + # white_value should be ignored + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", effect="Random") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Scheme 4", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -811,10 +941,10 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;Color2 255,128,0", + "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", 0, False, ) @@ -830,9 +960,10 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.reset_mock() await common.async_turn_on(hass, "light.test", white_value=128) + # white_value should be ignored mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;White 50", + "NoDelay;Power1 ON", 0, False, ) @@ -1000,7 +1131,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", - '{"POWER":"ON","Dimmer":50, "Color":"0,255,0"}', + '{"POWER":"ON","Dimmer":50, "Color":"0,255,0", "White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1040,7 +1171,9 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): # Fake state update from the light async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50, "CT":153}' + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Dimmer":50, "CT":153, "White":50}', ) state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1324,10 +1457,8 @@ async def test_discovery_update_reconfigure_light( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1) await hass.async_block_till_done() state = hass.states.get("light.test") - assert ( - state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION - ) + assert state.attributes.get("supported_features") == SUPPORT_TRANSITION + assert state.attributes.get("supported_color_modes") == ["brightness"] # Reconfigure as RGB light async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data2) @@ -1335,8 +1466,9 @@ async def test_discovery_update_reconfigure_light( state = hass.states.get("light.test") assert ( state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT | SUPPORT_TRANSITION + == SUPPORT_EFFECT | SUPPORT_TRANSITION ) + assert state.attributes.get("supported_color_modes") == ["rgb"] async def test_availability_when_connection_lost( From 65cf13836063392e27bad2e913a1e41c5b4ce30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 5 May 2021 17:09:18 +0200 Subject: [PATCH 184/852] Allign error handling for restart for hassio with core (#50114) * Allign error handling for restart for hassio with core * Reuse HASS_DOMAIN * Address comments --- homeassistant/components/hassio/__init__.py | 36 ++++++++++++++----- .../components/homeassistant/__init__.py | 8 +++-- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index eabd9bc7cd9..2c25868dfcd 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -9,7 +9,10 @@ from typing import Any import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN -from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG +from homeassistant.components.homeassistant import ( + SERVICE_CHECK_CONFIG, + SHUTDOWN_SERVICES, +) import homeassistant.config as conf_util from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -21,7 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as HASS_DOMAIN, Config, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, recorder from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.loader import bind_hass @@ -469,23 +472,40 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: # noqa: C90 async def async_handle_core_service(call): """Service handler for handling core services.""" + if ( + call.service in SHUTDOWN_SERVICES + and await recorder.async_migration_in_progress(hass) + ): + _LOGGER.error( + "The system cannot %s while a database upgrade is in progress", + call.service, + ) + raise HomeAssistantError( + f"The system cannot {call.service} " + "while a database upgrade is in progress." + ) + if call.service == SERVICE_HOMEASSISTANT_STOP: await hassio.stop_homeassistant() return - try: - errors = await conf_util.async_check_ha_config_file(hass) - except HomeAssistantError: - return + errors = await conf_util.async_check_ha_config_file(hass) if errors: - _LOGGER.error(errors) + _LOGGER.error( + "The system cannot %s because the configuration is not valid: %s", + call.service, + errors, + ) hass.components.persistent_notification.async_create( "Config error. See [the logs](/config/logs) for details.", "Config validating", f"{HASS_DOMAIN}.check_config", ) - return + raise HomeAssistantError( + f"The system cannot {call.service} " + f"because the configuration is not valid: {errors}" + ) if call.service == SERVICE_HOMEASSISTANT_RESTART: await hassio.restart_homeassistant() diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index fd7f2207bc7..44f0843871c 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -133,11 +133,12 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # noqa: C9 and await recorder.async_migration_in_progress(hass) ): _LOGGER.error( - "The system cannot %s while a database upgrade in progress", + "The system cannot %s while a database upgrade is in progress", call.service, ) raise HomeAssistantError( - f"The system cannot {call.service} while a database upgrade in progress." + f"The system cannot {call.service} " + "while a database upgrade is in progress." ) if call.service == SERVICE_HOMEASSISTANT_STOP: @@ -158,7 +159,8 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # noqa: C9 f"{ha.DOMAIN}.check_config", ) raise HomeAssistantError( - f"The system cannot {call.service} because the configuration is not valid: {errors}" + f"The system cannot {call.service} " + f"because the configuration is not valid: {errors}" ) if call.service == SERVICE_HOMEASSISTANT_RESTART: From 40a18c10a07d5906751d6a6b066d8207118aedc5 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 5 May 2021 17:13:06 +0200 Subject: [PATCH 185/852] Remove surepetcare usage of deprecated config options (#50113) --- homeassistant/components/surepetcare/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 3873d17343d..3283b4c97c9 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -105,7 +105,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: lock_state_service_schema = vol.Schema( { vol.Required(ATTR_FLAP_ID): vol.All( - cv.positive_int, vol.In(conf[CONF_FLAPS]) + cv.positive_int, vol.In(spc.states.keys()) ), vol.Required(ATTR_LOCK_STATE): vol.All( cv.string, From 4136f9f203a2629776e47fe9bbba695c5ba77be3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 May 2021 17:59:26 +0200 Subject: [PATCH 186/852] Fix Tasmota color scaling and RGBW lights (#50120) --- homeassistant/components/tasmota/light.py | 24 +++++++++++++++++------ tests/components/tasmota/test_light.py | 6 +++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index 440b6f4267d..53db34a9001 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -143,9 +143,14 @@ class TasmotaLight( rgb = attributes["color"] # Tasmota's RGB color is adjusted for brightness, compensate - red_compensated = clamp(round(rgb[0] / self._brightness * 255)) - green_compensated = clamp(round(rgb[1] / self._brightness * 255)) - blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) + if self._brightness > 0: + red_compensated = clamp(round(rgb[0] / self._brightness * 255)) + green_compensated = clamp(round(rgb[1] / self._brightness * 255)) + blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) + else: + red_compensated = 0 + green_compensated = 0 + blue_compensated = 0 self._rgb = [red_compensated, green_compensated, blue_compensated] if "color_temp" in attributes: self._color_temp = attributes["color_temp"] @@ -211,6 +216,13 @@ class TasmotaLight( return None return [*self._rgb, self._white_value] + @property + def force_update(self): + """Force update.""" + if self.color_mode == COLOR_MODE_RGBW: + return True + return False + @property def is_on(self): """Return true if device is on.""" @@ -239,9 +251,9 @@ class TasmotaLight( if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes: rgbw = kwargs[ATTR_RGBW_COLOR] # Tasmota does not support direct RGBW control, the light must be set to - # either white mode or color mode. Set the mode according to max of rgb - # and white channels - if max(rgbw[0:3]) > rgbw[3]: + # either white mode or color mode. Set the mode to white if white channel + # is on, and to color otheruse + if rgbw[3] == 0: attributes["color"] = [rgbw[0], rgbw[1], rgbw[2]] else: white_value_normalized = rgbw[3] / DEFAULT_BRIGHTNESS_MAX diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 6b450fa805d..3a27409e433 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -855,8 +855,8 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - # Set color when setting brighter color than white - await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 16]) + # Set color when setting white is off + await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", @@ -865,7 +865,7 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - # Set white when setting brighter white than color + # Set white when white is on await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", From 5dd59415a8d63ba4fa6ea0ba5b06dd606b3f77a3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 5 May 2021 21:14:20 +0200 Subject: [PATCH 187/852] Drop OWFS support in onewire (#50121) --- .../components/onewire/config_flow.py | 15 ---- homeassistant/components/onewire/const.py | 1 - homeassistant/components/onewire/sensor.py | 72 ------------------- 3 files changed, 88 deletions(-) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 88ad731ae21..4ae3b1468db 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -7,7 +7,6 @@ from homeassistant.core import HomeAssistant from .const import ( CONF_MOUNT_DIR, - CONF_TYPE_OWFS, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, DEFAULT_OWSERVER_HOST, @@ -166,20 +165,6 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): platform_config[CONF_PORT] = DEFAULT_OWSERVER_PORT return await self.async_step_owserver(platform_config) - # OWFS - if platform_config[CONF_TYPE] == CONF_TYPE_OWFS: # pragma: no cover - # This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated. - # https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices - await self.async_set_unique_id( - f"{CONF_TYPE_OWFS}:{platform_config[CONF_MOUNT_DIR]}" - ) - self._abort_if_unique_id_configured( - updates=platform_config, reload_on_update=True - ) - return self.async_create_entry( - title=platform_config[CONF_MOUNT_DIR], data=platform_config - ) - # SysBus if CONF_MOUNT_DIR not in platform_config: platform_config[CONF_MOUNT_DIR] = DEFAULT_SYSBUS_MOUNT_DIR diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 54b18f7c905..9dc67dbd9b8 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -20,7 +20,6 @@ from homeassistant.const import ( CONF_MOUNT_DIR = "mount_dir" CONF_NAMES = "names" -CONF_TYPE_OWFS = "OWFS" CONF_TYPE_OWSERVER = "OWServer" CONF_TYPE_SYSBUS = "SysBus" diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index b3a5be0a1ca..bc4dfc3dcf1 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from glob import glob import logging import os @@ -18,7 +17,6 @@ from homeassistant.helpers.typing import StateType from .const import ( CONF_MOUNT_DIR, CONF_NAMES, - CONF_TYPE_OWFS, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, DEFAULT_OWSERVER_PORT, @@ -242,10 +240,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= config[CONF_TYPE] = CONF_TYPE_OWSERVER elif config[CONF_MOUNT_DIR] == DEFAULT_SYSBUS_MOUNT_DIR: config[CONF_TYPE] = CONF_TYPE_SYSBUS - else: # pragma: no cover - # This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated. - # https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices - config[CONF_TYPE] = CONF_TYPE_OWFS hass.async_create_task( hass.config_entries.flow.async_init( @@ -361,38 +355,6 @@ def get_entities(onewirehub: OneWireHub, config): "Check the mount_dir parameter if it's defined" ) - # We have an owfs mounted - else: # pragma: no cover - # This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated. - # https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices - base_dir = config[CONF_MOUNT_DIR] - _LOGGER.debug("Initializing using OWFS %s", base_dir) - _LOGGER.warning( - "The OWFS implementation of 1-Wire sensors is deprecated, " - "and should be migrated to OWServer (on localhost:4304). " - "If migration to OWServer is not feasible on your installation, " - "please raise an issue at https://github.com/home-assistant/core/issues/new" - "?title=Unable%20to%20migrate%20onewire%20from%20OWFS%20to%20OWServer", - ) - for family_file_path in glob(os.path.join(base_dir, "*", "family")): - with open(family_file_path) as family_file: - family = family_file.read() - if "EF" in family: - continue - if family in DEVICE_SENSORS: - for sensor_key, sensor_value in DEVICE_SENSORS[family].items(): - sensor_id = os.path.split(os.path.split(family_file_path)[0])[1] - device_file = os.path.join( - os.path.split(family_file_path)[0], sensor_value - ) - entities.append( - OneWireOWFSSensor( - device_names.get(sensor_id, sensor_id), - device_file, - sensor_key, - ) - ) - return entities @@ -460,37 +422,3 @@ class OneWireDirectSensor(OneWireSensor): ) as ex: _LOGGER.warning("Cannot read from sensor %s: %s", self._device_file, ex) self._state = value - - -class OneWireOWFSSensor(OneWireSensor): # pragma: no cover - """Implementation of a 1-Wire sensor through owfs. - - This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated. - https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices - """ - - @property - def state(self) -> StateType: - """Return the state of the entity.""" - return self._state - - def _read_value_raw(self): - """Read the value as it is returned by the sensor.""" - with open(self._device_file) as ds_device_file: - lines = ds_device_file.readlines() - return lines - - def update(self): - """Get the latest data from the device.""" - value = None - try: - value_read = self._read_value_raw() - if len(value_read) == 1: - value = round(float(value_read[0]), 1) - self._value_raw = float(value_read[0]) - except ValueError: - _LOGGER.warning("Invalid value read from %s", self._device_file) - except FileNotFoundError: - _LOGGER.warning("Cannot read from sensor: %s", self._device_file) - - self._state = value From e4ef06d6b147f3804f2af0ea07ad0679a79cd526 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 6 May 2021 00:33:32 +0100 Subject: [PATCH 188/852] System Bridge Integration (#48156) Co-authored-by: Martin Hjelmare --- .coveragerc | 4 + CODEOWNERS | 1 + .../components/system_bridge/__init__.py | 269 ++++++++++++ .../components/system_bridge/binary_sensor.py | 72 +++ .../components/system_bridge/config_flow.py | 187 ++++++++ .../components/system_bridge/const.py | 19 + .../components/system_bridge/manifest.json | 12 + .../components/system_bridge/sensor.py | 340 +++++++++++++++ .../components/system_bridge/services.yaml | 46 ++ .../components/system_bridge/strings.json | 32 ++ .../system_bridge/translations/en.json | 32 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 5 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/system_bridge/__init__.py | 1 + .../system_bridge/test_config_flow.py | 409 ++++++++++++++++++ 17 files changed, 1436 insertions(+) create mode 100644 homeassistant/components/system_bridge/__init__.py create mode 100644 homeassistant/components/system_bridge/binary_sensor.py create mode 100644 homeassistant/components/system_bridge/config_flow.py create mode 100644 homeassistant/components/system_bridge/const.py create mode 100644 homeassistant/components/system_bridge/manifest.json create mode 100644 homeassistant/components/system_bridge/sensor.py create mode 100644 homeassistant/components/system_bridge/services.yaml create mode 100644 homeassistant/components/system_bridge/strings.json create mode 100644 homeassistant/components/system_bridge/translations/en.json create mode 100644 tests/components/system_bridge/__init__.py create mode 100644 tests/components/system_bridge/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 977cf11a752..8e0b7dba206 100644 --- a/.coveragerc +++ b/.coveragerc @@ -977,6 +977,10 @@ omit = homeassistant/components/synology_dsm/switch.py homeassistant/components/synology_srm/device_tracker.py homeassistant/components/syslog/notify.py + homeassistant/components/system_bridge/__init__.py + homeassistant/components/system_bridge/const.py + homeassistant/components/system_bridge/binary_sensor.py + homeassistant/components/system_bridge/sensor.py homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/* homeassistant/components/tado/device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index f69e9b3ecfe..ae71cde1754 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -481,6 +481,7 @@ homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185 homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff +homeassistant/components/system_bridge/* @timmo001 homeassistant/components/tado/* @michaelarnauts @bdraco @noltari homeassistant/components/tag/* @balloob @dmulcahey homeassistant/components/tahoma/* @philklei diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py new file mode 100644 index 00000000000..279c63680f0 --- /dev/null +++ b/homeassistant/components/system_bridge/__init__.py @@ -0,0 +1,269 @@ +"""The System Bridge integration.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging +import shlex + +import async_timeout +from systembridge import Bridge +from systembridge.client import BridgeClient +from systembridge.exceptions import BridgeAuthenticationException +from systembridge.objects.command.response import CommandResponse +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_COMMAND, + CONF_HOST, + CONF_PATH, + CONF_PORT, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import ( + aiohttp_client, + config_validation as cv, + device_registry as dr, +) +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["binary_sensor", "sensor"] + +CONF_ARGUMENTS = "arguments" +CONF_BRIDGE = "bridge" +CONF_WAIT = "wait" + +SERVICE_SEND_COMMAND = "send_command" +SERVICE_SEND_COMMAND_SCHEMA = vol.Schema( + { + vol.Required(CONF_BRIDGE): cv.string, + vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_ARGUMENTS, []): cv.string, + } +) +SERVICE_OPEN = "open" +SERVICE_OPEN_SCHEMA = vol.Schema( + {vol.Required(CONF_BRIDGE): cv.string, vol.Required(CONF_PATH): cv.string} +) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up System Bridge from a config entry.""" + + client = Bridge( + BridgeClient(aiohttp_client.async_get_clientsession(hass)), + f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}", + entry.data[CONF_API_KEY], + ) + + async def async_update_data() -> Bridge: + """Fetch data from Bridge.""" + try: + async with async_timeout.timeout(60): + await asyncio.gather( + *[ + client.async_get_battery(), + client.async_get_cpu(), + client.async_get_filesystem(), + client.async_get_network(), + client.async_get_os(), + client.async_get_processes(), + client.async_get_system(), + ] + ) + return client + except BridgeAuthenticationException as exception: + raise ConfigEntryAuthFailed from exception + except BRIDGE_CONNECTION_ERRORS as exception: + raise UpdateFailed("Could not connect to System Bridge.") from exception + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name=f"{DOMAIN}_coordinator", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=60), + ) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_config_entry_first_refresh() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + if hass.services.has_service(DOMAIN, SERVICE_SEND_COMMAND): + return True + + async def handle_send_command(call): + """Handle the send_command service call.""" + device_registry = dr.async_get(hass) + device_id = call.data[CONF_BRIDGE] + device_entry = device_registry.async_get(device_id) + if device_entry is None: + _LOGGER.warning("Missing device: %s", device_id) + return + + command = call.data[CONF_COMMAND] + arguments = shlex.split(call.data.get(CONF_ARGUMENTS, "")) + + entry_id = next( + entry.entry_id + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.entry_id in device_entry.config_entries + ) + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry_id] + bridge: Bridge = coordinator.data + + _LOGGER.debug( + "Command payload: %s", + {CONF_COMMAND: command, CONF_ARGUMENTS: arguments, CONF_WAIT: False}, + ) + try: + response: CommandResponse = await bridge.async_send_command( + {CONF_COMMAND: command, CONF_ARGUMENTS: arguments, CONF_WAIT: False} + ) + if response.success: + _LOGGER.debug( + "Sent command. Response message was: %s", response.message + ) + else: + _LOGGER.warning( + "Error sending command. Response message was: %s", response.message + ) + except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: + _LOGGER.warning("Error sending command. Error was: %s", exception) + + async def handle_open(call): + """Handle the open service call.""" + device_registry = dr.async_get(hass) + device_id = call.data[CONF_BRIDGE] + device_entry = device_registry.async_get(device_id) + if device_entry is None: + _LOGGER.warning("Missing device: %s", device_id) + return + + path = call.data[CONF_PATH] + + entry_id = next( + entry.entry_id + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.entry_id in device_entry.config_entries + ) + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry_id] + bridge: Bridge = coordinator.data + + _LOGGER.debug("Open payload: %s", {CONF_PATH: path}) + try: + await bridge.async_open({CONF_PATH: path}) + _LOGGER.debug("Sent open request") + except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: + _LOGGER.warning("Error sending. Error was: %s", exception) + + hass.services.async_register( + DOMAIN, + SERVICE_SEND_COMMAND, + handle_send_command, + schema=SERVICE_SEND_COMMAND_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_OPEN, + handle_open, + schema=SERVICE_OPEN_SCHEMA, + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, SERVICE_SEND_COMMAND) + hass.services.async_remove(DOMAIN, SERVICE_OPEN) + + return unload_ok + + +class BridgeEntity(CoordinatorEntity): + """Defines a base System Bridge entity.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + bridge: Bridge, + key: str, + name: str, + icon: str | None, + enabled_by_default: bool, + ) -> None: + """Initialize the System Bridge entity.""" + super().__init__(coordinator) + self._key = f"{bridge.os.hostname}_{key}" + self._name = f"{bridge.os.hostname} {name}" + self._icon = icon + self._enabled_default = enabled_by_default + self._hostname = bridge.os.hostname + self._default_interface = bridge.network.interfaces[ + bridge.network.interfaceDefault + ] + self._manufacturer = bridge.system.system.manufacturer + self._model = bridge.system.system.model + self._version = bridge.system.system.version + + @property + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return self._key + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str | None: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enabled_default + + +class BridgeDeviceEntity(BridgeEntity): + """Defines a System Bridge device entity.""" + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this System Bridge instance.""" + return { + "connections": { + (dr.CONNECTION_NETWORK_MAC, self._default_interface["mac"]) + }, + "manufacturer": self._manufacturer, + "model": self._model, + "name": self._hostname, + "sw_version": self._version, + } diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py new file mode 100644 index 00000000000..b1010a19ae4 --- /dev/null +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -0,0 +1,72 @@ +"""Support for System Bridge sensors.""" +from __future__ import annotations + +from systembridge import Bridge + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import BridgeDeviceEntity +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up System Bridge sensor based on a config entry.""" + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + bridge: Bridge = coordinator.data + + if bridge.battery.hasBattery: + async_add_entities([BridgeBatteryIsChargingBinarySensor(coordinator, bridge)]) + + +class BridgeBinarySensor(BridgeDeviceEntity, BinarySensorEntity): + """Defines a System Bridge sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + bridge: Bridge, + key: str, + name: str, + icon: str | None, + device_class: str | None, + enabled_by_default: bool, + ) -> None: + """Initialize System Bridge sensor.""" + self._device_class = device_class + + super().__init__(coordinator, bridge, key, name, icon, enabled_by_default) + + @property + def device_class(self) -> str | None: + """Return the class of this sensor.""" + return self._device_class + + +class BridgeBatteryIsChargingBinarySensor(BridgeBinarySensor): + """Defines a Battery is charging sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "battery_is_charging", + "Battery Is Charging", + None, + DEVICE_CLASS_BATTERY_CHARGING, + True, + ) + + @property + def is_on(self) -> bool: + """Return if the state is on.""" + bridge: Bridge = self.coordinator.data + return bridge.battery.isCharging diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py new file mode 100644 index 00000000000..a74c060fccf --- /dev/null +++ b/homeassistant/components/system_bridge/config_flow.py @@ -0,0 +1,187 @@ +"""Config flow for System Bridge integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import async_timeout +from systembridge import Bridge +from systembridge.client import BridgeClient +from systembridge.exceptions import BridgeAuthenticationException +from systembridge.objects.os import Os +from systembridge.objects.system import System +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_AUTHENTICATE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): cv.string}) +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT, default=9170): cv.string, + vol.Required(CONF_API_KEY): cv.string, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + bridge = Bridge( + BridgeClient(aiohttp_client.async_get_clientsession(hass)), + f"http://{data[CONF_HOST]}:{data[CONF_PORT]}", + data[CONF_API_KEY], + ) + + hostname = data[CONF_HOST] + try: + async with async_timeout.timeout(30): + bridge_os: Os = await bridge.async_get_os() + if bridge_os.hostname is not None: + hostname = bridge_os.hostname + bridge_system: System = await bridge.async_get_system() + except BridgeAuthenticationException as exception: + _LOGGER.info(exception) + raise InvalidAuth from exception + except BRIDGE_CONNECTION_ERRORS as exception: + _LOGGER.info(exception) + raise CannotConnect from exception + + return {"hostname": hostname, "uuid": bridge_system.uuid.os} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for System Bridge.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize flow.""" + self._name: str | None = None + self._input: dict[str, Any] = {} + self._reauth = False + + async def _async_get_info( + self, user_input: dict[str, Any] + ) -> tuple[dict[str, str], dict[str, str] | None]: + errors = {} + + try: + info = await validate_input(self.hass, 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" + else: + return errors, info + + return errors, None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors, info = await self._async_get_info(user_input) + if not errors and info is not None: + # Check if already configured + await self.async_set_unique_id(info["uuid"], raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_HOST: info["hostname"]}) + + return self.async_create_entry(title=info["hostname"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_authenticate( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle getting the api-key for authentication.""" + errors: dict[str, str] = {} + + if user_input is not None: + user_input = {**self._input, **user_input} + errors, info = await self._async_get_info(user_input) + if not errors and info is not None: + # Check if already configured + existing_entry = await self.async_set_unique_id(info["uuid"]) + + if self._reauth and existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data=user_input + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + self._abort_if_unique_id_configured( + updates={CONF_HOST: info["hostname"]} + ) + + return self.async_create_entry(title=info["hostname"], data=user_input) + + return self.async_show_form( + step_id="authenticate", + data_schema=STEP_AUTHENTICATE_DATA_SCHEMA, + description_placeholders={"name": self._name}, + errors=errors, + ) + + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle zeroconf discovery.""" + host = discovery_info["properties"].get("ip") + uuid = discovery_info["properties"].get("uuid") + + if host is None or uuid is None: + return self.async_abort(reason="unknown") + + # Check if already configured + await self.async_set_unique_id(uuid) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + + self._name = host + self._input = { + CONF_HOST: host, + CONF_PORT: discovery_info["properties"].get("port"), + } + + return await self.async_step_authenticate() + + async def async_step_reauth(self, entry_data: ConfigType) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._name = entry_data[CONF_HOST] + self._input = { + CONF_HOST: entry_data[CONF_HOST], + CONF_PORT: entry_data[CONF_PORT], + } + self._reauth = True + return await self.async_step_authenticate() + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/system_bridge/const.py b/homeassistant/components/system_bridge/const.py new file mode 100644 index 00000000000..5560d79a769 --- /dev/null +++ b/homeassistant/components/system_bridge/const.py @@ -0,0 +1,19 @@ +"""Constants for the System Bridge integration.""" +import asyncio + +from aiohttp.client_exceptions import ( + ClientConnectionError, + ClientConnectorError, + ClientResponseError, +) +from systembridge.exceptions import BridgeException + +DOMAIN = "system_bridge" + +BRIDGE_CONNECTION_ERRORS = ( + asyncio.TimeoutError, + BridgeException, + ClientConnectionError, + ClientConnectorError, + ClientResponseError, +) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json new file mode 100644 index 00000000000..c960c1c6557 --- /dev/null +++ b/homeassistant/components/system_bridge/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "system_bridge", + "name": "System Bridge", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/system_bridge", + "requirements": ["systembridge==1.1.3"], + "codeowners": ["@timmo001"], + "zeroconf": ["_system-bridge._udp.local."], + "after_dependencies": ["zeroconf"], + "quality_scale": "silver", + "iot_class": "local_polling" +} diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py new file mode 100644 index 00000000000..7fa9efd791e --- /dev/null +++ b/homeassistant/components/system_bridge/sensor.py @@ -0,0 +1,340 @@ +"""Support for System Bridge sensors.""" +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import Any + +from systembridge import Bridge + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, + FREQUENCY_GIGAHERTZ, + PERCENTAGE, + TEMP_CELSIUS, + VOLT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import BridgeDeviceEntity +from .const import DOMAIN + +ATTR_AVAILABLE = "available" +ATTR_FILESYSTEM = "filesystem" +ATTR_LOAD_AVERAGE = "load_average" +ATTR_LOAD_IDLE = "load_idle" +ATTR_LOAD_SYSTEM = "load_system" +ATTR_LOAD_USER = "load_user" +ATTR_MOUNT = "mount" +ATTR_SIZE = "size" +ATTR_TYPE = "type" +ATTR_USED = "used" + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up System Bridge sensor based on a config entry.""" + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + bridge: Bridge = coordinator.data + + entities = [ + BridgeCpuSpeedSensor(coordinator, bridge), + BridgeCpuTemperatureSensor(coordinator, bridge), + BridgeCpuVoltageSensor(coordinator, bridge), + *[ + BridgeFilesystemSensor(coordinator, bridge, key) + for key, _ in bridge.filesystem.fsSize.items() + ], + BridgeKernelSensor(coordinator, bridge), + BridgeOsSensor(coordinator, bridge), + BridgeProcessesLoadSensor(coordinator, bridge), + ] + + if bridge.battery.hasBattery: + entities.append(BridgeBatterySensor(coordinator, bridge)) + entities.append(BridgeBatteryTimeRemainingSensor(coordinator, bridge)) + + async_add_entities(entities) + + +class BridgeSensor(BridgeDeviceEntity, SensorEntity): + """Defines a System Bridge sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + bridge: Bridge, + key: str, + name: str, + icon: str | None, + device_class: str | None, + unit_of_measurement: str | None, + enabled_by_default: bool, + ) -> None: + """Initialize System Bridge sensor.""" + self._device_class = device_class + self._unit_of_measurement = unit_of_measurement + + super().__init__(coordinator, bridge, key, name, icon, enabled_by_default) + + @property + def device_class(self) -> str | None: + """Return the class of this sensor.""" + return self._device_class + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + +class BridgeBatterySensor(BridgeSensor): + """Defines a Battery sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "battery", + "Battery", + None, + DEVICE_CLASS_BATTERY, + PERCENTAGE, + True, + ) + + @property + def state(self) -> float: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return bridge.battery.percent + + +class BridgeBatteryTimeRemainingSensor(BridgeSensor): + """Defines the Battery Time Remaining sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "battery_time_remaining", + "Battery Time Remaining", + None, + DEVICE_CLASS_TIMESTAMP, + None, + True, + ) + + @property + def state(self) -> str | None: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + if bridge.battery.timeRemaining is None: + return None + return str(datetime.now() + timedelta(minutes=bridge.battery.timeRemaining)) + + +class BridgeCpuSpeedSensor(BridgeSensor): + """Defines a CPU speed sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "cpu_speed", + "CPU Speed", + None, + None, + FREQUENCY_GIGAHERTZ, + True, + ) + + @property + def state(self) -> float: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return bridge.cpu.currentSpeed.avg + + +class BridgeCpuTemperatureSensor(BridgeSensor): + """Defines a CPU temperature sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "cpu_temperature", + "CPU Temperature", + None, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + False, + ) + + @property + def state(self) -> float: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return bridge.cpu.temperature.main + + +class BridgeCpuVoltageSensor(BridgeSensor): + """Defines a CPU voltage sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "cpu_voltage", + "CPU Voltage", + None, + DEVICE_CLASS_VOLTAGE, + VOLT, + False, + ) + + @property + def state(self) -> float: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return bridge.cpu.cpu.voltage + + +class BridgeFilesystemSensor(BridgeSensor): + """Defines a filesystem sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge, key: str): + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + f"filesystem_{key}", + f"{key} Space Used", + None, + None, + PERCENTAGE, + True, + ) + self._key = key + + @property + def state(self) -> float: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return ( + round(bridge.filesystem.fsSize[self._key]["use"], 2) + if bridge.filesystem.fsSize[self._key]["use"] is not None + else None + ) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of the entity.""" + bridge: Bridge = self.coordinator.data + return { + ATTR_AVAILABLE: bridge.filesystem.fsSize[self._key]["available"], + ATTR_FILESYSTEM: bridge.filesystem.fsSize[self._key]["fs"], + ATTR_MOUNT: bridge.filesystem.fsSize[self._key]["mount"], + ATTR_SIZE: bridge.filesystem.fsSize[self._key]["size"], + ATTR_TYPE: bridge.filesystem.fsSize[self._key]["type"], + ATTR_USED: bridge.filesystem.fsSize[self._key]["used"], + } + + +class BridgeKernelSensor(BridgeSensor): + """Defines a kernel sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "kernel", + "Kernel", + "mdi:devices", + None, + None, + True, + ) + + @property + def state(self) -> str: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return bridge.os.kernel + + +class BridgeOsSensor(BridgeSensor): + """Defines an OS sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "os", + "Operating System", + "mdi:devices", + None, + None, + True, + ) + + @property + def state(self) -> str: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return f"{bridge.os.distro} {bridge.os.release}" + + +class BridgeProcessesLoadSensor(BridgeSensor): + """Defines a Processes Load sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "processes_load", + "Load", + "mdi:percent", + None, + PERCENTAGE, + True, + ) + + @property + def state(self) -> float: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return ( + round(bridge.processes.load.currentLoad, 2) + if bridge.processes.load.currentLoad is not None + else None + ) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of the entity.""" + bridge: Bridge = self.coordinator.data + attrs = {} + if bridge.processes.load.avgLoad is not None: + attrs[ATTR_LOAD_AVERAGE] = round(bridge.processes.load.avgLoad, 2) + if bridge.processes.load.currentLoadUser is not None: + attrs[ATTR_LOAD_USER] = round(bridge.processes.load.currentLoadUser, 2) + if bridge.processes.load.currentLoadSystem is not None: + attrs[ATTR_LOAD_SYSTEM] = round(bridge.processes.load.currentLoadSystem, 2) + if bridge.processes.load.currentLoadIdle is not None: + attrs[ATTR_LOAD_IDLE] = round(bridge.processes.load.currentLoadIdle, 2) + return attrs diff --git a/homeassistant/components/system_bridge/services.yaml b/homeassistant/components/system_bridge/services.yaml new file mode 100644 index 00000000000..0ee12b39846 --- /dev/null +++ b/homeassistant/components/system_bridge/services.yaml @@ -0,0 +1,46 @@ +send_command: + name: Send Command + description: Sends a command to the server to run. + fields: + bridge: + name: Bridge + description: The server to send the command to. + example: "" + required: true + selector: + device: + integration: system_bridge + command: + name: Command + description: Command to send to the server. + required: true + example: "echo" + selector: + text: + arguments: + name: Arguments + description: Arguments to send to the server. + required: false + default: "" + example: "hello" + selector: + text: +open: + name: Open Path/URL + description: Open a URL or file on the server using the default application. + fields: + bridge: + name: Bridge + description: The server to talk to. + example: "" + required: true + selector: + device: + integration: system_bridge + path: + name: Path/URL + description: Path/URL to open. + required: true + example: "https://www.home-assistant.io" + selector: + text: diff --git a/homeassistant/components/system_bridge/strings.json b/homeassistant/components/system_bridge/strings.json new file mode 100644 index 00000000000..eeaad92fd1b --- /dev/null +++ b/homeassistant/components/system_bridge/strings.json @@ -0,0 +1,32 @@ +{ + "title": "System Bridge", + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "flow_title": "System Bridge: {name}", + "step": { + "authenticate": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "description": "Please enter the API Key you set in your configuration for {name}." + }, + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "description": "Please enter your connection details." + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/system_bridge/translations/en.json b/homeassistant/components/system_bridge/translations/en.json new file mode 100644 index 00000000000..71d10c54476 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful", + "unknown": "Unexpected error" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "flow_title": "System Bridge: {name}", + "step": { + "authenticate": { + "data": { + "api_key": "API Key" + }, + "description": "Please enter the API Key you set in your configuration for {name}." + }, + "user": { + "data": { + "api_key": "API Key", + "host": "Host", + "port": "Port" + }, + "description": "Please enter your connection details." + } + } + }, + "title": "System Bridge" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ef577726b99..35b48cf4cb5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -240,6 +240,7 @@ FLOWS = [ "subaru", "syncthru", "synology_dsm", + "system_bridge", "tado", "tasmota", "tellduslive", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 6ea8b7d2ebb..33c08579c36 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -172,6 +172,11 @@ ZEROCONF = { "name": "smappee50*" } ], + "_system-bridge._udp.local.": [ + { + "domain": "system_bridge" + } + ], "_touch-able._tcp.local.": [ { "domain": "apple_tv" diff --git a/requirements_all.txt b/requirements_all.txt index 13b693d7480..cfe8e6f48c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2189,6 +2189,9 @@ synology-srm==0.2.0 # homeassistant.components.synology_dsm synologydsm-api==1.0.2 +# homeassistant.components.system_bridge +systembridge==1.1.3 + # homeassistant.components.tahoma tahoma-api==0.0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 274f5b1d75d..1d425345a5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1173,6 +1173,9 @@ surepy==0.6.0 # homeassistant.components.synology_dsm synologydsm-api==1.0.2 +# homeassistant.components.system_bridge +systembridge==1.1.3 + # homeassistant.components.tellduslive tellduslive==0.10.11 diff --git a/tests/components/system_bridge/__init__.py b/tests/components/system_bridge/__init__.py new file mode 100644 index 00000000000..f049f887584 --- /dev/null +++ b/tests/components/system_bridge/__init__.py @@ -0,0 +1 @@ +"""Tests for the System Bridge integration.""" diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py new file mode 100644 index 00000000000..5cd7a77d911 --- /dev/null +++ b/tests/components/system_bridge/test_config_flow.py @@ -0,0 +1,409 @@ +"""Test the System Bridge config flow.""" +from unittest.mock import patch + +from aiohttp.client_exceptions import ClientConnectionError +from systembridge.exceptions import BridgeAuthenticationException + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.system_bridge.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + +FIXTURE_MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +FIXTURE_UUID = "e91bf575-56f3-4c83-8f42-70ac17adcd33" + +FIXTURE_AUTH_INPUT = {CONF_API_KEY: "abc-123-def-456-ghi"} + +FIXTURE_USER_INPUT = { + CONF_API_KEY: "abc-123-def-456-ghi", + CONF_HOST: "test-bridge", + CONF_PORT: "9170", +} + +FIXTURE_ZEROCONF_INPUT = { + CONF_API_KEY: "abc-123-def-456-ghi", + CONF_HOST: "1.1.1.1", + CONF_PORT: "9170", +} + +FIXTURE_ZEROCONF = { + CONF_HOST: "1.1.1.1", + CONF_PORT: 9170, + "hostname": "test-bridge.local.", + "type": "_system-bridge._udp.local.", + "name": "System Bridge - test-bridge._system-bridge._udp.local.", + "properties": { + "address": "http://test-bridge:9170", + "fqdn": "test-bridge", + "host": "test-bridge", + "ip": "1.1.1.1", + "mac": FIXTURE_MAC_ADDRESS, + "port": "9170", + "uuid": FIXTURE_UUID, + }, +} + +FIXTURE_ZEROCONF_BAD = { + CONF_HOST: "1.1.1.1", + CONF_PORT: 9170, + "hostname": "test-bridge.local.", + "type": "_system-bridge._udp.local.", + "name": "System Bridge - test-bridge._system-bridge._udp.local.", + "properties": { + "something": "bad", + }, +} + +FIXTURE_OS = { + "platform": "linux", + "distro": "Ubuntu", + "release": "20.10", + "codename": "Groovy Gorilla", + "kernel": "5.8.0-44-generic", + "arch": "x64", + "hostname": "test-bridge", + "fqdn": "test-bridge.local", + "codepage": "UTF-8", + "logofile": "ubuntu", + "serial": "abcdefghijklmnopqrstuvwxyz", + "build": "", + "servicepack": "", + "uefi": True, + "users": [], +} + + +FIXTURE_NETWORK = { + "connections": [], + "gatewayDefault": "192.168.1.1", + "interfaceDefault": "wlp2s0", + "interfaces": { + "wlp2s0": { + "iface": "wlp2s0", + "ifaceName": "wlp2s0", + "ip4": "1.1.1.1", + "mac": FIXTURE_MAC_ADDRESS, + }, + }, + "stats": {}, +} + +FIXTURE_SYSTEM = { + "baseboard": { + "manufacturer": "System manufacturer", + "model": "Model", + "version": "Rev X.0x", + "serial": "1234567", + "assetTag": "", + "memMax": 134217728, + "memSlots": 4, + }, + "bios": { + "vendor": "System vendor", + "version": "12345", + "releaseDate": "2019-11-13", + "revision": "", + }, + "chassis": { + "manufacturer": "Default string", + "model": "", + "type": "Desktop", + "version": "Default string", + "serial": "Default string", + "assetTag": "", + "sku": "", + }, + "system": { + "manufacturer": "System manufacturer", + "model": "System Product Name", + "version": "System Version", + "serial": "System Serial Number", + "uuid": "abc123-def456", + "sku": "SKU", + "virtual": False, + }, + "uuid": { + "os": FIXTURE_UUID, + "hardware": "abc123-def456", + "macs": [FIXTURE_MAC_ADDRESS], + }, +} + + +FIXTURE_BASE_URL = ( + f"http://{FIXTURE_USER_INPUT[CONF_HOST]}:{FIXTURE_USER_INPUT[CONF_PORT]}" +) + +FIXTURE_ZEROCONF_BASE_URL = ( + f"http://{FIXTURE_ZEROCONF[CONF_HOST]}:{FIXTURE_ZEROCONF[CONF_PORT]}" +) + + +async def test_user_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test full user flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", json=FIXTURE_OS) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", json=FIXTURE_NETWORK) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", json=FIXTURE_SYSTEM) + + with patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "test-bridge" + assert result2["data"] == FIXTURE_USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=BridgeAuthenticationException) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=BridgeAuthenticationException) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=BridgeAuthenticationException) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=ClientConnectionError) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=ClientConnectionError) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=ClientConnectionError) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknow_error( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.system_bridge.config_flow.Bridge.async_get_os", + side_effect=Exception("Boom"), + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + +async def test_reauth_authorization_error( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test we show user form on authorization error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authenticate" + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=BridgeAuthenticationException) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=BridgeAuthenticationException) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=BridgeAuthenticationException) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "authenticate" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_connection_error( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test we show user form on connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authenticate" + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=ClientConnectionError) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=ClientConnectionError) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=ClientConnectionError) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "authenticate" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test reauth flow.""" + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=FIXTURE_UUID, data=FIXTURE_USER_INPUT + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authenticate" + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", json=FIXTURE_OS) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", json=FIXTURE_NETWORK) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", json=FIXTURE_SYSTEM) + + with patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test zeroconf flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=FIXTURE_ZEROCONF, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert not result["errors"] + + aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/os", json=FIXTURE_OS) + aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/network", json=FIXTURE_NETWORK) + aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/system", json=FIXTURE_SYSTEM) + + with patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "test-bridge" + assert result2["data"] == FIXTURE_ZEROCONF_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_cannot_connect( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test zeroconf cannot connect flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=FIXTURE_ZEROCONF, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert not result["errors"] + + aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/os", exc=ClientConnectionError) + aioclient_mock.get( + f"{FIXTURE_ZEROCONF_BASE_URL}/network", exc=ClientConnectionError + ) + aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/system", exc=ClientConnectionError) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "authenticate" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_zeroconf_bad_zeroconf_info( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test zeroconf cannot connect flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=FIXTURE_ZEROCONF_BAD, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" From ae692a003f47cca79afc828bade5f53be0d568cb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 6 May 2021 01:41:32 +0200 Subject: [PATCH 189/852] Add support for Elgato Light Strip (#49988) Co-authored-by: Paulus Schoutsen --- homeassistant/components/elgato/__init__.py | 6 +- .../components/elgato/config_flow.py | 6 +- homeassistant/components/elgato/const.py | 2 +- homeassistant/components/elgato/light.py | 100 ++++++++++++---- homeassistant/components/elgato/manifest.json | 2 +- homeassistant/components/elgato/strings.json | 8 +- .../components/elgato/translations/en.json | 8 +- tests/components/elgato/__init__.py | 40 +++++-- tests/components/elgato/test_light.py | 113 ++++++++++++++++-- tests/fixtures/elgato/settings-color.json | 10 ++ tests/fixtures/elgato/settings.json | 8 ++ tests/fixtures/elgato/state-color.json | 11 ++ 12 files changed, 249 insertions(+), 65 deletions(-) create mode 100644 tests/fixtures/elgato/settings-color.json create mode 100644 tests/fixtures/elgato/settings.json create mode 100644 tests/fixtures/elgato/state-color.json diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index 1c83844debc..21b8de53c17 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -1,4 +1,4 @@ -"""Support for Elgato Key Lights.""" +"""Support for Elgato Lights.""" import logging from elgato import Elgato, ElgatoConnectionError @@ -16,7 +16,7 @@ PLATFORMS = [LIGHT_DOMAIN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Elgato Key Light from a config entry.""" + """Set up Elgato Light from a config entry.""" session = async_get_clientsession(hass) elgato = Elgato( entry.data[CONF_HOST], @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Elgato Key Light config entry.""" + """Unload Elgato Light config entry.""" # Unload entities for this entry/device. unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index bcfcc4dcb7f..6008ccbee77 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow to configure the Elgato Key Light integration.""" +"""Config flow to configure the Elgato Light integration.""" from __future__ import annotations from typing import Any @@ -16,7 +16,7 @@ from .const import CONF_SERIAL_NUMBER, DOMAIN class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): - """Handle a Elgato Key Light config flow.""" + """Handle a Elgato Light config flow.""" VERSION = 1 @@ -91,7 +91,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): ) async def _get_elgato_serial_number(self, raise_on_progress: bool = True) -> None: - """Get device information from an Elgato Key Light device.""" + """Get device information from an Elgato Light device.""" session = async_get_clientsession(self.hass) elgato = Elgato( host=self.host, diff --git a/homeassistant/components/elgato/const.py b/homeassistant/components/elgato/const.py index 1cfb48390cf..03a52b7e305 100644 --- a/homeassistant/components/elgato/const.py +++ b/homeassistant/components/elgato/const.py @@ -1,4 +1,4 @@ -"""Constants for the Elgato Key Light integration.""" +"""Constants for the Elgato Light integration.""" # Integration domain DOMAIN = "elgato" diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index a81f5f214ba..abd1fae410e 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -1,17 +1,18 @@ -"""Support for LED lights.""" +"""Support for Elgato lights.""" from __future__ import annotations from datetime import timedelta import logging from typing import Any -from elgato import Elgato, ElgatoError, Info, State +from elgato import Elgato, ElgatoError, Info, Settings, State from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, + ATTR_HS_COLOR, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, LightEntity, ) from homeassistant.config_entries import ConfigEntry @@ -42,10 +43,11 @@ async def async_setup_entry( entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up Elgato Key Light based on a config entry.""" + """Set up Elgato Light based on a config entry.""" elgato: Elgato = hass.data[DOMAIN][entry.entry_id][DATA_ELGATO_CLIENT] info = await elgato.info() - async_add_entities([ElgatoLight(elgato, info)], True) + settings = await elgato.settings() + async_add_entities([ElgatoLight(elgato, info, settings)], True) platform = async_get_current_platform() platform.async_register_entity_service( @@ -56,18 +58,25 @@ async def async_setup_entry( class ElgatoLight(LightEntity): - """Defines a Elgato Key Light.""" + """Defines an Elgato Light.""" - def __init__( - self, - elgato: Elgato, - info: Info, - ) -> None: - """Initialize Elgato Key Light.""" - self._info: Info = info + def __init__(self, elgato: Elgato, info: Info, settings: Settings) -> None: + """Initialize Elgato Light.""" + self._info = info + self._settings = settings self._state: State | None = None self.elgato = elgato + self._min_mired = 143 + self._max_mired = 344 + self._supported_color_modes = {COLOR_MODE_COLOR_TEMP} + + # Elgato Light supporting color, have a different temperature range + if settings.power_on_hue is not None: + self._supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + self._min_mired = 153 + self._max_mired = 285 + @property def name(self) -> str: """Return the name of the entity.""" @@ -99,17 +108,38 @@ class ElgatoLight(LightEntity): @property def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" - return 143 + return self._min_mired @property def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" - return 344 + # Elgato lights with color capabilities have a different highest value + return self._max_mired @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + def supported_color_modes(self) -> set[str]: + """Flag supported color modes.""" + return self._supported_color_modes + + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + if self._state and self._state.hue is not None: + return COLOR_MODE_HS + + return COLOR_MODE_COLOR_TEMP + + @property + def hs_color(self) -> tuple[float, float] | None: + """Return the hue and saturation color value [float, float].""" + if ( + self._state is None + or self._state.hue is None + or self._state.saturation is None + ): + return None + + return (self._state.hue, self._state.saturation) @property def is_on(self) -> bool: @@ -122,22 +152,44 @@ class ElgatoLight(LightEntity): try: await self.elgato.light(on=False) except ElgatoError: - _LOGGER.error("An error occurred while updating the Elgato Key Light") + _LOGGER.error("An error occurred while updating the Elgato Light") self._state = None async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" temperature = kwargs.get(ATTR_COLOR_TEMP) + + hue = None + saturation = None + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] + brightness = None if ATTR_BRIGHTNESS in kwargs: brightness = round((kwargs[ATTR_BRIGHTNESS] / 255) * 100) + # For Elgato lights supporting color mode, but in temperature mode; + # adjusting only brightness make them jump back to color mode. + # Resending temperature prevents that. + if ( + brightness + and ATTR_HS_COLOR not in kwargs + and ATTR_COLOR_TEMP not in kwargs + and COLOR_MODE_HS in self.supported_color_modes + and self.color_mode == COLOR_MODE_COLOR_TEMP + ): + temperature = self.color_temp + try: await self.elgato.light( - on=True, brightness=brightness, temperature=temperature + on=True, + brightness=brightness, + hue=hue, + saturation=saturation, + temperature=temperature, ) except ElgatoError: - _LOGGER.error("An error occurred while updating the Elgato Key Light") + _LOGGER.error("An error occurred while updating the Elgato Light") self._state = None async def async_update(self) -> None: @@ -149,12 +201,12 @@ class ElgatoLight(LightEntity): _LOGGER.info("Connection restored") except ElgatoError as err: meth = _LOGGER.error if self._state else _LOGGER.debug - meth("An error occurred while updating the Elgato Key Light: %s", err) + meth("An error occurred while updating the Elgato Light: %s", err) self._state = None @property def device_info(self) -> DeviceInfo: - """Return device information about this Elgato Key Light.""" + """Return device information about this Elgato Light.""" return { ATTR_IDENTIFIERS: {(DOMAIN, self._info.serial_number)}, ATTR_NAME: self._info.product_name, diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index ebc337c2925..dbb83f18995 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -1,6 +1,6 @@ { "domain": "elgato", - "name": "Elgato Key Light", + "name": "Elgato Light", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/elgato", "requirements": ["elgato==2.1.0"], diff --git a/homeassistant/components/elgato/strings.json b/homeassistant/components/elgato/strings.json index 54c5f43a5da..577ed6c0206 100644 --- a/homeassistant/components/elgato/strings.json +++ b/homeassistant/components/elgato/strings.json @@ -1,17 +1,17 @@ { "config": { - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "Elgato Light: {serial_number}", "step": { "user": { - "description": "Set up your Elgato Key Light to integrate with Home Assistant.", + "description": "Set up your Elgato Light to integrate with Home Assistant.", "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" } }, "zeroconf_confirm": { - "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", - "title": "Discovered Elgato Key Light device" + "description": "Do you want to add the Elgato Light with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Elgato Light device" } }, "error": { diff --git a/homeassistant/components/elgato/translations/en.json b/homeassistant/components/elgato/translations/en.json index 7866631db42..fc75c20032c 100644 --- a/homeassistant/components/elgato/translations/en.json +++ b/homeassistant/components/elgato/translations/en.json @@ -7,18 +7,18 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "Elgato Light: {serial_number}", "step": { "user": { "data": { "host": "Host", "port": "Port" }, - "description": "Set up your Elgato Key Light to integrate with Home Assistant." + "description": "Set up your Elgato Light to integrate with Home Assistant." }, "zeroconf_confirm": { - "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", - "title": "Discovered Elgato Key Light device" + "description": "Do you want to add the Elgato Light with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Elgato Light device" } } } diff --git a/tests/components/elgato/__init__.py b/tests/components/elgato/__init__.py index ea63bc0c4d0..12df481d182 100644 --- a/tests/components/elgato/__init__.py +++ b/tests/components/elgato/__init__.py @@ -12,6 +12,8 @@ async def init_integration( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, + color: bool = False, + mode_color: bool = False, ) -> MockConfigEntry: """Set up the Elgato Key Light integration in Home Assistant.""" aioclient_mock.get( @@ -20,24 +22,38 @@ async def init_integration( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - aioclient_mock.put( - "http://127.0.0.1:9123/elgato/lights", - text=load_fixture("elgato/state.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - "http://127.0.0.1:9123/elgato/lights", - text=load_fixture("elgato/state.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( "http://127.0.0.2:9123/elgato/accessory-info", text=load_fixture("elgato/info.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) + settings = "elgato/settings.json" + if color: + settings = "elgato/settings-color.json" + + aioclient_mock.get( + "http://127.0.0.1:9123/elgato/lights/settings", + text=load_fixture(settings), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + state = "elgato/state.json" + if mode_color: + state = "elgato/state-color.json" + + aioclient_mock.get( + "http://127.0.0.1:9123/elgato/lights", + text=load_fixture(state), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.put( + "http://127.0.0.1:9123/elgato/lights", + text=load_fixture("elgato/state.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + entry = MockConfigEntry( domain=DOMAIN, unique_id="CN11A1A00001", diff --git a/tests/components/elgato/test_light.py b/tests/components/elgato/test_light.py index 38da5856f75..fbc926d318f 100644 --- a/tests/components/elgato/test_light.py +++ b/tests/components/elgato/test_light.py @@ -2,11 +2,19 @@ from unittest.mock import patch from elgato import ElgatoError +import pytest from homeassistant.components.elgato.const import DOMAIN, SERVICE_IDENTIFY from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.const import ( @@ -24,10 +32,10 @@ from tests.components.elgato import init_integration from tests.test_util.aiohttp import AiohttpClientMocker -async def test_light_state( +async def test_light_state_temperature( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: - """Test the creation and values of the Elgato Key Lights.""" + """Test the creation and values of the Elgato Lights in temperature mode.""" await init_integration(hass, aioclient_mock) entity_registry = er.async_get(hass) @@ -37,6 +45,11 @@ async def test_light_state( assert state assert state.attributes.get(ATTR_BRIGHTNESS) == 54 assert state.attributes.get(ATTR_COLOR_TEMP) == 297 + assert state.attributes.get(ATTR_HS_COLOR) is None + assert state.attributes.get(ATTR_COLOR_MODE) == COLOR_MODE_COLOR_TEMP + assert state.attributes.get(ATTR_MIN_MIREDS) == 143 + assert state.attributes.get(ATTR_MAX_MIREDS) == 344 + assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [COLOR_MODE_COLOR_TEMP] assert state.state == STATE_ON entry = entity_registry.async_get("light.frenck") @@ -44,13 +57,42 @@ async def test_light_state( assert entry.unique_id == "CN11A1A00001" -async def test_light_change_state( +async def test_light_state_color( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Elgato Lights in temperature mode.""" + await init_integration(hass, aioclient_mock, color=True, mode_color=True) + + entity_registry = er.async_get(hass) + + # First segment of the strip + state = hass.states.get("light.frenck") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 128 + assert state.attributes.get(ATTR_COLOR_TEMP) is None + assert state.attributes.get(ATTR_HS_COLOR) == (358.0, 6.0) + assert state.attributes.get(ATTR_MIN_MIREDS) == 153 + assert state.attributes.get(ATTR_MAX_MIREDS) == 285 + assert state.attributes.get(ATTR_COLOR_MODE) == COLOR_MODE_HS + assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + ] + assert state.state == STATE_ON + + entry = entity_registry.async_get("light.frenck") + assert entry + assert entry.unique_id == "CN11A1A00001" + + +async def test_light_change_state_temperature( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the change of state of a Elgato Key Light device.""" - await init_integration(hass, aioclient_mock) + await init_integration(hass, aioclient_mock, color=True, mode_color=False) state = hass.states.get("light.frenck") + assert state assert state.state == STATE_ON with patch( @@ -69,12 +111,25 @@ async def test_light_change_state( ) await hass.async_block_till_done() assert len(mock_light.mock_calls) == 1 - mock_light.assert_called_with(on=True, brightness=100, temperature=100) + mock_light.assert_called_with( + on=True, brightness=100, temperature=100, hue=None, saturation=None + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.frenck", + ATTR_BRIGHTNESS: 255, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_light.mock_calls) == 2 + mock_light.assert_called_with( + on=True, brightness=100, temperature=297, hue=None, saturation=None + ) - with patch( - "homeassistant.components.elgato.light.Elgato.light", - return_value=mock_coro(), - ) as mock_light: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -82,14 +137,46 @@ async def test_light_change_state( blocking=True, ) await hass.async_block_till_done() - assert len(mock_light.mock_calls) == 1 + assert len(mock_light.mock_calls) == 3 mock_light.assert_called_with(on=False) -async def test_light_unavailable( +async def test_light_change_state_color( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: - """Test error/unavailable handling of an Elgato Key Light.""" + """Test the color state state of a Elgato Light device.""" + await init_integration(hass, aioclient_mock, color=True) + + state = hass.states.get("light.frenck") + assert state + assert state.state == STATE_ON + + with patch( + "homeassistant.components.elgato.light.Elgato.light", + return_value=mock_coro(), + ) as mock_light: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.frenck", + ATTR_BRIGHTNESS: 255, + ATTR_HS_COLOR: (10.1, 20.2), + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_light.mock_calls) == 1 + mock_light.assert_called_with( + on=True, brightness=100, temperature=None, hue=10.1, saturation=20.2 + ) + + +@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TURN_OFF]) +async def test_light_unavailable( + service: str, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error/unavailable handling of an Elgato Light.""" await init_integration(hass, aioclient_mock) with patch( "homeassistant.components.elgato.light.Elgato.light", @@ -100,7 +187,7 @@ async def test_light_unavailable( ): await hass.services.async_call( LIGHT_DOMAIN, - SERVICE_TURN_OFF, + service, {ATTR_ENTITY_ID: "light.frenck"}, blocking=True, ) diff --git a/tests/fixtures/elgato/settings-color.json b/tests/fixtures/elgato/settings-color.json new file mode 100644 index 00000000000..14a78c6fcaf --- /dev/null +++ b/tests/fixtures/elgato/settings-color.json @@ -0,0 +1,10 @@ +{ + "powerOnBehavior": 2, + "powerOnHue": 40.0, + "powerOnSaturation": 15.0, + "powerOnBrightness": 40, + "powerOnTemperature": 0, + "switchOnDurationMs": 150, + "switchOffDurationMs": 400, + "colorChangeDurationMs": 150 +} diff --git a/tests/fixtures/elgato/settings.json b/tests/fixtures/elgato/settings.json new file mode 100644 index 00000000000..bd918e24526 --- /dev/null +++ b/tests/fixtures/elgato/settings.json @@ -0,0 +1,8 @@ +{ + "powerOnBehavior": 1, + "powerOnBrightness": 20, + "powerOnTemperature": 213, + "switchOnDurationMs": 100, + "switchOffDurationMs": 300, + "colorChangeDurationMs": 100 +} diff --git a/tests/fixtures/elgato/state-color.json b/tests/fixtures/elgato/state-color.json new file mode 100644 index 00000000000..b49a6f6dd80 --- /dev/null +++ b/tests/fixtures/elgato/state-color.json @@ -0,0 +1,11 @@ +{ + "numberOfLights": 1, + "lights": [ + { + "on": 1, + "hue": 358.0, + "saturation": 6.0, + "brightness": 50 + } + ] +} From 7dad5e8a4c97a2db5b822fb0d041e8e90a06c6be Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 6 May 2021 00:03:11 +0000 Subject: [PATCH 190/852] [ci skip] Translation update --- .../buienradar/translations/es.json | 29 +++++++++++++++++++ .../buienradar/translations/zh-Hant.json | 29 +++++++++++++++++++ .../components/flume/translations/es.json | 10 ++++++- .../components/myq/translations/es.json | 10 ++++++- 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/buienradar/translations/es.json create mode 100644 homeassistant/components/buienradar/translations/zh-Hant.json diff --git a/homeassistant/components/buienradar/translations/es.json b/homeassistant/components/buienradar/translations/es.json new file mode 100644 index 00000000000..246f58d6311 --- /dev/null +++ b/homeassistant/components/buienradar/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + }, + "error": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "C\u00f3digo del pa\u00eds para mostrar las im\u00e1genes de la c\u00e1mara.", + "delta": "Segundos entre actualizaciones de la imagen de la c\u00e1mara", + "timeframe": "Minutos de anticipaci\u00f3n para la previsi\u00f3n de precipitaciones" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/zh-Hant.json b/homeassistant/components/buienradar/translations/zh-Hant.json new file mode 100644 index 00000000000..d276c75f2db --- /dev/null +++ b/homeassistant/components/buienradar/translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "\u651d\u5f71\u6a5f\u5f71\u50cf\u986f\u793a\u4e4b\u570b\u5bb6\u4ee3\u78bc", + "delta": "\u651d\u5f71\u6a5f\u5f71\u50cf\u66f4\u65b0\u9593\u9694\u79d2\u6578", + "timeframe": "\u964d\u96e8\u9810\u5831\u63d0\u524d\u76e3\u6e2c\u5206\u9418\u6578" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index 0ff805a3591..e8baedf243c 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Esta cuenta ya est\u00e1 configurada" + "already_configured": "Esta cuenta ya est\u00e1 configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -9,6 +10,13 @@ "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "La contrase\u00f1a de {username} ya no es v\u00e1lida.", + "title": "Reautenticar tu cuenta de Flume" + }, "user": { "data": { "client_id": "Client ID", diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index a405883fd3c..08d2ccbee73 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "MyQ ya est\u00e1 configurado" + "already_configured": "MyQ ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", @@ -9,6 +10,13 @@ "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "La contrase\u00f1a de {username} ya no es v\u00e1lida.", + "title": "Reautenticar tu cuenta MyQ" + }, "user": { "data": { "password": "Contrase\u00f1a", From 0ed31b0ba7a1e68cb32dd86e88fe4e9b8c22513e Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 6 May 2021 02:19:52 +0200 Subject: [PATCH 191/852] Bump python-miio dependency (#50129) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 6566270041a..939e30edda8 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "python-miio==0.5.5"], + "requirements": ["construct==2.10.56", "python-miio==0.5.6"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index cfe8e6f48c3..14edc0b1aa6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1816,7 +1816,7 @@ python-juicenet==1.0.1 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.5 +python-miio==0.5.6 # homeassistant.components.mpd python-mpd2==3.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d425345a5a..159555d86aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -983,7 +983,7 @@ python-izone==1.1.4 python-juicenet==1.0.1 # homeassistant.components.xiaomi_miio -python-miio==0.5.5 +python-miio==0.5.6 # homeassistant.components.nest python-nest==4.1.0 From 906de230877420a87d1aff19e535d73974995764 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 May 2021 22:08:48 -0500 Subject: [PATCH 192/852] Bump sqlalchemy to 1.4.13 (#50138) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index a79a79fbc4a..6a3f6ae6b54 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.11"], + "requirements": ["sqlalchemy==1.4.13"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 1716664c129..e9805040648 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.11"], + "requirements": ["sqlalchemy==1.4.13"], "codeowners": ["@dgomes"], "iot_class": "local_polling" } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 67c8436f67a..5e4d3e2c2ff 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 scapy==2.4.5 -sqlalchemy==1.4.11 +sqlalchemy==1.4.13 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 diff --git a/requirements_all.txt b/requirements_all.txt index 14edc0b1aa6..be6b3992a11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2139,7 +2139,7 @@ spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.11 +sqlalchemy==1.4.13 # homeassistant.components.srp_energy srpenergy==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 159555d86aa..cddbaea560c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1144,7 +1144,7 @@ spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.11 +sqlalchemy==1.4.13 # homeassistant.components.srp_energy srpenergy==1.3.2 From 60b90c4546f7c164e8ae2a98921bf67c4011e6c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 May 2021 22:11:06 -0500 Subject: [PATCH 193/852] Bump zeroconf to 0.30.0 to fix thread safety race (#50130) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 6e0c50e0683..d76639f3b5b 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.29.0","pyroute2==0.5.18"], + "requirements": ["zeroconf==0.30.0","pyroute2==0.5.18"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5e4d3e2c2ff..7e90e375e80 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ sqlalchemy==1.4.13 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.29.0 +zeroconf==0.30.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index be6b3992a11..cda78d652d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2391,7 +2391,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.29.0 +zeroconf==0.30.0 # homeassistant.components.zha zha-quirks==0.0.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cddbaea560c..f56c94e8c1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1270,7 +1270,7 @@ yeelight==0.6.2 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.29.0 +zeroconf==0.30.0 # homeassistant.components.zha zha-quirks==0.0.57 From 23a2417c42e2ea9895a7b434eb0a8dcdf298d8c6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 6 May 2021 05:12:06 +0200 Subject: [PATCH 194/852] Adjust GRPC wheel build (#50119) --- azure-pipelines-wheels.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index bcf16e8dee7..760030ec3cc 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -94,7 +94,8 @@ jobs: # Write env for build settings ( - echo "GRPC_BUILD_WITH_BORING_SSL_ASM=" - echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1" + echo "GRPC_BUILD_WITH_BORING_SSL_ASM=false" + echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true" + echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true" ) > .env_file displayName: 'Prepare requirements files for Home Assistant wheels' From d6c300aeb1742a66060243246a1a61b2435c2ce7 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 5 May 2021 23:15:21 -0400 Subject: [PATCH 195/852] Fix group selector (#50088) --- homeassistant/components/group/services.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index aac3e9aad59..f1e64a60022 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -34,7 +34,7 @@ set: object: add_entities: name: Add Entities - description: List of members they will change on group listening. + description: List of members that will change on group listening. example: domain.entity_id1, domain.entity_id2 selector: object: @@ -55,5 +55,4 @@ remove: required: true example: "test_group" selector: - entity: - domain: group + object: From 7dec23d58bbd432d76b63aba3ce1699699b126ba Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 6 May 2021 06:25:28 +0200 Subject: [PATCH 196/852] Improve translation strings for MQTT config and option flows (#50018) Co-authored-by: Franck Nijhof --- homeassistant/components/mqtt/config_flow.py | 7 ++----- homeassistant/components/mqtt/strings.json | 4 +++- homeassistant/components/mqtt/translations/en.json | 10 ++++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index baa5b1cc29a..c6af0cc08b5 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -65,6 +65,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) if can_connect: + user_input[CONF_DISCOVERY] = DEFAULT_DISCOVERY return self.async_create_entry( title=user_input[CONF_BROKER], data=user_input ) @@ -76,7 +77,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): fields[vol.Required(CONF_PORT, default=1883)] = vol.Coerce(int) fields[vol.Optional(CONF_USERNAME)] = str fields[vol.Optional(CONF_PASSWORD)] = str - fields[vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY)] = bool return self.async_show_form( step_id="broker", data_schema=vol.Schema(fields), errors=errors @@ -126,7 +126,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_USERNAME: data.get(CONF_USERNAME), CONF_PASSWORD: data.get(CONF_PASSWORD), CONF_PROTOCOL: data.get(CONF_PROTOCOL), - CONF_DISCOVERY: user_input[CONF_DISCOVERY], + CONF_DISCOVERY: DEFAULT_DISCOVERY, }, ) @@ -135,9 +135,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="hassio_confirm", description_placeholders={"addon": self._hassio_discovery["addon"]}, - data_schema=vol.Schema( - {vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): bool} - ), errors=errors, ) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 2edbc86eb8c..9de9075f19d 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -51,6 +51,7 @@ "options": { "step": { "broker": { + "title": "Broker options", "description": "Please enter the connection information of your MQTT broker.", "data": { "broker": "Broker", @@ -60,7 +61,8 @@ } }, "options": { - "description": "Please select MQTT options.", + "title": "MQTT options", + "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.", "data": { "discovery": "Enable discovery", "birth_enable": "Enable birth message", diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index c8d24b78fb7..ddd2a38c3b6 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -15,7 +15,7 @@ "port": "Port", "username": "Username" }, - "description": "Please enter the connection information of your MQTT broker." + "description": "Please enter the connection information of your MQTT broker and select if MQTT discovery should be enabled (recommended). If discovery is enabled, Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually." }, "hassio_confirm": { "data": { @@ -62,7 +62,8 @@ "port": "Port", "username": "Username" }, - "description": "Please enter the connection information of your MQTT broker." + "description": "Please enter the connection information of your MQTT broker.", + "title": "Broker options" }, "options": { "data": { @@ -78,8 +79,9 @@ "will_retain": "Will message retain", "will_topic": "Will message topic" }, - "description": "Please select MQTT options." + "description": "### Discovery \n If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually. \n ### Birth message\n The birth message will be sent each time Home Assistant (re)connects to the MQTT broker. \n ### Will message \n The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.", + "title": "MQTT options" } } } -} \ No newline at end of file +} From af832e543491113a8aa4cd049e866ec783ee0b9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 May 2021 23:47:44 -0500 Subject: [PATCH 197/852] Use shared httpx client in gogogate2 (#45575) --- CODEOWNERS | 2 +- homeassistant/components/gogogate2/common.py | 10 ++++++---- homeassistant/components/gogogate2/config_flow.py | 6 +++--- homeassistant/components/gogogate2/cover.py | 2 +- homeassistant/components/gogogate2/manifest.json | 4 ++-- homeassistant/components/gogogate2/sensor.py | 2 +- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- tests/components/gogogate2/test_config_flow.py | 6 +++--- tests/components/gogogate2/test_cover.py | 4 ++-- tests/components/gogogate2/test_init.py | 2 +- tests/components/gogogate2/test_sensor.py | 4 ++-- 12 files changed, 28 insertions(+), 26 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index ae71cde1754..f241832fb4e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -179,7 +179,7 @@ homeassistant/components/gios/* @bieniu homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/goalzero/* @tkdrob -homeassistant/components/gogogate2/* @vangorra +homeassistant/components/gogogate2/* @vangorra @bdraco homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton homeassistant/components/gpsd/* @fabaff diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 8a51b210c5b..9345f8d5fed 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -6,8 +6,8 @@ from datetime import timedelta import logging from typing import Callable, NamedTuple -from gogogate2_api import AbstractGateApi, GogoGate2Api, ISmartGateApi -from gogogate2_api.common import AbstractDoor, get_door_by_id +from ismartgate import AbstractGateApi, GogoGate2Api, ISmartGateApi +from ismartgate.common import AbstractDoor, get_door_by_id from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -112,7 +113,7 @@ def get_data_update_coordinator( config_entry_data = hass.data[DOMAIN][config_entry.entry_id] if DATA_UPDATE_COORDINATOR not in config_entry_data: - api = get_api(config_entry.data) + api = get_api(hass, config_entry.data) async def async_update_data(): try: @@ -148,7 +149,7 @@ def sensor_unique_id( return f"{config_entry.unique_id}_{door.door_id}_{sensor_type}" -def get_api(config_data: dict) -> AbstractGateApi: +def get_api(hass: HomeAssistant, config_data: dict) -> AbstractGateApi: """Get an api object for config data.""" gate_class = GogoGate2Api @@ -159,4 +160,5 @@ def get_api(config_data: dict) -> AbstractGateApi: config_data[CONF_IP_ADDRESS], config_data[CONF_USERNAME], config_data[CONF_PASSWORD], + httpx_async_client=get_async_client(hass), ) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index aa6fa1988ea..94ac97a3ef2 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -2,8 +2,8 @@ import dataclasses import re -from gogogate2_api.common import AbstractInfoResponse, ApiError -from gogogate2_api.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode +from ismartgate.common import AbstractInfoResponse, ApiError +from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigFlow @@ -55,7 +55,7 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): errors = {} if user_input: - api = get_api(user_input) + api = get_api(self.hass, user_input) try: data: AbstractInfoResponse = await api.async_info() data_dict = dataclasses.asdict(data) diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index d6b7a8beb3b..0097198f1c2 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from gogogate2_api.common import AbstractDoor, DoorStatus, get_configured_doors +from ismartgate.common import AbstractDoor, DoorStatus, get_configured_doors from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 519291c40d1..a4c07fa1fb8 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -3,8 +3,8 @@ "name": "Gogogate2 and iSmartGate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["gogogate2-api==3.0.0"], - "codeowners": ["@vangorra"], + "requirements": ["ismartgate==4.0.0"], + "codeowners": ["@vangorra", "@bdraco"], "homekit": { "models": ["iSmartGate"] }, diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index 2d530d1b2f4..99edc855733 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from itertools import chain -from gogogate2_api.common import AbstractDoor, get_configured_doors +from ismartgate.common import AbstractDoor, get_configured_doors from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry diff --git a/requirements_all.txt b/requirements_all.txt index cda78d652d8..727904fc6a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -671,9 +671,6 @@ gntp==1.0.3 # homeassistant.components.goalzero goalzero==0.1.7 -# homeassistant.components.gogogate2 -gogogate2-api==3.0.0 - # homeassistant.components.google google-api-python-client==1.6.4 @@ -833,6 +830,9 @@ influxdb==5.2.3 # homeassistant.components.iperf3 iperf3==0.1.11 +# homeassistant.components.gogogate2 +ismartgate==4.0.0 + # homeassistant.components.rest jsonpath==0.82 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f56c94e8c1b..f7a63940183 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -368,9 +368,6 @@ glances_api==0.2.0 # homeassistant.components.goalzero goalzero==0.1.7 -# homeassistant.components.gogogate2 -gogogate2-api==3.0.0 - # homeassistant.components.google google-api-python-client==1.6.4 @@ -462,6 +459,9 @@ influxdb-client==1.14.0 # homeassistant.components.influxdb influxdb==5.2.3 +# homeassistant.components.gogogate2 +ismartgate==4.0.0 + # homeassistant.components.rest jsonpath==0.82 diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 621ea4d5232..3cc70ddf7ab 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -1,9 +1,9 @@ """Tests for the GogoGate2 component.""" from unittest.mock import MagicMock, patch -from gogogate2_api import GogoGate2Api -from gogogate2_api.common import ApiError -from gogogate2_api.const import GogoGate2ApiErrorCode +from ismartgate import GogoGate2Api +from ismartgate.common import ApiError +from ismartgate.const import GogoGate2ApiErrorCode from homeassistant import config_entries, setup from homeassistant.components.gogogate2.const import ( diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 41b4368c640..3a044c33a94 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -2,8 +2,8 @@ from datetime import timedelta from unittest.mock import MagicMock, patch -from gogogate2_api import GogoGate2Api, ISmartGateApi -from gogogate2_api.common import ( +from ismartgate import GogoGate2Api, ISmartGateApi +from ismartgate.common import ( ApiError, DoorMode, DoorStatus, diff --git a/tests/components/gogogate2/test_init.py b/tests/components/gogogate2/test_init.py index 7bcd2f8d2f2..1cfbf52284f 100644 --- a/tests/components/gogogate2/test_init.py +++ b/tests/components/gogogate2/test_init.py @@ -2,7 +2,7 @@ import asyncio from unittest.mock import MagicMock, patch -from gogogate2_api import GogoGate2Api +from ismartgate import GogoGate2Api import pytest from homeassistant.components.gogogate2 import DEVICE_TYPE_GOGOGATE2, async_setup_entry diff --git a/tests/components/gogogate2/test_sensor.py b/tests/components/gogogate2/test_sensor.py index 020989c003a..5adc4532750 100644 --- a/tests/components/gogogate2/test_sensor.py +++ b/tests/components/gogogate2/test_sensor.py @@ -2,8 +2,8 @@ from datetime import timedelta from unittest.mock import MagicMock, patch -from gogogate2_api import GogoGate2Api, ISmartGateApi -from gogogate2_api.common import ( +from ismartgate import GogoGate2Api, ISmartGateApi +from ismartgate.common import ( DoorMode, DoorStatus, GogoGate2ActivateResponse, From e3e92397984579cc1eb595e87d66b406fb63b8b0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 6 May 2021 07:04:09 +0200 Subject: [PATCH 198/852] Strictly type Twente Milieu integration (#50062) --- .../components/twentemilieu/sensor.py | 19 ++++++++----------- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index c0174263d23..cc17bf6f1a2 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -12,7 +12,7 @@ from twentemilieu import ( from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ID +from homeassistant.const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_NAME, CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -89,7 +89,6 @@ class TwenteMilieuSensor(SensorEntity): self._name = name self._twentemilieu = twentemilieu self._waste_type = waste_type - self._unsub_dispatcher = None self._state = None @@ -120,14 +119,12 @@ class TwenteMilieuSensor(SensorEntity): async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, DATA_UPDATE, self._schedule_immediate_update + self.async_on_remove( + async_dispatcher_connect( + self.hass, DATA_UPDATE, self._schedule_immediate_update + ) ) - async def async_will_remove_from_hass(self) -> None: - """Disconnect from update signal.""" - self._unsub_dispatcher() - @callback def _schedule_immediate_update(self, unique_id: str) -> None: """Schedule an immediate update of the entity.""" @@ -149,7 +146,7 @@ class TwenteMilieuSensor(SensorEntity): def device_info(self) -> DeviceInfo: """Return device information about Twente Milieu.""" return { - "identifiers": {(DOMAIN, self._unique_id)}, - "name": "Twente Milieu", - "manufacturer": "Twente Milieu", + ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)}, + ATTR_NAME: "Twente Milieu", + ATTR_MANUFACTURER: "Twente Milieu", } diff --git a/mypy.ini b/mypy.ini index 948b2de0e11..e457c331199 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1269,9 +1269,6 @@ ignore_errors = true [mypy-homeassistant.components.tuya.*] ignore_errors = true -[mypy-homeassistant.components.twentemilieu.*] -ignore_errors = true - [mypy-homeassistant.components.unifi.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index dc4485864f9..a1761f2847f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -226,7 +226,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.trace.*", "homeassistant.components.tradfri.*", "homeassistant.components.tuya.*", - "homeassistant.components.twentemilieu.*", "homeassistant.components.unifi.*", "homeassistant.components.upcloud.*", "homeassistant.components.updater.*", From 465161b38c3434b675e9854d80cc96ce305a479e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 6 May 2021 07:04:32 +0200 Subject: [PATCH 199/852] Deprecate Canary YAML configuration (#50078) --- homeassistant/components/canary/__init__.py | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 90854cb3fa3..c29dfeb1a71 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -28,15 +28,20 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional( + CONF_TIMEOUT, default=DEFAULT_TIMEOUT + ): cv.positive_int, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From 177317a345878fb7d6b3ebd9b6db3304d1bf1d5e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 6 May 2021 07:14:01 +0200 Subject: [PATCH 200/852] Allow passing options in config flow entry creation (#49912) --- homeassistant/config_entries.py | 26 +++++++++- homeassistant/data_entry_flow.py | 1 + .../components/config/test_config_entries.py | 2 + .../components/philips_js/test_config_flow.py | 1 + tests/components/ps4/test_init.py | 1 + tests/components/subaru/test_config_flow.py | 2 + tests/test_config_entries.py | 50 +++++++++++++++++++ 7 files changed, 81 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7275d3101a9..8c9ad8da4c5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -153,7 +153,7 @@ class ConfigEntry: data: Mapping[str, Any], source: str, system_options: dict, - options: dict | None = None, + options: Mapping[str, Any] | None = None, unique_id: str | None = None, entry_id: str | None = None, state: str = ENTRY_STATE_NOT_LOADED, @@ -631,7 +631,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): domain=result["handler"], title=result["title"], data=result["data"], - options={}, + options=result["options"], system_options={}, source=flow.context["source"], unique_id=flow.unique_id, @@ -1297,6 +1297,28 @@ class ConfigFlow(data_entry_flow.FlowHandler): """Handle a flow initialized by DHCP discovery.""" return await self.async_step_discovery(discovery_info) + @callback + def async_create_entry( # pylint: disable=arguments-differ + self, + *, + title: str, + data: Mapping[str, Any], + description: str | None = None, + description_placeholders: dict | None = None, + options: Mapping[str, Any] | None = None, + ) -> data_entry_flow.FlowResult: + """Finish config flow and create a config entry.""" + result = super().async_create_entry( + title=title, + data=data, + description=description, + description_placeholders=description_placeholders, + ) + + result["options"] = options or {} + + return result + class OptionsFlowManager(data_entry_flow.FlowManager): """Flow to set options for a configuration entry.""" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 720711e07e3..dd8b1c53a68 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -73,6 +73,7 @@ class FlowResult(TypedDict, total=False): context: dict[str, Any] result: Any last_step: bool | None + options: Mapping[str, Any] class FlowManager(abc.ABC): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 8cd86ce6df2..2763e5912fa 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -333,6 +333,7 @@ async def test_create_account(hass, client): }, "description": None, "description_placeholders": None, + "options": {}, } @@ -403,6 +404,7 @@ async def test_two_step_flow(hass, client): }, "description": None, "description_placeholders": None, + "options": {}, } diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index 48230c72dc9..4841cd5a940 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -152,6 +152,7 @@ async def test_pairing(hass, mock_tv_pairable, mock_setup_entry): "title": "55PUS7181/12 (ABCDEFGHIJKLF)", "data": MOCK_CONFIG_PAIRED, "version": 1, + "options": {}, } await hass.async_block_till_done() diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index cfe2f4b8e87..94167528b21 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -46,6 +46,7 @@ MOCK_FLOW_RESULT = { "type": data_entry_flow.RESULT_TYPE_CREATE_ENTRY, "title": "test_ps4", "data": MOCK_DATA, + "options": {}, } MOCK_ENTRY_ID = "SomeID" diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 031b9c29d09..aed15150619 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -115,6 +115,7 @@ async def test_user_form_pin_not_required(hass, user_form): "type": "create_entry", "version": 1, "data": deepcopy(TEST_CONFIG), + "options": {}, } expected["data"][CONF_PIN] = None result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID @@ -176,6 +177,7 @@ async def test_pin_form_success(hass, pin_form): "type": "create_entry", "version": 1, "data": TEST_CONFIG, + "options": {}, } result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID assert result == expected diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 9c6e469291e..0c12e69364c 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -941,6 +941,56 @@ async def test_setup_retrying_during_unload_before_started(hass): ) +async def test_create_entry_options(hass): + """Test a config entry being created with options.""" + + async def mock_async_setup(hass, config): + """Mock setup.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IMPORT}, + data={"data": "data", "option": "option"}, + ) + ) + return True + + async_setup_entry = AsyncMock(return_value=True) + mock_integration( + hass, + MockModule( + "comp", async_setup=mock_async_setup, async_setup_entry=async_setup_entry + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + await async_setup_component(hass, "persistent_notification", {}) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_import(self, user_input): + """Test import step creating entry, with options.""" + return self.async_create_entry( + title="title", + data={"example": user_input["data"]}, + options={"example": user_input["option"]}, + ) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + assert await async_setup_component(hass, "comp", {}) + + await hass.async_block_till_done() + + assert len(async_setup_entry.mock_calls) == 1 + + entries = hass.config_entries.async_entries("comp") + assert len(entries) == 1 + assert entries[0].data == {"example": "data"} + assert entries[0].options == {"example": "option"} + + async def test_entry_options(hass, manager): """Test that we can set options on an entry.""" entry = MockConfigEntry(domain="test", data={"first": True}, options=None) From 38d7652176924c9012dcd74dabc02bb49b933c04 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 6 May 2021 16:43:14 +0200 Subject: [PATCH 201/852] Fix zwave_js websocket api KeyError on unloaded entry (#50154) --- .../components/websocket_api/const.py | 1 + homeassistant/components/zwave_js/api.py | 62 ++-- tests/components/zwave_js/test_api.py | 351 ++++++++++++++++-- 3 files changed, 350 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 7c3f18f856c..0681a422db1 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -23,6 +23,7 @@ MAX_PENDING_MSG = 2048 ERR_ID_REUSE = "id_reuse" ERR_INVALID_FORMAT = "invalid_format" ERR_NOT_FOUND = "not_found" +ERR_NOT_LOADED = "not_loaded" ERR_NOT_SUPPORTED = "not_supported" ERR_HOME_ASSISTANT_ERROR = "home_assistant_error" ERR_UNKNOWN_COMMAND = "unknown_command" diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 3fd443e5643..7eba39d1b7c 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -22,10 +22,11 @@ from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.components.websocket_api.const import ( ERR_NOT_FOUND, + ERR_NOT_LOADED, ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ENTRY_STATE_LOADED, ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -83,6 +84,13 @@ def async_get_entry(orig_func: Callable) -> Callable: msg[ID], ERR_NOT_FOUND, f"Config entry {entry_id} not found" ) return + + if entry.state != ENTRY_STATE_LOADED: + connection.send_error( + msg[ID], ERR_NOT_LOADED, f"Config entry {entry_id} not loaded" + ) + return + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] await orig_func(hass, connection, msg, entry, client) @@ -137,17 +145,20 @@ def async_register_api(hass: HomeAssistant) -> None: hass.http.register_view(DumpView) # type: ignore -@websocket_api.require_admin +@websocket_api.require_admin # type: ignore +@websocket_api.async_response @websocket_api.websocket_command( {vol.Required(TYPE): "zwave_js/network_status", vol.Required(ENTRY_ID): str} ) -@callback -def websocket_network_status( - hass: HomeAssistant, connection: ActiveConnection, msg: dict +@async_get_entry +async def websocket_network_status( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, ) -> None: """Get the status of the Z-Wave JS network.""" - entry_id = msg[ENTRY_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] data = { "client": { "ws_server_url": client.ws_server_url, @@ -166,6 +177,7 @@ def websocket_network_status( ) +@websocket_api.async_response # type: ignore @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/node_status", @@ -173,20 +185,14 @@ def websocket_network_status( vol.Required(NODE_ID): int, } ) -@callback -def websocket_node_status( - hass: HomeAssistant, connection: ActiveConnection, msg: dict +@async_get_node +async def websocket_node_status( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, ) -> None: """Get the status of a Z-Wave JS node.""" - entry_id = msg[ENTRY_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - node_id = msg[NODE_ID] - node = client.driver.controller.nodes.get(node_id) - - if node is None: - connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found") - return - data = { "node_id": node.node_id, "is_routing": node.is_routing, @@ -537,7 +543,8 @@ async def websocket_set_config_parameter( ) -@websocket_api.require_admin +@websocket_api.require_admin # type: ignore +@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/get_config_parameters", @@ -545,20 +552,11 @@ async def websocket_set_config_parameter( vol.Required(NODE_ID): int, } ) -@callback -def websocket_get_config_parameters( - hass: HomeAssistant, connection: ActiveConnection, msg: dict +@async_get_node +async def websocket_get_config_parameters( + hass: HomeAssistant, connection: ActiveConnection, msg: dict, node: Node ) -> None: """Get a list of configuration parameters for a Z-Wave node.""" - entry_id = msg[ENTRY_ID] - node_id = msg[NODE_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - node = client.driver.controller.nodes.get(node_id) - - if node is None: - connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found") - return - values = node.get_configuration_values() result = {} for value_id, zwave_value in values.items(): diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 0a88a8e02ff..e471b1dd1a7 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -6,7 +6,7 @@ from zwave_js_server.const import LogLevel from zwave_js_server.event import Event from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed -from homeassistant.components.websocket_api.const import ERR_NOT_FOUND +from homeassistant.components.websocket_api.const import ERR_NOT_FOUND, ERR_NOT_LOADED from homeassistant.components.zwave_js.api import ( COMMAND_CLASS_ID, CONFIG, @@ -31,8 +31,8 @@ from homeassistant.components.zwave_js.const import ( from homeassistant.helpers import device_registry as dr -async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): - """Test the network and node status websocket commands.""" +async def test_network_status(hass, integration, hass_ws_client): + """Test the network status websocket command.""" entry = integration ws_client = await hass_ws_client(hass) @@ -45,6 +45,24 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): assert result["client"]["ws_server_url"] == "ws://test:3000/zjs" assert result["client"]["server_version"] == "1.0.0" + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 3, TYPE: "zwave_js/network_status", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_node_status(hass, integration, multisensor_6, hass_ws_client): + """Test the node status websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + node = multisensor_6 await ws_client.send_json( { @@ -63,33 +81,10 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): assert not result["is_secure"] assert result["status"] == 1 - # Test getting configuration parameter values - await ws_client.send_json( - { - ID: 4, - TYPE: "zwave_js/get_config_parameters", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, - } - ) - msg = await ws_client.receive_json() - result = msg["result"] - - assert len(result) == 61 - key = "52-112-0-2" - assert result[key]["property"] == 2 - assert result[key]["property_key"] is None - assert result[key]["metadata"]["type"] == "number" - assert result[key]["configuration_value_type"] == "enumerated" - assert result[key]["metadata"]["states"] - - key = "52-112-0-201-255" - assert result[key]["property_key"] == 255 - # Test getting non-existent node fails await ws_client.send_json( { - ID: 5, + ID: 4, TYPE: "zwave_js/node_status", ENTRY_ID: entry.entry_id, NODE_ID: 99999, @@ -99,18 +94,22 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND - # Test getting non-existent node config params fails + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + await ws_client.send_json( { - ID: 6, - TYPE: "zwave_js/get_config_parameters", + ID: 5, + TYPE: "zwave_js/node_status", ENTRY_ID: entry.entry_id, - NODE_ID: 99999, + NODE_ID: node.node_id, } ) msg = await ws_client.receive_json() + assert not msg["success"] - assert msg["error"]["code"] == ERR_NOT_FOUND + assert msg["error"]["code"] == ERR_NOT_LOADED async def test_add_node( @@ -145,6 +144,29 @@ async def test_add_node( client.driver.receive_event(nortek_thermostat_added_event) msg = await ws_client.receive_json() assert msg["event"]["event"] == "node added" + node_details = { + "node_id": 53, + "status": 0, + "ready": False, + } + assert msg["event"]["node"] == node_details + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "device registered" + # Check the keys of the device item + assert list(msg["event"]["device"]) == ["name", "id"] + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 4, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED async def test_cancel_inclusion_exclusion(hass, integration, client, hass_ws_client): @@ -168,6 +190,26 @@ async def test_cancel_inclusion_exclusion(hass, integration, client, hass_ws_cli msg = await ws_client.receive_json() assert msg["success"] + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 6, TYPE: "zwave_js/stop_inclusion", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + await ws_client.send_json( + {ID: 7, TYPE: "zwave_js/stop_exclusion", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_remove_node( hass, @@ -226,6 +268,18 @@ async def test_remove_node( ) assert device is None + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 4, TYPE: "zwave_js/remove_node", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_refresh_node_info( hass, client, integration, hass_ws_client, multisensor_6 @@ -295,6 +349,36 @@ async def test_refresh_node_info( client.async_send_command_no_wait.reset_mock() + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/refresh_node_info", + ENTRY_ID: entry.entry_id, + NODE_ID: 9999, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/refresh_node_info", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_refresh_node_values( hass, client, integration, hass_ws_client, multisensor_6 @@ -391,6 +475,38 @@ async def test_refresh_node_cc_values( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/refresh_node_cc_values", + ENTRY_ID: entry.entry_id, + NODE_ID: 9999, + COMMAND_CLASS_ID: 112, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/refresh_node_cc_values", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + COMMAND_CLASS_ID: 112, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_set_config_parameter( hass, client, hass_ws_client, multisensor_6, integration @@ -510,6 +626,103 @@ async def test_set_config_parameter( assert msg["error"]["code"] == "unknown_error" assert msg["error"]["message"] == "test" + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 9999, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_get_config_parameters(hass, integration, multisensor_6, hass_ws_client): + """Test the get config parameters websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + node = multisensor_6 + + # Test getting configuration parameter values + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/get_config_parameters", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert len(result) == 61 + key = "52-112-0-2" + assert result[key]["property"] == 2 + assert result[key]["property_key"] is None + assert result[key]["metadata"]["type"] == "number" + assert result[key]["configuration_value_type"] == "enumerated" + assert result[key]["metadata"]["states"] + + key = "52-112-0-201-255" + assert result[key]["property_key"] == 255 + + # Test getting non-existent node config params fails + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/get_config_parameters", + ENTRY_ID: entry.entry_id, + NODE_ID: 99999, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/get_config_parameters", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_dump_view(integration, hass_client): """Test the HTTP dump view.""" @@ -571,6 +784,18 @@ async def test_subscribe_logs(hass, integration, client, hass_ws_client): "timestamp": "time", } + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 2, TYPE: "zwave_js/subscribe_logs", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_update_log_config(hass, client, integration, hass_ws_client): """Test that the update_log_config WS API call works and that schema validation works.""" @@ -691,6 +916,23 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): and "must be provided if logging to file" in msg["error"]["message"] ) + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/update_log_config", + ENTRY_ID: entry.entry_id, + CONFIG: {LEVEL: "Error"}, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_get_log_config(hass, client, integration, hass_ws_client): """Test that the get_log_config WS API call works.""" @@ -726,6 +968,22 @@ async def test_get_log_config(hass, client, integration, hass_ws_client): assert log_config["filename"] == "/test.txt" assert log_config["force_console"] is False + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/get_log_config", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_data_collection(hass, client, integration, hass_ws_client): """Test that the data collection WS API commands work.""" @@ -794,3 +1052,32 @@ async def test_data_collection(hass, client, integration, hass_ws_client): assert not entry.data[CONF_DATA_COLLECTION_OPTED_IN] client.async_send_command.reset_mock() + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/data_collection_status", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/update_data_collection_preference", + ENTRY_ID: entry.entry_id, + OPTED_IN: True, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED From ce692afead189ac63a2aa1f5b286caae8581e8e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 May 2021 09:50:28 -0500 Subject: [PATCH 202/852] Add rainmachine discovery (#49970) Co-authored-by: Paulus Schoutsen --- .../components/rainmachine/__init__.py | 56 ++-- .../components/rainmachine/config_flow.py | 143 ++++++++--- homeassistant/components/rainmachine/const.py | 1 - .../components/rainmachine/manifest.json | 11 +- .../components/rainmachine/strings.json | 1 + .../rainmachine/translations/en.json | 1 + homeassistant/generated/zeroconf.py | 6 + .../rainmachine/test_config_flow.py | 239 +++++++++++++++--- 8 files changed, 352 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 8c72922699e..66fcc8939fb 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -24,7 +24,9 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, UpdateFailed, ) +from homeassistant.util.network import is_ip_address +from .config_flow import get_client_controller from .const import ( CONF_ZONE_RUN_TIME, DATA_CONTROLLER, @@ -38,8 +40,6 @@ from .const import ( LOGGER, ) -DATA_LISTENER = "listener" - DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" DEFAULT_ICON = "mdi:water" DEFAULT_SSL = True @@ -70,32 +70,10 @@ async def async_update_programs_and_zones( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the RainMachine component.""" - hass.data[DOMAIN] = {DATA_CONTROLLER: {}, DATA_COORDINATOR: {}, DATA_LISTENER: {}} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up RainMachine as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_CONTROLLER: {}, DATA_COORDINATOR: {}}) hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} - - entry_updates = {} - if not entry.unique_id: - # If the config entry doesn't already have a unique ID, set one: - entry_updates["unique_id"] = entry.data[CONF_IP_ADDRESS] - if CONF_ZONE_RUN_TIME in entry.data: - # If a zone run time exists in the config entry's data, pop it and move it to - # options: - data = {**entry.data} - entry_updates["data"] = data - entry_updates["options"] = { - **entry.options, - CONF_ZONE_RUN_TIME: data.pop(CONF_ZONE_RUN_TIME), - } - if entry_updates: - hass.config_entries.async_update_entry(entry, **entry_updates) - websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -107,14 +85,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ssl=entry.data.get(CONF_SSL, DEFAULT_SSL), ) except RainMachineError as err: - LOGGER.error("An error occurred: %s", err) raise ConfigEntryNotReady from err # regenmaschine can load multiple controllers at once, but we only grab the one # we loaded above: - controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] = next( - iter(client.controllers.values()) - ) + controller = hass.data[DOMAIN][DATA_CONTROLLER][ + entry.entry_id + ] = get_client_controller(client) + + entry_updates = {} + if not entry.unique_id or is_ip_address(entry.unique_id): + # If the config entry doesn't already have a unique ID, set one: + entry_updates["unique_id"] = controller.mac + if CONF_ZONE_RUN_TIME in entry.data: + # If a zone run time exists in the config entry's data, pop it and move it to + # options: + data = {**entry.data} + entry_updates["data"] = data + entry_updates["options"] = { + **entry.options, + CONF_ZONE_RUN_TIME: data.pop(CONF_ZONE_RUN_TIME), + } + if entry_updates: + hass.config_entries.async_update_entry(entry, **entry_updates) async def async_update(api_category: str) -> dict: """Update the appropriate API data based on a category.""" @@ -158,7 +151,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True @@ -168,9 +161,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) - cancel_listener() - return unload_ok diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 37b7da4b56b..8ef21fb185e 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -6,7 +6,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_ZONE_RUN_TIME, DEFAULT_PORT, DEFAULT_ZONE_RUN, DOMAIN @@ -19,58 +21,129 @@ DATA_SCHEMA = vol.Schema( ) +def get_client_controller(client): + """Enumerate controllers to find the first mac.""" + for controller in client.controllers.values(): + return controller + + +async def async_get_controller(hass, ip_address, password, port, ssl): + """Auth and fetch the mac address from the controller.""" + websession = aiohttp_client.async_get_clientsession(hass) + client = Client(session=websession) + try: + await client.load_local(ip_address, password, port=port, ssl=ssl) + except RainMachineError: + return None + else: + return get_client_controller(client) + + class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a RainMachine config flow.""" VERSION = 1 + def __init__(self): + """Initialize config flow.""" + self.discovered_ip_address = None + @staticmethod @callback def async_get_options_flow(config_entry): """Define the config flow to handle options.""" return RainMachineOptionsFlowHandler(config_entry) + @callback + def _async_abort_ip_address_configured(self, ip_address): + """Abort if we already have an entry for the ip.""" + # IP already configured + for entry in self._async_current_entries(include_ignore=False): + if ip_address == entry.data[CONF_IP_ADDRESS]: + raise AbortFlow("already_configured") + + async def async_step_homekit(self, discovery_info): + """Handle a flow initialized by homekit discovery.""" + return await self.async_step_zeroconf(discovery_info) + + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + """Handle discovery via zeroconf.""" + ip_address = discovery_info["host"] + + self._async_abort_ip_address_configured(ip_address) + # Handle IP change + for entry in self._async_current_entries(include_ignore=False): + # Try our existing credentials to check for ip change + if controller := await async_get_controller( + self.hass, + ip_address, + entry.data[CONF_PASSWORD], + entry.data[CONF_PORT], + entry.data.get(CONF_SSL, True), + ): + await self.async_set_unique_id(controller.mac) + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: ip_address} + ) + + # A new rain machine: We will change out the unique id + # for the mac address once we authenticate, however we want to + # prevent multiple different rain machines on the same network + # from being shown in discovery + await self.async_set_unique_id(ip_address) + self._abort_if_unique_id_configured() + self.discovered_ip_address = ip_address + return await self.async_step_user() + + @callback + def _async_generate_schema(self): + """Generate schema.""" + return vol.Schema( + { + vol.Required(CONF_IP_ADDRESS, default=self.discovered_ip_address): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + } + ) + async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - if not user_input: - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors={} - ) - - await self.async_set_unique_id(user_input[CONF_IP_ADDRESS]) - self._abort_if_unique_id_configured() - - websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(session=websession) - - try: - await client.load_local( + errors = {} + if user_input: + self._async_abort_ip_address_configured(user_input[CONF_IP_ADDRESS]) + controller = await async_get_controller( + self.hass, user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], - port=user_input[CONF_PORT], - ssl=user_input.get(CONF_SSL, True), - ) - except RainMachineError: - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={CONF_PASSWORD: "invalid_auth"}, + user_input[CONF_PORT], + user_input.get(CONF_SSL, True), ) + if controller: + await self.async_set_unique_id(controller.mac) + self._abort_if_unique_id_configured() - # Unfortunately, RainMachine doesn't provide a way to refresh the - # access token without using the IP address and password, so we have to - # store it: - return self.async_create_entry( - title=user_input[CONF_IP_ADDRESS], - data={ - CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_PORT: user_input[CONF_PORT], - CONF_SSL: user_input.get(CONF_SSL, True), - CONF_ZONE_RUN_TIME: user_input.get( - CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN - ), - }, + # Unfortunately, RainMachine doesn't provide a way to refresh the + # access token without using the IP address and password, so we have to + # store it: + return self.async_create_entry( + title=controller.name, + data={ + CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_PORT: user_input[CONF_PORT], + CONF_SSL: user_input.get(CONF_SSL, True), + CONF_ZONE_RUN_TIME: user_input.get( + CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN + ), + }, + ) + + errors = {CONF_PASSWORD: "invalid_auth"} + + if self.discovered_ip_address: + self.context["title_placeholders"] = {"ip": self.discovered_ip_address} + return self.async_show_form( + step_id="user", data_schema=self._async_generate_schema(), errors=errors ) diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index 568108e23a6..56c1660a0ba 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -9,7 +9,6 @@ CONF_ZONE_RUN_TIME = "zone_run_time" DATA_CONTROLLER = "controller" DATA_COORDINATOR = "coordinator" -DATA_LISTENER = "listener" DATA_PROGRAMS = "programs" DATA_PROVISION_SETTINGS = "provision.settings" DATA_RESTRICTIONS_CURRENT = "restrictions.current" diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 17429a74d40..b6021d02c39 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -5,5 +5,14 @@ "documentation": "https://www.home-assistant.io/integrations/rainmachine", "requirements": ["regenmaschine==3.0.0"], "codeowners": ["@bachya"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "homekit": { + "models": ["Touch HD", "SPK5"] + }, + "zeroconf": [ + { + "type": "_http._tcp.local.", + "name": "rainmachine*" + } + ] } diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index 1f5a21d37d8..ec65d1c7c09 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "RainMachine {ip}", "step": { "user": { "title": "Fill in your information", diff --git a/homeassistant/components/rainmachine/translations/en.json b/homeassistant/components/rainmachine/translations/en.json index f65463626e4..0c8b6eef766 100644 --- a/homeassistant/components/rainmachine/translations/en.json +++ b/homeassistant/components/rainmachine/translations/en.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Invalid authentication" }, + "flow_title": "RainMachine {ip}", "step": { "user": { "data": { diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 33c08579c36..0b1c0adb9c6 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -98,6 +98,10 @@ ZEROCONF = { "domain": "rachio", "name": "rachio*" }, + { + "domain": "rainmachine", + "name": "rainmachine*" + }, { "domain": "shelly", "name": "shelly*" @@ -217,9 +221,11 @@ HOMEKIT = { "PowerView": "hunterdouglas_powerview", "Presence": "netatmo", "Rachio": "rachio", + "SPK5": "rainmachine", "Smart Bridge": "lutron_caseta", "Socket": "wemo", "TRADFRI": "tradfri", + "Touch HD": "rainmachine", "Welcome": "netatmo", "Wemo": "wemo", "iSmartGate": "gogogate2", diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index e79874831fe..1a015a5b181 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -1,16 +1,25 @@ """Define tests for the OpenUV config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch +import pytest from regenmaschine.errors import RainMachineError -from homeassistant import data_entry_flow -from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN, config_flow -from homeassistant.config_entries import SOURCE_USER +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL from tests.common import MockConfigEntry +def _get_mock_client(): + mock_controller = Mock() + mock_controller.name = "My Rain Machine" + mock_controller.mac = "aa:bb:cc:dd:ee:ff" + return Mock( + load_local=AsyncMock(), controllers={"aa:bb:cc:dd:ee:ff": mock_controller} + ) + + async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" conf = { @@ -20,13 +29,19 @@ async def test_duplicate_error(hass): CONF_SSL: True, } - MockConfigEntry(domain=DOMAIN, unique_id="192.168.1.100", data=conf).add_to_hass( - hass - ) + MockConfigEntry( + domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=conf + ).add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf - ) + with patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=conf, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -40,16 +55,18 @@ async def test_invalid_password(hass): CONF_SSL: True, } - flow = config_flow.RainMachineFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} - with patch( "regenmaschine.client.Client.load_local", side_effect=RainMachineError, ): - result = await flow.async_step_user(user_input=conf) - assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=conf, + ) + await hass.async_block_till_done() + + assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} async def test_options_flow(hass): @@ -88,11 +105,11 @@ async def test_options_flow(hass): async def test_show_form(hass): """Test that the form is served with no input.""" - flow = config_flow.RainMachineFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} - - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=None, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -107,22 +124,172 @@ async def test_step_user(hass): CONF_SSL: True, } - flow = config_flow.RainMachineFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} + with patch( + "homeassistant.components.rainmachine.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=conf, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My Rain Machine" + assert result["data"] == { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + CONF_ZONE_RUN_TIME: 600, + } + assert mock_setup_entry.called + + +@pytest.mark.parametrize( + "source", [config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_HOMEKIT] +) +async def test_step_homekit_zeroconf_ip_already_exists(hass, source): + """Test homekit and zeroconf with an ip that already exists.""" + conf = { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + } + + MockConfigEntry( + domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=conf + ).add_to_hass(hass) with patch( - "regenmaschine.client.Client.load_local", - return_value=True, + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), ): - result = await flow.async_step_user(user_input=conf) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data={"host": "192.168.1.100"}, + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "192.168.1.100" - assert result["data"] == { - CONF_IP_ADDRESS: "192.168.1.100", - CONF_PASSWORD: "password", - CONF_PORT: 8080, - CONF_SSL: True, - CONF_ZONE_RUN_TIME: 600, - } + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + "source", [config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_HOMEKIT] +) +async def test_step_homekit_zeroconf_ip_change(hass, source): + """Test zeroconf with an ip change.""" + conf = { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + } + + entry = MockConfigEntry(domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=conf) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data={"host": "192.168.1.2"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "192.168.1.2" + + +@pytest.mark.parametrize( + "source", [config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_HOMEKIT] +) +async def test_step_homekit_zeroconf_new_controller_when_some_exist(hass, source): + """Test homekit and zeroconf for a new controller when one already exists.""" + existing_conf = { + CONF_IP_ADDRESS: "192.168.1.3", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + } + entry = MockConfigEntry( + domain=DOMAIN, unique_id="zz:bb:cc:dd:ee:ff", data=existing_conf + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data={"host": "192.168.1.100"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.rainmachine.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "My Rain Machine" + assert result2["data"] == { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + CONF_ZONE_RUN_TIME: 600, + } + assert mock_setup_entry.called + + +async def test_discovery_by_homekit_and_zeroconf_same_time(hass): + """Test the same controller gets discovered by two different methods.""" + + with patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={"host": "192.168.1.100"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"host": "192.168.1.100"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" From 33e044431e095eac5a46410a2b2576894a880e72 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 6 May 2021 23:34:50 +0200 Subject: [PATCH 203/852] Bump PyRMVtransport to 0.2.3 (#50183) --- homeassistant/components/rmvtransport/manifest.json | 10 +++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index a2e91b9a01c..a8e2b253324 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -2,7 +2,11 @@ "domain": "rmvtransport", "name": "RMV", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", - "requirements": ["PyRMVtransport==0.3.1"], - "codeowners": ["@cgtobi"], + "requirements": [ + "PyRMVtransport==0.3.2" + ], + "codeowners": [ + "@cgtobi" + ], "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 727904fc6a3..e416ac8631c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.3.1 +PyRMVtransport==0.3.2 # homeassistant.components.telegram_bot PySocks==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7a63940183..72f745c92fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,7 +21,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.3.1 +PyRMVtransport==0.3.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 0bfc386be37ed137c51797a04833e2ee7ed6368e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 7 May 2021 01:23:11 +0200 Subject: [PATCH 204/852] Upgrade TwitterAPI to 2.7.3 (#50195) --- homeassistant/components/twitter/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index c25ce304ae0..077e72f5485 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -2,7 +2,7 @@ "domain": "twitter", "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", - "requirements": ["TwitterAPI==2.7.2"], + "requirements": ["TwitterAPI==2.7.3"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index e416ac8631c..7446c8b9487 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -78,7 +78,7 @@ RtmAPI==0.7.2 TravisPy==0.3.5 # homeassistant.components.twitter -TwitterAPI==2.7.2 +TwitterAPI==2.7.3 # homeassistant.components.tof # VL53L1X2==0.1.5 From 623a9c99fe500c008d7e624cd2b518ff108283cb Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 7 May 2021 00:04:03 +0000 Subject: [PATCH 205/852] [ci skip] Translation update --- .../buienradar/translations/it.json | 29 +++++++++++++++++ .../components/elgato/translations/ca.json | 8 ++--- .../components/elgato/translations/et.json | 8 ++--- .../components/elgato/translations/it.json | 8 ++--- .../components/elgato/translations/ru.json | 8 ++--- .../elgato/translations/zh-Hant.json | 8 ++--- .../components/flume/translations/it.json | 10 +++++- .../components/fritzbox/translations/it.json | 2 +- .../components/mqtt/translations/ca.json | 24 +++++++------- .../components/mqtt/translations/en.json | 6 ++-- .../components/mqtt/translations/es.json | 6 ++-- .../components/mqtt/translations/et.json | 6 ++-- .../components/mqtt/translations/it.json | 16 ++++++---- .../components/mqtt/translations/nl.json | 6 ++-- .../components/mqtt/translations/ru.json | 8 +++-- .../components/myq/translations/it.json | 10 +++++- .../components/onvif/translations/es.json | 2 +- .../rainmachine/translations/ca.json | 1 + .../rainmachine/translations/et.json | 1 + .../rainmachine/translations/it.json | 1 + .../rainmachine/translations/ru.json | 1 + .../system_bridge/translations/ca.json | 32 +++++++++++++++++++ .../system_bridge/translations/es.json | 30 +++++++++++++++++ .../system_bridge/translations/et.json | 32 +++++++++++++++++++ .../system_bridge/translations/it.json | 32 +++++++++++++++++++ .../system_bridge/translations/nl.json | 32 +++++++++++++++++++ .../system_bridge/translations/ru.json | 32 +++++++++++++++++++ .../system_bridge/translations/zh-Hant.json | 32 +++++++++++++++++++ .../components/weather/translations/de.json | 2 +- 29 files changed, 338 insertions(+), 55 deletions(-) create mode 100644 homeassistant/components/buienradar/translations/it.json create mode 100644 homeassistant/components/system_bridge/translations/ca.json create mode 100644 homeassistant/components/system_bridge/translations/es.json create mode 100644 homeassistant/components/system_bridge/translations/et.json create mode 100644 homeassistant/components/system_bridge/translations/it.json create mode 100644 homeassistant/components/system_bridge/translations/nl.json create mode 100644 homeassistant/components/system_bridge/translations/ru.json create mode 100644 homeassistant/components/system_bridge/translations/zh-Hant.json diff --git a/homeassistant/components/buienradar/translations/it.json b/homeassistant/components/buienradar/translations/it.json new file mode 100644 index 00000000000..85573ead2fc --- /dev/null +++ b/homeassistant/components/buienradar/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "error": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Codice del paese per visualizzare le immagini della telecamera.", + "delta": "Intervallo di tempo in secondi tra gli aggiornamenti delle immagini della telecamera", + "timeframe": "Minuti da considerare per le previsioni delle precipitazioni" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/ca.json b/homeassistant/components/elgato/translations/ca.json index f662da3b0cc..2302b833481 100644 --- a/homeassistant/components/elgato/translations/ca.json +++ b/homeassistant/components/elgato/translations/ca.json @@ -7,18 +7,18 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "Elgato Light: {serial_number}", "step": { "user": { "data": { "host": "Amfitri\u00f3", "port": "Port" }, - "description": "Configura l'Elgato Key Light per integrar-lo amb Home Assistant." + "description": "Configura l'Elgato Light per integrar-lo amb Home Assistant." }, "zeroconf_confirm": { - "description": "Vols afegir l'Elgato Key Light amb n\u00famero de s\u00e8rie `{serial_number}` a Home Assistant?", - "title": "Dispositiu Elgato Key Light descobert" + "description": "Vols afegir a Home Assistant l'Elgato Light amb n\u00famero de s\u00e8rie `{serial_number}`?", + "title": "Dispositiu Elgato Light descobert" } } } diff --git a/homeassistant/components/elgato/translations/et.json b/homeassistant/components/elgato/translations/et.json index 61357ccb601..da01933c787 100644 --- a/homeassistant/components/elgato/translations/et.json +++ b/homeassistant/components/elgato/translations/et.json @@ -7,18 +7,18 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "Elgato Light: {serial_number}", "step": { "user": { "data": { "host": "", "port": "" }, - "description": "Seadista oma Elegato Key Light sidumine Home Assistant-iga." + "description": "Seadista oma Elegato Light sidumine Home Assistant-iga." }, "zeroconf_confirm": { - "description": "Kas soovite lisada Home Assistanti Elegato Key Light'i seerianumbriga \" {serial_number} \"?", - "title": "Leitud Elegato Key Light seade" + "description": "Kas soovid lisada Home Assistanti Elegato Light'i seerianumbriga \" {serial_number} \"?", + "title": "Leitud ElegatoLight seade" } } } diff --git a/homeassistant/components/elgato/translations/it.json b/homeassistant/components/elgato/translations/it.json index 00c0937e008..b23a0aa9392 100644 --- a/homeassistant/components/elgato/translations/it.json +++ b/homeassistant/components/elgato/translations/it.json @@ -7,18 +7,18 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "Elgato Light: {serial_number}", "step": { "user": { "data": { "host": "Host", "port": "Porta" }, - "description": "Configura Elgato Key Light per l'integrazione con Home Assistant." + "description": "Configura il tuo Elgato Light per l'integrazione con Home Assistant." }, "zeroconf_confirm": { - "description": "Vuoi aggiungere il dispositivo Elgato Key Light con il numero di serie `{serial_number}` a Home Assistant?", - "title": "Dispositivo Elgato Key Light rilevato" + "description": "Vuoi aggiungere Elgato Light con il numero di serie `{serial_number}` a Home Assistant?", + "title": "Rilevato il dispositivo Elgato Light" } } } diff --git a/homeassistant/components/elgato/translations/ru.json b/homeassistant/components/elgato/translations/ru.json index 7a4cefe2797..e3af9572232 100644 --- a/homeassistant/components/elgato/translations/ru.json +++ b/homeassistant/components/elgato/translations/ru.json @@ -7,18 +7,18 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "Elgato Light: {serial_number}", "step": { "user": { "data": { "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Elgato Key Light \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Elgato Light \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant." }, "zeroconf_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Elgato Key Light \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", - "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgato Key Light" + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Elgato Light \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgato Light" } } } diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json index 6f113fed4a5..f1180a719ba 100644 --- a/homeassistant/components/elgato/translations/zh-Hant.json +++ b/homeassistant/components/elgato/translations/zh-Hant.json @@ -7,18 +7,18 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "Elgato Key \u7167\u660e\uff1a{serial_number}", + "flow_title": "Elgato \u7167\u660e\uff1a{serial_number}", "step": { "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, - "description": "\u8a2d\u5b9a Elgato Key \u7167\u660e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" + "description": "\u8a2d\u5b9a Elgato \u7167\u660e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Elgato Key \u7167\u660e\u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Elgato Key \u7167\u660e\u88dd\u7f6e" + "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Elgato \u7167\u660e\u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Elgato \u7167\u660e\u88dd\u7f6e" } } } diff --git a/homeassistant/components/flume/translations/it.json b/homeassistant/components/flume/translations/it.json index 43b82331840..3fdca3a5cb4 100644 --- a/homeassistant/components/flume/translations/it.json +++ b/homeassistant/components/flume/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +10,13 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "La password per {username} non \u00e8 pi\u00f9 valida.", + "title": "Riautentica il tuo account Flume" + }, "user": { "data": { "client_id": "Client ID", diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json index 6aba6a007d7..da01b34ad02 100644 --- a/homeassistant/components/fritzbox/translations/it.json +++ b/homeassistant/components/fritzbox/translations/it.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Autenticazione non valida" }, - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "AVM FRITZ! SmartHome: {name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 8a314f33d94..108da3b1263 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -50,7 +50,7 @@ }, "options": { "error": { - "bad_birth": "Topic missatge de naixement inv\u00e0lid.", + "bad_birth": "Topic del missatge de naixement inv\u00e0lid.", "bad_will": "Topic missatge d'\u00faltima voluntat inv\u00e0lid.", "cannot_connect": "Ha fallat la connexi\u00f3" }, @@ -62,23 +62,25 @@ "port": "Port", "username": "Nom d'usuari" }, - "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT." + "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT.", + "title": "Opcions del broker" }, "options": { "data": { "birth_enable": "Activa el missatge de naixement", - "birth_payload": "Dades (payload) missatge de naixement", - "birth_qos": "QoS missatge de naixement", - "birth_retain": "Retenci\u00f3 missatge de naixement", - "birth_topic": "Topic missatge de naixement", + "birth_payload": "Dades (payload) del missatge de naixement", + "birth_qos": "QoS del missatge de naixement", + "birth_retain": "Retenci\u00f3 del missatge de naixement", + "birth_topic": "Topic del missatge de naixement", "discovery": "Activar descobriment", "will_enable": "Activa el missatge d'\u00faltima voluntat", - "will_payload": "Dades (payload) missatge d'\u00faltima voluntat", - "will_qos": "QoS missatge d'\u00faltima voluntat", - "will_retain": "Retenci\u00f3 missatge d'\u00faltima voluntat", - "will_topic": "Topic missatge d'\u00faltima voluntat" + "will_payload": "Dades (payload) del missatge d'\u00faltima voluntat", + "will_qos": "QoS del missatge d'\u00faltima voluntat", + "will_retain": "Retenci\u00f3 del missatge d'\u00faltima voluntat", + "will_topic": "Topic del missatge d'\u00faltima voluntat" }, - "description": "Selecciona les opcions MQTT." + "description": "Descobriment - Si est\u00e0 activat (recomanat), Home Assistant descobrir\u00e0 autom\u00e0ticament dispositius i entitats que publiquin la seva configuraci\u00f3 al broker MQTT. Si est\u00e0 desactivat, les configuracions s'han de fer manualment.\nMissatge de naixement - S'enviar\u00e0 cada vegada que Home Assistant \u00e9s connecti al broker MQTT.\nMissatge d'\u00faltima voluntat - S'enviar\u00e0 cada vegada que Home Assistant perdi la connexi\u00f3 amb el broker, tant si \u00e9s una desconnexi\u00f3 neta (per exemple si s'apaga Home Assistant) com si \u00e9s una desconnexi\u00f3 dolenta (per exemple si Home Assistant falla o perd la connexi\u00f3 a Internet).", + "title": "Opcions d'MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index ddd2a38c3b6..775b4d21c9b 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -15,7 +15,7 @@ "port": "Port", "username": "Username" }, - "description": "Please enter the connection information of your MQTT broker and select if MQTT discovery should be enabled (recommended). If discovery is enabled, Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually." + "description": "Please enter the connection information of your MQTT broker." }, "hassio_confirm": { "data": { @@ -79,9 +79,9 @@ "will_retain": "Will message retain", "will_topic": "Will message topic" }, - "description": "### Discovery \n If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually. \n ### Birth message\n The birth message will be sent each time Home Assistant (re)connects to the MQTT broker. \n ### Will message \n The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.", + "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.", "title": "MQTT options" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 70107efa269..2cabe392308 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -62,7 +62,8 @@ "port": "Puerto", "username": "Usuario" }, - "description": "Por favor, introduce la informaci\u00f3n de tu agente MQTT." + "description": "Por favor, introduce la informaci\u00f3n de tu agente MQTT.", + "title": "Opciones para el Broker" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Retendr\u00e1 el mensaje", "will_topic": "Enviar\u00e1 un mensaje al tema" }, - "description": "Por favor, selecciona las opciones para MQTT." + "description": "Por favor, selecciona las opciones para MQTT.", + "title": "Opciones para MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index 4bc267450bb..3b7a0c87f57 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -62,7 +62,8 @@ "port": "Port", "username": "Kasutajanimi" }, - "description": "Sisesta oma MQTT vahendaja \u00fchenduse teave." + "description": "Sisesta oma MQTT vahendaja \u00fchenduse teave.", + "title": "MQTT maakleri valikud" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "L\u00f5petamisteate j\u00e4\u00e4dvustamine", "will_topic": "L\u00f5petamisteade" }, - "description": "Vali MQTT s\u00e4tted." + "description": "Avastamine - kui avastamine on lubatud (soovitatav) avastab Home Assistant automaatselt seadmed ja \u00fcksused, kes avaldavad oma konfiguratsiooni MQTT maakleris. Kui avastamine on keelatud, tuleb kogu seadistamine teha k\u00e4sitsi.\n S\u00fcnnis\u00f5num - s\u00fcnnis\u00f5num saadetakse iga kord kui Home Assistant (uuesti) MQTT maakleriga \u00fchendust v\u00f5tab.\n Tahte s\u00f5num - tahte s\u00f5num saadetakse iga kord kui Home Assistant kaotab \u00fchenduse maakleriga, nii korralisel (nt Home Assistant sulgub) kui ka erakorralisel (nt Home Assistant krahhi v\u00f5i v\u00f5rgu\u00fchenduse kaotamisel) \u00fchenduse kadumisel.", + "title": "MQTT valikud" } } } diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index a7cad033cdb..9636e0ea446 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -62,7 +62,8 @@ "port": "Porta", "username": "Nome utente" }, - "description": "Inserisci le informazioni di connessione del tuo broker MQTT." + "description": "Inserisci le informazioni di connessione del tuo broker MQTT.", + "title": "Opzioni del broker" }, "options": { "data": { @@ -72,13 +73,14 @@ "birth_retain": "Persistenza del messaggio birth", "birth_topic": "Argomento del messaggio birth", "discovery": "Attiva l'individuazione", - "will_enable": "Abilita messaggio di ultima volont\u00e0 e testamento", - "will_payload": "Payload del messaggio will", - "will_qos": "QoS del messaggio will", - "will_retain": "Persistenza del messaggio will", - "will_topic": "Argomento del messaggio will" + "will_enable": "Abilita il messaggio testamento", + "will_payload": "Payload del messaggio testamento", + "will_qos": "QoS del messaggio testamento", + "will_retain": "Persistenza del messaggio testamento", + "will_topic": "Argomento del messaggio testamento" }, - "description": "Selezionare le opzioni MQTT." + "description": "Rilevamento: se il rilevamento \u00e8 abilitato (consigliato), Home Assistant rilever\u00e0 automaticamente i dispositivi e le entit\u00e0 che pubblicano la loro configurazione sul broker MQTT. Se il rilevamento \u00e8 disabilitato, tutta la configurazione deve essere eseguita manualmente.\nMessaggio di nascita: il messaggio di nascita verr\u00e0 inviato ogni volta che Home Assistant si (ri)collega al broker MQTT.\nMessaggio testamento: Il messaggio testamento verr\u00e0 inviato ogni volta che Home Assistant perde la connessione al broker, sia in caso di buona (es. arresto di Home Assistant) sia in caso di cattiva (es. Home Assistant in crash o perdita della connessione di rete) disconnessione.", + "title": "Opzioni MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index b56ef2413d7..fc8606d7b43 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -62,7 +62,8 @@ "port": "Poort", "username": "Gebruikersnaam" }, - "description": "Voer de verbindingsgegevens van uw MQTT-broker in." + "description": "Voer de verbindingsgegevens van uw MQTT-broker in.", + "title": "Broker opties" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Will message behouden", "will_topic": "Will message topic" }, - "description": "Selecteer MQTT-opties." + "description": "Selecteer MQTT-opties.", + "title": "MQTT-opties" } } } diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 8ff5a13138c..321a1e5e56c 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -62,7 +62,8 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0411\u0440\u043e\u043a\u0435\u0440\u0430" }, "options": { "data": { @@ -72,13 +73,14 @@ "birth_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "birth_topic": "\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)", "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", - "will_enable": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "will_enable": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_topic": "\u0422\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 MQTT." + "description": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u2014 \u0435\u0441\u043b\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f), Home Assistant \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0443\u0431\u043b\u0438\u043a\u0443\u044e\u0442 \u0441\u0432\u043e\u044e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043d\u0430 \u0431\u0440\u043e\u043a\u0435\u0440\u0435 MQTT. \u0415\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e, \u0432\u0441\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c\u0441\u044f \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u2014 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437, \u043a\u043e\u0433\u0434\u0430 Home Assistant \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.\n\u0422\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u2014 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043a\u0430\u043a \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043e\u0442 \u0431\u0440\u043e\u043a\u0435\u0440\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 Home Assistant), \u0442\u0430\u043a \u0438 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0441\u0431\u043e\u0435 Home Assistant \u0438\u043b\u0438 \u043f\u043e\u0442\u0435\u0440\u0435 \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f).", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b MQTT" } } } diff --git a/homeassistant/components/myq/translations/it.json b/homeassistant/components/myq/translations/it.json index ac793e62c6d..3ed9280b462 100644 --- a/homeassistant/components/myq/translations/it.json +++ b/homeassistant/components/myq/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +10,13 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "La password per {username} non \u00e8 pi\u00f9 valida.", + "title": "Riautentica il tuo account MyQ" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index 4f75a639ee5..5b52990dde5 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ONVIF ya est\u00e1 en marcha.", "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifique la configuraci\u00f3n del perfil en su dispositivo.", "no_mac": "No se pudo configurar una identificaci\u00f3n \u00fanica para el dispositivo ONVIF.", - "onvif_error": "Error de configuraci\u00f3n del dispositivo ONVIF. Revise los registros para m\u00e1s informaci\u00f3n." + "onvif_error": "Error de configuraci\u00f3n del dispositivo ONVIF. Comprueba el registro para m\u00e1s informaci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/rainmachine/translations/ca.json b/homeassistant/components/rainmachine/translations/ca.json index 9472211f8df..76c91db7f32 100644 --- a/homeassistant/components/rainmachine/translations/ca.json +++ b/homeassistant/components/rainmachine/translations/ca.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, + "flow_title": "RainMachine {ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/et.json b/homeassistant/components/rainmachine/translations/et.json index e3eb3e60462..81c474e4942 100644 --- a/homeassistant/components/rainmachine/translations/et.json +++ b/homeassistant/components/rainmachine/translations/et.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Tuvastamise viga" }, + "flow_title": "RainMachine {ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/it.json b/homeassistant/components/rainmachine/translations/it.json index 72e377dd95d..7834c61bb44 100644 --- a/homeassistant/components/rainmachine/translations/it.json +++ b/homeassistant/components/rainmachine/translations/it.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Autenticazione non valida" }, + "flow_title": "RainMachine {ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/ru.json b/homeassistant/components/rainmachine/translations/ru.json index 8502b66aff7..6e56f9214a2 100644 --- a/homeassistant/components/rainmachine/translations/ru.json +++ b/homeassistant/components/rainmachine/translations/ru.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, + "flow_title": "RainMachine {ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/system_bridge/translations/ca.json b/homeassistant/components/system_bridge/translations/ca.json new file mode 100644 index 00000000000..09a3f9922ed --- /dev/null +++ b/homeassistant/components/system_bridge/translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "flow_title": "Enlla\u00e7 de sistema: {name}", + "step": { + "authenticate": { + "data": { + "api_key": "Clau API" + }, + "description": "Introdueix la clau API que has posat a la configuraci\u00f3 de {name}." + }, + "user": { + "data": { + "api_key": "Clau API", + "host": "Amfitri\u00f3", + "port": "Port" + }, + "description": "Introdueix les dades de connexi\u00f3." + } + } + }, + "title": "Enlla\u00e7 de sistema" +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/es.json b/homeassistant/components/system_bridge/translations/es.json new file mode 100644 index 00000000000..fc2c8a111ae --- /dev/null +++ b/homeassistant/components/system_bridge/translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "authenticate": { + "data": { + "api_key": "Clave API" + }, + "description": "Escribe la clave API que estableciste en la configuraci\u00f3n para {name}." + }, + "user": { + "data": { + "api_key": "Clave API", + "host": "Host", + "port": "Puerto" + }, + "description": "Por favor, introduce tus datos de conexi\u00f3n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/et.json b/homeassistant/components/system_bridge/translations/et.json new file mode 100644 index 00000000000..d4aff33d335 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/et.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "unknown": "Tundmatu t\u00f5rge" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Tundmatu t\u00f5rge" + }, + "flow_title": "S\u00fcsteemi sild: {name}", + "step": { + "authenticate": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Sisesta API-v\u00f5ti mille m\u00e4\u00e4rasid {name} s\u00e4tetes." + }, + "user": { + "data": { + "api_key": "API v\u00f5ti", + "host": "Host", + "port": "Port" + }, + "description": "Sisesta oma \u00fchenduse \u00fcksikasjad." + } + } + }, + "title": "S\u00fcsteemi sild" +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/it.json b/homeassistant/components/system_bridge/translations/it.json new file mode 100644 index 00000000000..5cc7ac8c7be --- /dev/null +++ b/homeassistant/components/system_bridge/translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "flow_title": "Bridge di sistema: {name}", + "step": { + "authenticate": { + "data": { + "api_key": "Chiave API" + }, + "description": "Immettere la chiave API impostata nella configurazione per {name} ." + }, + "user": { + "data": { + "api_key": "Chiave API", + "host": "Host", + "port": "Porta" + }, + "description": "Inserisci i dettagli della tua connessione." + } + } + }, + "title": "Bridge di sistema" +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/nl.json b/homeassistant/components/system_bridge/translations/nl.json new file mode 100644 index 00000000000..f04d15cc453 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol", + "unknown": "Onverwachte fout" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "flow_title": "System Bridge: {name}", + "step": { + "authenticate": { + "data": { + "api_key": "API-sleutel" + }, + "description": "Voer de API-sleutel in die u in uw configuratie heeft ingesteld voor {name}." + }, + "user": { + "data": { + "api_key": "API-sleutel", + "host": "Host", + "port": "Poort" + }, + "description": "Voer uw verbindingsgegevens in." + } + } + }, + "title": "System Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/ru.json b/homeassistant/components/system_bridge/translations/ru.json new file mode 100644 index 00000000000..35c576620b4 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/ru.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\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.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "System Bridge: {name}", + "step": { + "authenticate": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0412\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0438 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f {name}." + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + } + } + }, + "title": "System Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/zh-Hant.json b/homeassistant/components/system_bridge/translations/zh-Hant.json new file mode 100644 index 00000000000..b123cc6e9dd --- /dev/null +++ b/homeassistant/components/system_bridge/translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "System Bridge\uff1a{name}", + "step": { + "authenticate": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u4e2d\u6240\u8a2d\u5b9a\u4e4b API \u5bc6\u9470\u3002" + }, + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8acb\u8f38\u5165\u9023\u7dda\u8a0a\u606f\u3002" + } + } + }, + "title": "System Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/weather/translations/de.json b/homeassistant/components/weather/translations/de.json index 123cae340ab..280922ba5bd 100644 --- a/homeassistant/components/weather/translations/de.json +++ b/homeassistant/components/weather/translations/de.json @@ -9,7 +9,7 @@ "lightning": "Gewitter", "lightning-rainy": "Gewitter, regnerisch", "partlycloudy": "Teilweise bew\u00f6lkt", - "pouring": "Str\u00f6mend", + "pouring": "Platzregen", "rainy": "Regnerisch", "snowy": "Verschneit", "snowy-rainy": "Verschneit, regnerisch", From 89811fcbaada1be7c87eba3331989a7147e38536 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 May 2021 19:58:44 -0500 Subject: [PATCH 206/852] Ensure tesla setup is retried on timeout (#50202) --- homeassistant/components/tesla/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 2b0373dba33..54e3bab3f44 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.httpx_client import SERVER_SOFTWARE, USER_AGENT from homeassistant.helpers.update_coordinator import ( @@ -170,6 +170,9 @@ async def async_setup_entry(hass, config_entry): except IncompleteCredentials as ex: await async_client.aclose() raise ConfigEntryAuthFailed from ex + except httpx.ConnectTimeout as ex: + await async_client.aclose() + raise ConfigEntryNotReady from ex except TeslaException as ex: await async_client.aclose() if ex.code == HTTP_UNAUTHORIZED: From 5ec09eab421c7f85c9ec5b78f53d583e36a1576f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 7 May 2021 02:59:03 +0200 Subject: [PATCH 207/852] Move not loaded websocket constant to zwave_js (#50188) --- homeassistant/components/websocket_api/const.py | 1 - homeassistant/components/zwave_js/api.py | 2 +- tests/components/zwave_js/test_api.py | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 0681a422db1..7c3f18f856c 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -23,7 +23,6 @@ MAX_PENDING_MSG = 2048 ERR_ID_REUSE = "id_reuse" ERR_INVALID_FORMAT = "invalid_format" ERR_NOT_FOUND = "not_found" -ERR_NOT_LOADED = "not_loaded" ERR_NOT_SUPPORTED = "not_supported" ERR_HOME_ASSISTANT_ERROR = "home_assistant_error" ERR_UNKNOWN_COMMAND = "unknown_command" diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 7eba39d1b7c..81600ec6c16 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -22,7 +22,6 @@ from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.components.websocket_api.const import ( ERR_NOT_FOUND, - ERR_NOT_LOADED, ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, ) @@ -45,6 +44,7 @@ from .helpers import async_enable_statistics, update_data_collection_preference # general API constants ID = "id" ENTRY_ID = "entry_id" +ERR_NOT_LOADED = "not_loaded" NODE_ID = "node_id" COMMAND_CLASS_ID = "command_class_id" TYPE = "type" diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index e471b1dd1a7..57961ee89e4 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -6,12 +6,13 @@ from zwave_js_server.const import LogLevel from zwave_js_server.event import Event from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed -from homeassistant.components.websocket_api.const import ERR_NOT_FOUND, ERR_NOT_LOADED +from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.zwave_js.api import ( COMMAND_CLASS_ID, CONFIG, ENABLED, ENTRY_ID, + ERR_NOT_LOADED, FILENAME, FORCE_CONSOLE, ID, From fec02c88afc5931a5675a87f72cdebdaef470c0f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 6 May 2021 20:03:35 -0600 Subject: [PATCH 208/852] Allow SimpliSafe startup to retry on failure (#50211) * Allow SimpliSafe startup to retry on failure * Update __init__.py * Black Co-authored-by: Paulus Schoutsen --- homeassistant/components/simplisafe/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 80784ce51b7..4f9b10abb8c 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -213,11 +213,14 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 _async_save_refresh_token(hass, config_entry, api.refresh_token) - simplisafe = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = SimpliSafe( - hass, api, config_entry - ) - await simplisafe.async_init() + simplisafe = SimpliSafe(hass, api, config_entry) + try: + await simplisafe.async_init() + except SimplipyError as err: + raise ConfigEntryNotReady from err + + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @callback From 0cf07ee2d8d801424faf93d73dce0b4ec7013d98 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 7 May 2021 05:23:46 +0200 Subject: [PATCH 209/852] Denonavr bugfixes (#49984) --- homeassistant/components/denonavr/__init__.py | 1 - homeassistant/components/denonavr/manifest.json | 2 +- homeassistant/components/denonavr/media_player.py | 1 - homeassistant/components/denonavr/receiver.py | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 76baf73c3e5..818c005b1cd 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -42,7 +42,6 @@ async def async_setup_entry( entry.options.get(CONF_ZONE2, DEFAULT_ZONE2), entry.options.get(CONF_ZONE3, DEFAULT_ZONE3), lambda: get_async_client(hass), - entry.state, ) try: await connect_denonavr.async_connect_receiver() diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index b3f45330c94..123eac5d2bf 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.10.5"], + "requirements": ["denonavr==0.10.6"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index ae001bfc312..2b8420e6774 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -246,7 +246,6 @@ class DenonDevice(MediaPlayerEntity): "manufacturer": self._config_entry.data[CONF_MANUFACTURER], "name": self._config_entry.title, "model": f"{self._config_entry.data[CONF_MODEL]}-{self._config_entry.data[CONF_TYPE]}", - "serial_number": self._config_entry.data[CONF_SERIAL_NUMBER], } return device_info diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py index 8b50373799b..c5d4661b1a8 100644 --- a/homeassistant/components/denonavr/receiver.py +++ b/homeassistant/components/denonavr/receiver.py @@ -20,7 +20,6 @@ class ConnectDenonAVR: zone2: bool, zone3: bool, async_client_getter: Callable, - entry_state: str | None = None, ): """Initialize the class.""" self._async_client_getter = async_client_getter @@ -28,7 +27,6 @@ class ConnectDenonAVR: self._host = host self._show_all_inputs = show_all_inputs self._timeout = timeout - self._entry_state = entry_state self._zones = {} if zone2: diff --git a/requirements_all.txt b/requirements_all.txt index 7446c8b9487..acd628d5e09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -479,7 +479,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.10.5 +denonavr==0.10.6 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72f745c92fc..453698567a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -264,7 +264,7 @@ debugpy==1.2.1 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.10.5 +denonavr==0.10.6 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.3 From 7b07bc2d6512966d50e1cf016e071558df27a014 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 May 2021 22:26:03 -0500 Subject: [PATCH 210/852] Bump netdisco to 2.8.3 for compat with latest zeroconf (#50212) --- homeassistant/components/discovery/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 5744d406dec..a2d2df1730a 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -2,7 +2,7 @@ "domain": "discovery", "name": "Discovery", "documentation": "https://www.home-assistant.io/integrations/discovery", - "requirements": ["netdisco==2.8.2"], + "requirements": ["netdisco==2.8.3"], "after_dependencies": ["zeroconf"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 5351fc8f7ea..76dd3976890 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ "defusedxml==0.6.0", - "netdisco==2.8.2", + "netdisco==2.8.3", "async-upnp-client==0.16.2" ], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7e90e375e80..2b5546ac53c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ hass-nabucasa==0.43.0 home-assistant-frontend==20210504.0 httpx==0.18.0 jinja2>=2.11.3 -netdisco==2.8.2 +netdisco==2.8.3 paho-mqtt==1.5.1 pillow==8.1.2 pip>=8.0.3,<20.3 diff --git a/requirements_all.txt b/requirements_all.txt index acd628d5e09..b79b989ab1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -985,7 +985,7 @@ netdata==0.2.0 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.8.2 +netdisco==2.8.3 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 453698567a9..82130eb6f41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -533,7 +533,7 @@ nessclient==0.9.15 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.8.2 +netdisco==2.8.3 # homeassistant.components.nexia nexia==0.9.6 From 9f1b1c6c569bbc1c916547a5c560d766d1302d0a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 7 May 2021 00:12:51 -0400 Subject: [PATCH 211/852] Add value map for Climacell V3 pollen sensors (#50200) --- homeassistant/components/climacell/const.py | 19 ++++++++++++++++--- .../components/climacell/manifest.json | 2 +- homeassistant/components/climacell/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/climacell/test_sensor.py | 6 +++--- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 977a5089783..0352807138a 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -6,6 +6,7 @@ from pyclimacell.const import ( HealthConcernType, PollenIndex, PrimaryPollutantType, + V3PollenIndex, WeatherCode, ) @@ -307,8 +308,20 @@ CC_V3_SENSOR_TYPES = [ ATTR_FIELD: CC_V3_ATTR_CHINA_HEALTH_CONCERN, ATTR_NAME: "China MEP Health Concern", }, - {ATTR_FIELD: CC_V3_ATTR_POLLEN_TREE, ATTR_NAME: "Tree Pollen Index"}, - {ATTR_FIELD: CC_V3_ATTR_POLLEN_WEED, ATTR_NAME: "Weed Pollen Index"}, - {ATTR_FIELD: CC_V3_ATTR_POLLEN_GRASS, ATTR_NAME: "Grass Pollen Index"}, + { + ATTR_FIELD: CC_V3_ATTR_POLLEN_TREE, + ATTR_NAME: "Tree Pollen Index", + ATTR_VALUE_MAP: V3PollenIndex, + }, + { + ATTR_FIELD: CC_V3_ATTR_POLLEN_WEED, + ATTR_NAME: "Weed Pollen Index", + ATTR_VALUE_MAP: V3PollenIndex, + }, + { + ATTR_FIELD: CC_V3_ATTR_POLLEN_GRASS, + ATTR_NAME: "Grass Pollen Index", + ATTR_VALUE_MAP: V3PollenIndex, + }, {ATTR_FIELD: CC_V3_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"}, ] diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json index 89f6d7bf846..bb7dea841e4 100644 --- a/homeassistant/components/climacell/manifest.json +++ b/homeassistant/components/climacell/manifest.json @@ -3,7 +3,7 @@ "name": "ClimaCell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/climacell", - "requirements": ["pyclimacell==0.18.0"], + "requirements": ["pyclimacell==0.18.2"], "codeowners": ["@raman325"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index fc35f2c2a2e..df611079403 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -128,7 +128,7 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): ): return round(self._state * self.sensor_type[ATTR_METRIC_CONVERSION], 4) - if ATTR_VALUE_MAP in self.sensor_type: + if ATTR_VALUE_MAP in self.sensor_type and self._state is not None: return self.sensor_type[ATTR_VALUE_MAP](self._state).name.lower() return self._state diff --git a/requirements_all.txt b/requirements_all.txt index b79b989ab1e..2e4c10e7f6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1319,7 +1319,7 @@ pychromecast==9.1.2 pycketcasts==1.0.0 # homeassistant.components.climacell -pyclimacell==0.18.0 +pyclimacell==0.18.2 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82130eb6f41..3e96e843884 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -720,7 +720,7 @@ pycfdns==1.2.1 pychromecast==9.1.2 # homeassistant.components.climacell -pyclimacell==0.18.0 +pyclimacell==0.18.2 # homeassistant.components.comfoconnect pycomfoconnect==0.4 diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index 7757fe208d3..44fc163848b 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -119,9 +119,9 @@ async def test_v3_sensor( check_sensor_state(hass, EPA_HEALTH_CONCERN, "Good") check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25") check_sensor_state(hass, FIRE_INDEX, "9") - check_sensor_state(hass, GRASS_POLLEN, "0") - check_sensor_state(hass, WEED_POLLEN, "0") - check_sensor_state(hass, TREE_POLLEN, "0") + check_sensor_state(hass, GRASS_POLLEN, "minimal_to_none") + check_sensor_state(hass, WEED_POLLEN, "minimal_to_none") + check_sensor_state(hass, TREE_POLLEN, "minimal_to_none") async def test_v4_sensor( From 1c4a44dc5c355db8103c641b571148605ce10914 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 7 May 2021 07:24:54 +0200 Subject: [PATCH 212/852] Upgrade discord.py to 1.7.2 (#50201) --- homeassistant/components/discord/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 508ddd126a3..c475c502f60 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -2,7 +2,7 @@ "domain": "discord", "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", - "requirements": ["discord.py==1.6.0"], + "requirements": ["discord.py==1.7.2"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 2e4c10e7f6d..6a6cf649400 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -491,7 +491,7 @@ directv==0.4.0 discogs_client==2.3.0 # homeassistant.components.discord -discord.py==1.6.0 +discord.py==1.7.2 # homeassistant.components.updater distro==1.5.0 From 084f139a4dfb47a31e40e6ad58a5c415be9cb5a7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 7 May 2021 07:26:21 +0200 Subject: [PATCH 213/852] Upgrade praw to 7.2.0 (#50197) --- homeassistant/components/reddit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index a9ffe490019..0b5f539bcce 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -2,7 +2,7 @@ "domain": "reddit", "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", - "requirements": ["praw==7.1.4"], + "requirements": ["praw==7.2.0"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 6a6cf649400..7fda4108b3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1165,7 +1165,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==7.1.4 +praw==7.2.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e96e843884..176e202ae0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -632,7 +632,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==7.1.4 +praw==7.2.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 From 93628554cb33f8e9a9440f3226131039f7b9f9b0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 7 May 2021 07:28:58 +0200 Subject: [PATCH 214/852] Upgrade slixmpp to 1.7.1 (#50192) --- homeassistant/components/xmpp/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 46acec9e567..55df2587898 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,7 +2,7 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.7.0"], + "requirements": ["slixmpp==1.7.1"], "codeowners": ["@fabaff", "@flowolf"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 7fda4108b3d..b965eef40e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2082,7 +2082,7 @@ slackclient==2.5.0 sleepyq==0.8.1 # homeassistant.components.xmpp -slixmpp==1.7.0 +slixmpp==1.7.1 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.0 From 316f6ba3979e12aa72cff4abcefe779cdeae83ac Mon Sep 17 00:00:00 2001 From: Sezer K Date: Fri, 7 May 2021 07:29:37 +0200 Subject: [PATCH 215/852] Only initialize Nuki configurations (#49747) Co-authored-by: Paulus Schoutsen --- homeassistant/components/nuki/__init__.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index f937bddf623..ea224612d82 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -10,7 +10,7 @@ from requests.exceptions import RequestException from homeassistant import exceptions from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.const import CONF_HOST, CONF_PLATFORM, CONF_PORT, CONF_TOKEN from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -22,6 +22,7 @@ from .const import ( DATA_COORDINATOR, DATA_LOCKS, DATA_OPENERS, + DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, ERROR_STATES, @@ -59,11 +60,18 @@ async def async_setup(hass, config): continue for conf in confs: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + if CONF_PLATFORM in conf and conf[CONF_PLATFORM] == DOMAIN: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: conf[CONF_HOST], + CONF_PORT: conf.get(CONF_PORT, DEFAULT_PORT), + CONF_TOKEN: conf[CONF_TOKEN], + }, + ) ) - ) return True From 47c4c681f4abc586ce9aba267d37c00df6eaa3b1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 7 May 2021 07:29:55 +0200 Subject: [PATCH 216/852] Upgrade sendgrid to 6.7.0 (#50194) --- homeassistant/components/sendgrid/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index 318bd87689f..216ea5f625b 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -2,7 +2,7 @@ "domain": "sendgrid", "name": "SendGrid", "documentation": "https://www.home-assistant.io/integrations/sendgrid", - "requirements": ["sendgrid==6.6.0"], + "requirements": ["sendgrid==6.7.0"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index b965eef40e4..6ec0c43d4cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2039,7 +2039,7 @@ screenlogicpy==0.4.1 scsgate==0.1.0 # homeassistant.components.sendgrid -sendgrid==6.6.0 +sendgrid==6.7.0 # homeassistant.components.sensehat sense-hat==2.2.0 From c2663d61d7e487263551b87ae54c56dd3a79ca7c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 May 2021 07:34:51 +0200 Subject: [PATCH 217/852] Add color_mode support to group light (#50165) * Add color_mode support to group light * Lint * Update tests --- homeassistant/components/group/light.py | 98 ++- tests/components/group/test_light.py | 613 ++++++++++++++---- tests/components/light/test_init.py | 4 +- .../custom_components/test/light.py | 29 +- 4 files changed, 610 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 26cc8e1c11c..84c218b5d72 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components import light from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, @@ -19,16 +20,20 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE_VALUE, + ATTR_XY_COLOR, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, + color_supported, + color_temp_supported, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -59,13 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) SUPPORT_GROUP_LIGHT = ( - SUPPORT_BRIGHTNESS - | SUPPORT_COLOR_TEMP - | SUPPORT_EFFECT - | SUPPORT_FLASH - | SUPPORT_COLOR - | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE + SUPPORT_EFFECT | SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_WHITE_VALUE ) @@ -89,13 +88,19 @@ class LightGroup(GroupEntity, light.LightEntity): self._available = False self._icon = "mdi:lightbulb-group" self._brightness: int | None = None + self._color_mode: str | None = None self._hs_color: tuple[float, float] | None = None + self._rgb_color: tuple[int, int, int] | None = None + self._rgbw_color: tuple[int, int, int, int] | None = None + self._rgbww_color: tuple[int, int, int, int, int] | None = None + self._xy_color: tuple[float, float] | None = None self._color_temp: int | None = None self._min_mireds: int = 154 self._max_mireds: int = 500 self._white_value: int | None = None self._effect_list: list[str] | None = None self._effect: str | None = None + self._supported_color_modes: set[str] | None = None self._supported_features: int = 0 async def async_added_to_hass(self) -> None: @@ -143,11 +148,36 @@ class LightGroup(GroupEntity, light.LightEntity): """Return the brightness of this light group between 0..255.""" return self._brightness + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + return self._color_mode + @property def hs_color(self) -> tuple[float, float] | None: """Return the HS color value [float, float].""" return self._hs_color + @property + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int].""" + return self._rgb_color + + @property + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the rgbw color value [int, int, int, int].""" + return self._rgbw_color + + @property + def rgbww_color(self) -> tuple[int, int, int, int, int] | None: + """Return the rgbww color value [int, int, int, int, int].""" + return self._rgbww_color + + @property + def xy_color(self) -> tuple[float, float] | None: + """Return the xy color value [float, float].""" + return self._xy_color + @property def color_temp(self) -> int | None: """Return the CT color value in mireds.""" @@ -178,6 +208,11 @@ class LightGroup(GroupEntity, light.LightEntity): """Return the current effect.""" return self._effect + @property + def supported_color_modes(self) -> set | None: + """Flag supported color modes.""" + return self._supported_color_modes + @property def supported_features(self) -> int: """Flag supported features.""" @@ -204,6 +239,18 @@ class LightGroup(GroupEntity, light.LightEntity): if ATTR_HS_COLOR in kwargs: data[ATTR_HS_COLOR] = kwargs[ATTR_HS_COLOR] + if ATTR_RGB_COLOR in kwargs: + data[ATTR_RGB_COLOR] = kwargs[ATTR_RGB_COLOR] + + if ATTR_RGBW_COLOR in kwargs: + data[ATTR_RGBW_COLOR] = kwargs[ATTR_RGBW_COLOR] + + if ATTR_RGBWW_COLOR in kwargs: + data[ATTR_RGBWW_COLOR] = kwargs[ATTR_RGBWW_COLOR] + + if ATTR_XY_COLOR in kwargs: + data[ATTR_XY_COLOR] = kwargs[ATTR_XY_COLOR] + if ATTR_COLOR_TEMP in kwargs: data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] @@ -215,11 +262,9 @@ class LightGroup(GroupEntity, light.LightEntity): state = self.hass.states.get(entity_id) if not state: continue - support = state.attributes.get(ATTR_SUPPORTED_FEATURES) + support = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) # Only pass color temperature to supported entity_ids - if bool(support & SUPPORT_COLOR) and not bool( - support & SUPPORT_COLOR_TEMP - ): + if color_supported(support) and not color_temp_supported(support): emulate_color_temp_entity_ids.append(entity_id) updated_entities.remove(entity_id) data[ATTR_ENTITY_ID] = updated_entities @@ -300,6 +345,16 @@ class LightGroup(GroupEntity, light.LightEntity): self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) self._hs_color = _reduce_attribute(on_states, ATTR_HS_COLOR, reduce=_mean_tuple) + self._rgb_color = _reduce_attribute( + on_states, ATTR_RGB_COLOR, reduce=_mean_tuple + ) + self._rgbw_color = _reduce_attribute( + on_states, ATTR_RGBW_COLOR, reduce=_mean_tuple + ) + self._rgbww_color = _reduce_attribute( + on_states, ATTR_RGBWW_COLOR, reduce=_mean_tuple + ) + self._xy_color = _reduce_attribute(on_states, ATTR_XY_COLOR, reduce=_mean_tuple) self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE) @@ -324,6 +379,21 @@ class LightGroup(GroupEntity, light.LightEntity): effects_count = Counter(itertools.chain(all_effects)) self._effect = effects_count.most_common(1)[0][0] + self._color_mode = None + all_color_modes = list(_find_state_attributes(on_states, ATTR_COLOR_MODE)) + if all_color_modes: + # Report the most common color mode. + color_mode_count = Counter(itertools.chain(all_color_modes)) + self._color_mode = color_mode_count.most_common(1)[0][0] + + self._supported_color_modes = None + all_supported_color_modes = list( + _find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) + ) + if all_supported_color_modes: + # Merge all color modes. + self._supported_color_modes = set().union(*all_supported_color_modes) + self._supported_features = 0 for support in _find_state_attributes(states, ATTR_SUPPORTED_FEATURES): # Merge supported features by emulating support for every feature diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 136da458f66..c9b861a46a9 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -8,6 +8,7 @@ from homeassistant.components.group import DOMAIN, SERVICE_RELOAD import homeassistant.components.group.light as group from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, @@ -16,13 +17,24 @@ from homeassistant.components.light import ( ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, DOMAIN as LIGHT_DOMAIN, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -104,85 +116,281 @@ async def test_state_reporting(hass): async def test_brightness(hass): """Test brightness reporting.""" - await async_setup_component( + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_BRIGHTNESS} + entity0.color_mode = COLOR_MODE_BRIGHTNESS + entity0.brightness = 255 + + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_BRIGHTNESS + + assert await async_setup_component( hass, LIGHT_DOMAIN, { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] }, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() - hass.states.async_set( - "light.test1", STATE_ON, {ATTR_BRIGHTNESS: 255, ATTR_SUPPORTED_FEATURES: 1} - ) - await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 1 assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert state.attributes[ATTR_COLOR_MODE] == "brightness" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] - hass.states.async_set( - "light.test2", STATE_ON, {ATTR_BRIGHTNESS: 100, ATTR_SUPPORTED_FEATURES: 1} + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id], ATTR_BRIGHTNESS: 100}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 177 + assert state.attributes[ATTR_COLOR_MODE] == "brightness" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] - hass.states.async_set( - "light.test1", STATE_OFF, {ATTR_BRIGHTNESS: 255, ATTR_SUPPORTED_FEATURES: 1} + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 1 assert state.attributes[ATTR_BRIGHTNESS] == 100 + assert state.attributes[ATTR_COLOR_MODE] == "brightness" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] -async def test_color(hass): - """Test RGB reporting.""" - await async_setup_component( +async def test_color_hs(hass): + """Test hs color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_HS} + entity0.color_mode = COLOR_MODE_HS + entity0.brightness = 255 + entity0.hs_color = (0, 100) + + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_COLOR + + assert await async_setup_component( hass, LIGHT_DOMAIN, { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] }, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() - hass.states.async_set( - "light.test1", STATE_ON, {ATTR_HS_COLOR: (0, 100), ATTR_SUPPORTED_FEATURES: 16} - ) - await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 16 + assert state.attributes[ATTR_COLOR_MODE] == "hs" assert state.attributes[ATTR_HS_COLOR] == (0, 100) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 - hass.states.async_set( - "light.test2", STATE_ON, {ATTR_HS_COLOR: (0, 50), ATTR_SUPPORTED_FEATURES: 16} + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id], ATTR_HS_COLOR: (0, 50)}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "hs" assert state.attributes[ATTR_HS_COLOR] == (0, 75) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 - hass.states.async_set( - "light.test1", STATE_OFF, {ATTR_HS_COLOR: (0, 0), ATTR_SUPPORTED_FEATURES: 16} + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "hs" assert state.attributes[ATTR_HS_COLOR] == (0, 50) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + +async def test_color_rgbw(hass): + """Test rgbw color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_RGBW} + entity0.color_mode = COLOR_MODE_RGBW + entity0.brightness = 255 + entity0.rgbw_color = (0, 64, 128, 255) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGBW} + entity1.color_mode = COLOR_MODE_RGBW + entity1.brightness = 255 + entity1.rgbw_color = (255, 128, 64, 0) + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "rgbw" + assert state.attributes[ATTR_RGBW_COLOR] == (0, 64, 128, 255) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbw" + assert state.attributes[ATTR_RGBW_COLOR] == (127, 96, 96, 127) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbw" + assert state.attributes[ATTR_RGBW_COLOR] == (255, 128, 64, 0) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + +async def test_color_rgbww(hass): + """Test rgbww color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_RGBWW} + entity0.color_mode = COLOR_MODE_RGBWW + entity0.brightness = 255 + entity0.rgbww_color = (0, 32, 64, 128, 255) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGBWW} + entity1.color_mode = COLOR_MODE_RGBWW + entity1.brightness = 255 + entity1.rgbww_color = (255, 128, 64, 32, 0) + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "rgbww" + assert state.attributes[ATTR_RGBWW_COLOR] == (0, 32, 64, 128, 255) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbww"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbww" + assert state.attributes[ATTR_RGBWW_COLOR] == (127, 80, 64, 80, 127) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbww"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbww" + assert state.attributes[ATTR_RGBWW_COLOR] == (255, 128, 64, 32, 0) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbww"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 async def test_white_value(hass): @@ -206,6 +414,7 @@ async def test_white_value(hass): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert state.attributes[ATTR_WHITE_VALUE] == 255 hass.states.async_set( @@ -213,6 +422,7 @@ async def test_white_value(hass): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert state.attributes[ATTR_WHITE_VALUE] == 177 hass.states.async_set( @@ -220,62 +430,36 @@ async def test_white_value(hass): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert state.attributes[ATTR_WHITE_VALUE] == 100 async def test_color_temp(hass): """Test color temp reporting.""" - await async_setup_component( - hass, - LIGHT_DOMAIN, - { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } - }, - ) - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() + platform = getattr(hass.components, "test.light") + platform.init(empty=True) - hass.states.async_set( - "light.test1", STATE_ON, {"color_temp": 2, ATTR_SUPPORTED_FEATURES: 2} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_TEMP] == 2 + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - hass.states.async_set( - "light.test2", STATE_ON, {"color_temp": 1000, ATTR_SUPPORTED_FEATURES: 2} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_TEMP] == 501 + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + entity0.brightness = 255 + entity0.color_temp = 2 - hass.states.async_set( - "light.test1", STATE_OFF, {"color_temp": 2, ATTR_SUPPORTED_FEATURES: 2} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_TEMP] == 1000 + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_COLOR_TEMP - -async def test_emulated_color_temp_group(hass): - """Test emulated color temperature in a group.""" - await async_setup_component( + assert await async_setup_component( hass, LIGHT_DOMAIN, { LIGHT_DOMAIN: [ - {"platform": "demo"}, + {"platform": "test"}, { "platform": DOMAIN, - "entities": [ - "light.bed_light", - "light.ceiling_lights", - "light.kitchen_lights", - ], + "entities": ["light.test1", "light.test2"], }, ] }, @@ -284,13 +468,78 @@ async def test_emulated_color_temp_group(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("light.bed_light", STATE_ON, {ATTR_SUPPORTED_FEATURES: 2}) - hass.states.async_set( - "light.ceiling_lights", STATE_ON, {ATTR_SUPPORTED_FEATURES: 63} + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" + assert state.attributes[ATTR_COLOR_TEMP] == 2 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id], ATTR_COLOR_TEMP: 1000}, + blocking=True, ) - hass.states.async_set( - "light.kitchen_lights", STATE_ON, {ATTR_SUPPORTED_FEATURES: 61} + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" + assert state.attributes[ATTR_COLOR_TEMP] == 501 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" + assert state.attributes[ATTR_COLOR_TEMP] == 1000 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] + + +async def test_emulated_color_temp_group(hass): + """Test emulated color temperature in a group.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity1.color_mode = COLOR_MODE_COLOR_TEMP + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {COLOR_MODE_HS} + entity2.color_mode = COLOR_MODE_HS + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + }, + ] + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + await hass.async_block_till_done() await hass.services.async_call( LIGHT_DOMAIN, @@ -300,61 +549,82 @@ async def test_emulated_color_temp_group(hass): ) await hass.async_block_till_done() - state = hass.states.get("light.bed_light") + state = hass.states.get("light.test1") assert state.state == STATE_ON assert state.attributes[ATTR_COLOR_TEMP] == 200 assert ATTR_HS_COLOR not in state.attributes.keys() - state = hass.states.get("light.ceiling_lights") + state = hass.states.get("light.test2") assert state.state == STATE_ON assert state.attributes[ATTR_COLOR_TEMP] == 200 assert ATTR_HS_COLOR not in state.attributes.keys() - state = hass.states.get("light.kitchen_lights") + state = hass.states.get("light.test3") assert state.state == STATE_ON assert state.attributes[ATTR_HS_COLOR] == (27.001, 19.243) async def test_min_max_mireds(hass): - """Test min/max mireds reporting.""" - await async_setup_component( + """Test min/max mireds reporting. + + min/max mireds is reported both when light is on and off + """ + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + entity0.color_temp = 2 + entity0.min_mireds = 2 + entity0.max_mireds = 5 + + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_COLOR_TEMP + entity1.min_mireds = 1 + entity1.max_mireds = 1234567890 + + assert await async_setup_component( hass, LIGHT_DOMAIN, { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] }, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() - hass.states.async_set( - "light.test1", - STATE_ON, - {ATTR_MIN_MIREDS: 2, ATTR_MAX_MIREDS: 5, ATTR_SUPPORTED_FEATURES: 2}, - ) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_MIN_MIREDS] == 2 - assert state.attributes[ATTR_MAX_MIREDS] == 5 - - hass.states.async_set( - "light.test2", - STATE_ON, - {ATTR_MIN_MIREDS: 7, ATTR_MAX_MIREDS: 1234567890, ATTR_SUPPORTED_FEATURES: 2}, - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_MIN_MIREDS] == 2 + assert state.attributes[ATTR_MIN_MIREDS] == 1 assert state.attributes[ATTR_MAX_MIREDS] == 1234567890 - hass.states.async_set( - "light.test1", - STATE_OFF, - {ATTR_MIN_MIREDS: 1, ATTR_MAX_MIREDS: 2, ATTR_SUPPORTED_FEATURES: 2}, + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_MIN_MIREDS] == 1 + assert state.attributes[ATTR_MAX_MIREDS] == 1234567890 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") @@ -465,6 +735,123 @@ async def test_effect(hass): assert state.attributes[ATTR_EFFECT] == "Random" +async def test_supported_color_modes(hass): + """Test supported_color_modes reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW} + + entity2 = platform.ENTITIES[2] + entity2.supported_features = SUPPORT_BRIGHTNESS + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert set(state.attributes[ATTR_SUPPORTED_COLOR_MODES]) == { + "brightness", + "color_temp", + "hs", + "rgbw", + "rgbww", + } + + +async def test_color_mode(hass): + """Test color_mode reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity1.color_mode = COLOR_MODE_COLOR_TEMP + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity2.color_mode = COLOR_MODE_HS + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity2.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id, entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_HS + + async def test_supported_features(hass): """Test supported features reporting.""" await async_setup_component( @@ -486,20 +873,26 @@ async def test_supported_features(hass): state = hass.states.get("light.light_group") assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + # SUPPORT_COLOR_TEMP = 2 + # SUPPORT_COLOR_TEMP = 2 will be blocked in favour of COLOR_MODE_COLOR_TEMP hass.states.async_set("light.test2", STATE_ON, {ATTR_SUPPORTED_FEATURES: 2}) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 2 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + # SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_BRIGHTNESS = 41 + # SUPPORT_BRIGHTNESS = 1 will be translated to COLOR_MODE_BRIGHTNESS hass.states.async_set("light.test1", STATE_OFF, {ATTR_SUPPORTED_FEATURES: 41}) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 43 + # SUPPORT_TRANSITION | SUPPORT_FLASH = 40 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 40 + # Test that unknown feature 256 is blocked hass.states.async_set("light.test2", STATE_OFF, {ATTR_SUPPORTED_FEATURES: 256}) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 41 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 40 async def test_service_calls(hass): @@ -629,8 +1022,6 @@ async def test_invalid_service_calls(hass): } await grouped_light.async_turn_on(**data) data[ATTR_ENTITY_ID] = ["light.test1", "light.test2"] - data.pop(ATTR_RGB_COLOR) - data.pop(ATTR_XY_COLOR) mock_call.assert_called_once_with( LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=None ) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 752de867542..ef781b56a56 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -748,9 +748,9 @@ async def test_light_brightness_step(hass): ) _, data = entity0.last_call("turn_on") - assert data["brightness"] == 126 # 100 + (255 * 0.10) + assert data["brightness"] == 116 # 90 + (255 * 0.10) _, data = entity1.last_call("turn_on") - assert data["brightness"] == 76 # 50 + (255 * 0.10) + assert data["brightness"] == 66 # 40 + (255 * 0.10) async def test_light_brightness_pct_conversion(hass): diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 84008d90c27..88ce04bdc92 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -36,18 +36,33 @@ async def async_setup_platform( class MockLight(MockToggleEntity, LightEntity): """Mock light class.""" - brightness = None + color_mode = None + max_mireds = 500 + min_mireds = 153 supported_color_modes = None supported_features = 0 - color_mode = None - + brightness = None + color_temp = None hs_color = None - xy_color = None rgb_color = None rgbw_color = None rgbww_color = None - - color_temp = None - + xy_color = None white_value = None + + def turn_on(self, **kwargs): + """Turn the entity on.""" + super().turn_on(**kwargs) + for key, value in kwargs.items(): + if key in [ + "brightness", + "hs_color", + "xy_color", + "rgb_color", + "rgbw_color", + "rgbww_color", + "color_temp", + "white_value", + ]: + setattr(self, key, value) From 64851dbac3c9e98303546d7bf01ebc7ba90141d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 May 2021 04:13:51 -0500 Subject: [PATCH 218/852] Add optimistic closing/opening to gogogate2 (#42048) * Add optimistic closing/opening to gogogate2 * package rename * update test * Update homeassistant/components/gogogate2/cover.py --- homeassistant/components/gogogate2/cover.py | 34 +++++++-- tests/components/gogogate2/test_cover.py | 85 ++++++++++++++++++++- 2 files changed, 110 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 0097198f1c2..1410bc9e97d 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -3,7 +3,12 @@ from __future__ import annotations import logging -from ismartgate.common import AbstractDoor, DoorStatus, get_configured_doors +from ismartgate.common import ( + AbstractDoor, + DoorStatus, + TransitionDoorStatus, + get_configured_doors, +) from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, @@ -84,11 +89,10 @@ class DeviceCover(GoGoGate2Entity, CoverEntity): @property def is_closed(self): """Return true if cover is closed, else False.""" - door = self._get_door() - - if door.status == DoorStatus.OPENED: + door_status = self._get_door_status() + if door_status == DoorStatus.OPENED: return False - if door.status == DoorStatus.CLOSED: + if door_status == DoorStatus.CLOSED: return True return None @@ -96,8 +100,7 @@ class DeviceCover(GoGoGate2Entity, CoverEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - door = self._get_door() - if door.gate: + if self._get_door().gate: return DEVICE_CLASS_GATE return DEVICE_CLASS_GARAGE @@ -107,15 +110,32 @@ class DeviceCover(GoGoGate2Entity, CoverEntity): """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._get_door_status() == TransitionDoorStatus.CLOSING + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._get_door_status() == TransitionDoorStatus.OPENING + async def async_open_cover(self, **kwargs): """Open the door.""" await self._api.async_open_door(self._get_door().door_id) + await self.coordinator.async_refresh() async def async_close_cover(self, **kwargs): """Close the door.""" await self._api.async_close_door(self._get_door().door_id) + await self.coordinator.async_refresh() @property def extra_state_attributes(self): """Return the state attributes.""" return {"door_id": self._get_door().door_id} + + def _get_door_status(self) -> AbstractDoor: + return self._api.async_get_door_statuses_from_info(self.coordinator.data)[ + self._door.door_id + ] diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 3a044c33a94..9c4f3ee8e69 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -14,6 +14,7 @@ from ismartgate.common import ( ISmartGateInfoResponse, Network, Outputs, + TransitionDoorStatus, Wifi, ) @@ -44,7 +45,9 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, CONF_USERNAME, STATE_CLOSED, + STATE_CLOSING, STATE_OPEN, + STATE_OPENING, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -331,6 +334,10 @@ async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None api = MagicMock(GogoGate2Api) api.async_activate.return_value = GogoGate2ActivateResponse(result=True) api.async_info.return_value = info_response(DoorStatus.OPENED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.OPENED, + 2: DoorStatus.OPENED, + } gogogate2api_mock.return_value = api config_entry = MockConfigEntry( @@ -351,32 +358,102 @@ async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None assert dict(hass.states.get("cover.door1").attributes) == expected_attributes api.async_info.return_value = info_response(DoorStatus.CLOSED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.CLOSED, + 2: DoorStatus.CLOSED, + } await hass.services.async_call( COVER_DOMAIN, "close_cover", service_data={"entity_id": "cover.door1"}, ) + api.async_get_door_statuses_from_info.return_value = { + 1: TransitionDoorStatus.CLOSING, + 2: TransitionDoorStatus.CLOSING, + } + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_CLOSING + api.async_close_door.assert_called_with(1) + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_CLOSING + + api.async_info.return_value = info_response(DoorStatus.CLOSED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.CLOSED, + 2: DoorStatus.CLOSED, + } async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_CLOSED - api.async_close_door.assert_called_with(1) api.async_info.return_value = info_response(DoorStatus.OPENED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.OPENED, + 2: DoorStatus.OPENED, + } await hass.services.async_call( COVER_DOMAIN, "open_cover", service_data={"entity_id": "cover.door1"}, ) + api.async_get_door_statuses_from_info.return_value = { + 1: TransitionDoorStatus.OPENING, + 2: TransitionDoorStatus.OPENING, + } + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_OPENING + api.async_open_door.assert_called_with(1) + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_OPENING + + api.async_info.return_value = info_response(DoorStatus.OPENED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.OPENED, + 2: DoorStatus.OPENED, + } async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_OPEN - api.async_open_door.assert_called_with(1) api.async_info.return_value = info_response(DoorStatus.UNDEFINED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.UNDEFINED, + 2: DoorStatus.UNDEFINED, + } async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_UNKNOWN + api.async_info.return_value = info_response(DoorStatus.OPENED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.OPENED, + 2: DoorStatus.OPENED, + } + await hass.services.async_call( + COVER_DOMAIN, + "close_cover", + service_data={"entity_id": "cover.door1"}, + ) + await hass.services.async_call( + COVER_DOMAIN, + "open_cover", + service_data={"entity_id": "cover.door1"}, + ) + api.async_get_door_statuses_from_info.return_value = { + 1: TransitionDoorStatus.OPENING, + 2: TransitionDoorStatus.OPENING, + } + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_OPENING + api.async_open_door.assert_called_with(1) + assert await hass.config_entries.async_unload(config_entry.entry_id) assert not hass.states.async_entity_ids(DOMAIN) @@ -430,6 +507,10 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: api.async_info.side_effect = None api.async_info.return_value = closed_door_response + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.CLOSED, + 2: DoorStatus.CLOSED, + } async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_CLOSED From 7a87846146a1f1b4e392fd78671b16ef50a2eee6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 7 May 2021 13:03:11 +0200 Subject: [PATCH 219/852] Bump `gios` library (#50145) * Bump gios library * Use consts for API strings * Do not store data locally * Use API_TIMEOUT const --- homeassistant/components/gios/__init__.py | 7 ++- homeassistant/components/gios/air_quality.py | 52 +++++++++----------- homeassistant/components/gios/config_flow.py | 6 +-- homeassistant/components/gios/const.py | 30 +++++++++++ homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gios/__init__.py | 4 +- tests/fixtures/gios/sensors.json | 14 +++--- 9 files changed, 72 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 90e12061da3..9c4b76d8009 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -8,7 +8,7 @@ from gios import ApiError, Gios, InvalidSensorsData, NoStationError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_STATION_ID, DOMAIN, SCAN_INTERVAL +from .const import API_TIMEOUT, CONF_STATION_ID, DOMAIN, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -51,8 +51,8 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update data via library.""" try: - with timeout(30): - await self.gios.update() + with timeout(API_TIMEOUT): + return await self.gios.async_update() except ( ApiError, NoStationError, @@ -60,4 +60,3 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator): InvalidSensorsData, ) as error: raise UpdateFailed(error) from error - return self.gios.data diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py index ab83191a1ac..9e4df19e7ad 100644 --- a/homeassistant/components/gios/air_quality.py +++ b/homeassistant/components/gios/air_quality.py @@ -1,28 +1,24 @@ """Support for the GIOS service.""" -from homeassistant.components.air_quality import ( - ATTR_CO, - ATTR_NO2, - ATTR_OZONE, - ATTR_PM_2_5, - ATTR_PM_10, - ATTR_SO2, - AirQualityEntity, -) +from homeassistant.components.air_quality import AirQualityEntity from homeassistant.const import CONF_NAME from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_STATION, DEFAULT_NAME, DOMAIN, ICONS_MAP, MANUFACTURER - -ATTRIBUTION = "Data provided by GIOŚ" - -SENSOR_MAP = { - "CO": ATTR_CO, - "NO2": ATTR_NO2, - "O3": ATTR_OZONE, - "PM10": ATTR_PM_10, - "PM2.5": ATTR_PM_2_5, - "SO2": ATTR_SO2, -} +from .const import ( + API_AQI, + API_CO, + API_NO2, + API_O3, + API_PM10, + API_PM25, + API_SO2, + ATTR_STATION, + ATTRIBUTION, + DEFAULT_NAME, + DOMAIN, + ICONS_MAP, + MANUFACTURER, + SENSOR_MAP, +) PARALLEL_UPDATES = 1 @@ -72,43 +68,43 @@ class GiosAirQuality(CoordinatorEntity, AirQualityEntity): @property def air_quality_index(self): """Return the air quality index.""" - return self._get_sensor_value("AQI") + return self._get_sensor_value(API_AQI) @property @round_state def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" - return self._get_sensor_value("PM2.5") + return self._get_sensor_value(API_PM25) @property @round_state def particulate_matter_10(self): """Return the particulate matter 10 level.""" - return self._get_sensor_value("PM10") + return self._get_sensor_value(API_PM10) @property @round_state def ozone(self): """Return the O3 (ozone) level.""" - return self._get_sensor_value("O3") + return self._get_sensor_value(API_O3) @property @round_state def carbon_monoxide(self): """Return the CO (carbon monoxide) level.""" - return self._get_sensor_value("CO") + return self._get_sensor_value(API_CO) @property @round_state def sulphur_dioxide(self): """Return the SO2 (sulphur dioxide) level.""" - return self._get_sensor_value("SO2") + return self._get_sensor_value(API_SO2) @property @round_state def nitrogen_dioxide(self): """Return the NO2 (nitrogen dioxide) level.""" - return self._get_sensor_value("NO2") + return self._get_sensor_value(API_NO2) @property def attribution(self): diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index cdf166b4073..b351fafc0c1 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -10,7 +10,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_STATION_ID, DEFAULT_NAME, DOMAIN +from .const import API_TIMEOUT, CONF_STATION_ID, DEFAULT_NAME, DOMAIN DATA_SCHEMA = vol.Schema( { @@ -38,9 +38,9 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): websession = async_get_clientsession(self.hass) - with timeout(30): + with timeout(API_TIMEOUT): gios = Gios(user_input[CONF_STATION_ID], websession) - await gios.update() + await gios.async_update() return self.async_create_entry( title=user_input[CONF_STATION_ID], diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index ab354e319a8..4d3d7e139ce 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -1,6 +1,17 @@ """Constants for GIOS integration.""" from datetime import timedelta +from homeassistant.components.air_quality import ( + ATTR_CO, + ATTR_NO2, + ATTR_OZONE, + ATTR_PM_2_5, + ATTR_PM_10, + ATTR_SO2, +) + +ATTRIBUTION = "Data provided by GIOŚ" + ATTR_STATION = "station" CONF_STATION_ID = "station_id" DEFAULT_NAME = "GIOŚ" @@ -9,6 +20,16 @@ SCAN_INTERVAL = timedelta(minutes=30) DOMAIN = "gios" MANUFACTURER = "Główny Inspektorat Ochrony Środowiska" +API_AQI = "aqi" +API_CO = "co" +API_NO2 = "no2" +API_O3 = "o3" +API_PM10 = "pm10" +API_PM25 = "pm2.5" +API_SO2 = "so2" + +API_TIMEOUT = 30 + AQI_GOOD = "dobry" AQI_MODERATE = "umiarkowany" AQI_POOR = "dostateczny" @@ -22,3 +43,12 @@ ICONS_MAP = { AQI_POOR: "mdi:emoticon-sad", AQI_VERY_POOR: "mdi:emoticon-dead", } + +SENSOR_MAP = { + API_CO: ATTR_CO, + API_NO2: ATTR_NO2, + API_O3: ATTR_OZONE, + API_PM10: ATTR_PM_10, + API_PM25: ATTR_PM_2_5, + API_SO2: ATTR_SO2, +} diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index f0d5422de24..3dfb2a168db 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -3,7 +3,7 @@ "name": "GIO\u015a", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], - "requirements": ["gios==0.2.1"], + "requirements": ["gios==1.0.1"], "config_flow": true, "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 6ec0c43d4cc..3a4ea78760a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -657,7 +657,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.2 # homeassistant.components.gios -gios==0.2.1 +gios==1.0.1 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 176e202ae0d..e8095e104a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -360,7 +360,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.2 # homeassistant.components.gios -gios==0.2.1 +gios==1.0.1 # homeassistant.components.glances glances_api==0.2.0 diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 6b1aa982c71..729d0d50f61 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -26,8 +26,8 @@ async def init_integration(hass, incomplete_data=False) -> MockConfigEntry: sensors = json.loads(load_fixture("gios/sensors.json")) if incomplete_data: indexes["stIndexLevel"]["indexLevelName"] = "foo" - sensors["PM10"]["values"][0]["value"] = None - sensors["PM10"]["values"][1]["value"] = None + sensors["pm10"]["values"][0]["value"] = None + sensors["pm10"]["values"][1]["value"] = None with patch( "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS diff --git a/tests/fixtures/gios/sensors.json b/tests/fixtures/gios/sensors.json index 3103a2bf16e..62732552172 100644 --- a/tests/fixtures/gios/sensors.json +++ b/tests/fixtures/gios/sensors.json @@ -1,47 +1,47 @@ { - "SO2": { + "so2": { "values": [ { "date": "2020-07-31 15:00:00", "value": 4.35478 }, { "date": "2020-07-31 14:00:00", "value": 4.25478 }, { "date": "2020-07-31 13:00:00", "value": 4.34309 } ] }, - "C6H6": { + "c6h6": { "values": [ { "date": "2020-07-31 15:00:00", "value": 0.23789 }, { "date": "2020-07-31 14:00:00", "value": 0.22789 }, { "date": "2020-07-31 13:00:00", "value": 0.21315 } ] }, - "CO": { + "co": { "values": [ { "date": "2020-07-31 15:00:00", "value": 251.874 }, { "date": "2020-07-31 14:00:00", "value": 250.874 }, { "date": "2020-07-31 13:00:00", "value": 251.097 } ] }, - "NO2": { + "no2": { "values": [ { "date": "2020-07-31 15:00:00", "value": 7.13411 }, { "date": "2020-07-31 14:00:00", "value": 7.33411 }, { "date": "2020-07-31 13:00:00", "value": 9.32578 } ] }, - "O3": { + "o3": { "values": [ { "date": "2020-07-31 15:00:00", "value": 95.7768 }, { "date": "2020-07-31 14:00:00", "value": 93.7768 }, { "date": "2020-07-31 13:00:00", "value": 89.4232 } ] }, - "PM2.5": { + "pm2.5": { "values": [ { "date": "2020-07-31 15:00:00", "value": 4 }, { "date": "2020-07-31 14:00:00", "value": 4 }, { "date": "2020-07-31 13:00:00", "value": 5 } ] }, - "PM10": { + "pm10": { "values": [ { "date": "2020-07-31 15:00:00", "value": 16.8344 }, { "date": "2020-07-31 14:00:00", "value": 17.8344 }, From 7ab505633de0e04d6f1016b0b661b32f06f87704 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 7 May 2021 13:22:08 +0200 Subject: [PATCH 220/852] Ignore empty output from MQTT fan's value template (#50122) * Allow empty payload * Add tests for ignoring empty payload * logging on empty state and osccilation with tests * Improve warning log when invalid value is received --- homeassistant/components/mqtt/fan.py | 24 +++++-- tests/components/mqtt/test_fan.py | 97 ++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index bdbe3412539..b4718499d64 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -344,6 +344,9 @@ class MqttFan(MqttEntity, FanEntity): def state_received(msg): """Handle new received MQTT message.""" payload = self._value_templates[CONF_STATE](msg.payload) + if not payload: + _LOGGER.debug("Ignoring empty state from '%s'", msg.topic) + return if payload == self._payload["STATE_ON"]: self._state = True elif payload == self._payload["STATE_OFF"]: @@ -362,22 +365,27 @@ class MqttFan(MqttEntity, FanEntity): def percentage_received(msg): """Handle new received MQTT message for the percentage.""" numeric_val_str = self._value_templates[ATTR_PERCENTAGE](msg.payload) + if not numeric_val_str: + _LOGGER.debug("Ignoring empty speed from '%s'", msg.topic) + return try: percentage = ranged_value_to_percentage( self._speed_range, int(numeric_val_str) ) except ValueError: _LOGGER.warning( - "'%s' received on topic %s is not a valid speed within the speed range", + "'%s' received on topic %s. '%s' is not a valid speed within the speed range", msg.payload, msg.topic, + numeric_val_str, ) return if percentage < 0 or percentage > 100: _LOGGER.warning( - "'%s' received on topic %s is not a valid speed within the speed range", + "'%s' received on topic %s. '%s' is not a valid speed within the speed range", msg.payload, msg.topic, + numeric_val_str, ) return self._percentage = percentage @@ -396,11 +404,15 @@ class MqttFan(MqttEntity, FanEntity): def preset_mode_received(msg): """Handle new received MQTT message for preset mode.""" preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload) + if not preset_mode: + _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) + return if preset_mode not in self.preset_modes: _LOGGER.warning( - "'%s' received on topic %s is not a valid preset mode", + "'%s' received on topic %s. '%s' is not a valid preset mode", msg.payload, msg.topic, + preset_mode, ) return @@ -436,9 +448,10 @@ class MqttFan(MqttEntity, FanEntity): self._speed = speed else: _LOGGER.warning( - "'%s' received on topic %s is not a valid speed", + "'%s' received on topic %s. '%s' is not a valid speed", msg.payload, msg.topic, + speed, ) return @@ -464,6 +477,9 @@ class MqttFan(MqttEntity, FanEntity): def oscillation_received(msg): """Handle new received MQTT message for the oscillation.""" payload = self._value_templates[ATTR_OSCILLATING](msg.payload) + if not payload: + _LOGGER.debug("Ignoring empty oscillation from '%s'", msg.topic) + return if payload == self._payload["OSCILLATE_ON_PAYLOAD"]: self._oscillation = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index bfa1f387bcd..ee12a7ce03c 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -408,6 +408,10 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 + async_fire_mqtt_message(hass, "percentage-state-topic", '{"otherval": 100}') + assert "Ignoring empty speed from" in caplog.text + caplog.clear() + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "low"}') assert "not a valid preset mode" in caplog.text caplog.clear() @@ -424,6 +428,99 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap state = hass.states.get("fan.test") assert state.attributes.get("preset_mode") == "silent" + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"otherval": 100}') + assert "Ignoring empty preset_mode from" in caplog.text + caplog.clear() + + +async def test_controlling_state_via_topic_and_json_message_shared_topic( + hass, mqtt_mock, caplog +): + """Test the controlling state via topic and JSON message using a shared topic.""" + assert await async_setup_component( + hass, + fan.DOMAIN, + { + fan.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "shared-state-topic", + "command_topic": "command-topic", + "oscillation_state_topic": "shared-state-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_state_topic": "shared-state-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_state_topic": "shared-state-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + "smart", + "whoosh", + "eco", + "breeze", + "silent", + ], + "state_value_template": "{{ value_json.state }}", + "oscillation_value_template": "{{ value_json.oscillation }}", + "percentage_value_template": "{{ value_json.percentage }}", + "preset_mode_value_template": "{{ value_json.preset_mode }}", + "speed_range_min": 1, + "speed_range_max": 100, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("fan.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"ON","preset_mode":"eco","oscillation":"oscillate_on","percentage": 50}', + ) + state = hass.states.get("fan.test") + assert state.state == STATE_ON + assert state.attributes.get("oscillating") is True + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50 + assert state.attributes.get("preset_mode") == "eco" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"ON","preset_mode":"auto","oscillation":"oscillate_off","percentage": 10}', + ) + state = hass.states.get("fan.test") + assert state.state == STATE_ON + assert state.attributes.get("oscillating") is False + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 10 + assert state.attributes.get("preset_mode") == "auto" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"OFF","preset_mode":"auto","oscillation":"oscillate_off","percentage": 0}', + ) + state = hass.states.get("fan.test") + assert state.state == STATE_OFF + assert state.attributes.get("oscillating") is False + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 + assert state.attributes.get("preset_mode") == "auto" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"percentage": 100}', + ) + state = hass.states.get("fan.test") + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 + assert state.attributes.get("preset_mode") == "auto" + assert "Ignoring empty preset_mode from" in caplog.text + assert "Ignoring empty state from" in caplog.text + assert "Ignoring empty oscillation from" in caplog.text + caplog.clear() + async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): """Test optimistic mode without state topic.""" From 0c288bcabb7e1b2c371d406cc69b485ff83b93ad Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 7 May 2021 13:37:38 +0200 Subject: [PATCH 221/852] Fix mysensors default persistence file on import (#48410) --- .../components/mysensors/__init__.py | 32 ++-- .../components/mysensors/config_flow.py | 4 +- .../components/mysensors/test_config_flow.py | 3 + tests/components/mysensors/test_init.py | 169 ++++++++++++++---- 4 files changed, 157 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 812e6bf1670..9d23cfd24b6 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -58,18 +58,23 @@ DEFAULT_TCP_PORT = 5003 DEFAULT_VERSION = "1.4" +def set_default_persistence_file(value: dict) -> dict: + """Set default persistence file.""" + for idx, gateway in enumerate(value): + fil = gateway.get(CONF_PERSISTENCE_FILE) + if fil is not None: + continue + new_name = f"mysensors{idx + 1}.pickle" + gateway[CONF_PERSISTENCE_FILE] = new_name + + return value + + def has_all_unique_files(value): """Validate that all persistence files are unique and set if any is set.""" - persistence_files = [gateway.get(CONF_PERSISTENCE_FILE) for gateway in value] - if None in persistence_files and any( - name is not None for name in persistence_files - ): - raise vol.Invalid( - "persistence file name of all devices must be set if any is set" - ) - if not all(name is None for name in persistence_files): - schema = vol.Schema(vol.Unique()) - schema(persistence_files) + persistence_files = [gateway[CONF_PERSISTENCE_FILE] for gateway in value] + schema = vol.Schema(vol.Unique()) + schema(persistence_files) return value @@ -128,7 +133,10 @@ CONFIG_SCHEMA = vol.Schema( deprecated(CONF_PERSISTENCE), { vol.Required(CONF_GATEWAYS): vol.All( - cv.ensure_list, has_all_unique_files, [GATEWAY_SCHEMA] + cv.ensure_list, + set_default_persistence_file, + has_all_unique_files, + [GATEWAY_SCHEMA], ), vol.Optional(CONF_RETAIN, default=True): cv.boolean, vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, @@ -159,7 +167,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: CONF_TOPIC_IN_PREFIX: gw.get(CONF_TOPIC_IN_PREFIX, ""), CONF_RETAIN: config[CONF_RETAIN], CONF_VERSION: config[CONF_VERSION], - CONF_PERSISTENCE_FILE: gw.get(CONF_PERSISTENCE_FILE) + CONF_PERSISTENCE_FILE: gw[CONF_PERSISTENCE_FILE] # nodes config ignored at this time. renaming nodes can now be done from the frontend. } for gw in config[CONF_GATEWAYS] diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 59dff4829de..847408abcc5 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -324,7 +324,9 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except vol.Invalid: errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file" else: - real_persistence_path = self._normalize_persistence_file( + real_persistence_path = user_input[ + CONF_PERSISTENCE_FILE + ] = self._normalize_persistence_file( user_input[CONF_PERSISTENCE_FILE] ) for other_entry in self.hass.config_entries.async_entries(DOMAIN): diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index 66900066cd1..161d00e44b3 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -380,6 +380,9 @@ async def test_config_invalid( with patch( "homeassistant.components.mysensors.config_flow.try_connect", return_value=True + ), patch( + "homeassistant.components.mysensors.gateway.socket.getaddrinfo", + side_effect=OSError, ), patch( "homeassistant.components.mysensors.async_setup", return_value=True ) as mock_setup, patch( diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index 4fb51d6c17a..05e3df76285 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -32,7 +32,7 @@ from homeassistant.setup import async_setup_component @pytest.mark.parametrize( - "config, expected_calls, expected_to_succeed, expected_config_flow_user_input", + "config, expected_calls, expected_to_succeed, expected_config_entry_data", [ ( { @@ -52,13 +52,19 @@ from homeassistant.setup import async_setup_component }, 1, True, - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, - CONF_DEVICE: "COM5", - CONF_PERSISTENCE_FILE: "bla.json", - CONF_BAUD_RATE: 57600, - CONF_VERSION: "2.3", - }, + [ + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, + CONF_DEVICE: "COM5", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_BAUD_RATE: 57600, + CONF_VERSION: "2.3", + CONF_TCP_PORT: 5003, + CONF_TOPIC_IN_PREFIX: "", + CONF_TOPIC_OUT_PREFIX: "", + CONF_RETAIN: True, + } + ], ), ( { @@ -78,13 +84,19 @@ from homeassistant.setup import async_setup_component }, 1, True, - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, - CONF_DEVICE: "127.0.0.1", - CONF_PERSISTENCE_FILE: "blub.pickle", - CONF_TCP_PORT: 343, - CONF_VERSION: "2.4", - }, + [ + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, + CONF_DEVICE: "127.0.0.1", + CONF_PERSISTENCE_FILE: "blub.pickle", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.4", + CONF_BAUD_RATE: 115200, + CONF_TOPIC_IN_PREFIX: "", + CONF_TOPIC_OUT_PREFIX: "", + CONF_RETAIN: False, + } + ], ), ( { @@ -100,12 +112,19 @@ from homeassistant.setup import async_setup_component }, 1, True, - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, - CONF_DEVICE: "127.0.0.1", - CONF_TCP_PORT: 5003, - CONF_VERSION: DEFAULT_VERSION, - }, + [ + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, + CONF_DEVICE: "127.0.0.1", + CONF_TCP_PORT: 5003, + CONF_VERSION: DEFAULT_VERSION, + CONF_BAUD_RATE: 115200, + CONF_TOPIC_IN_PREFIX: "", + CONF_TOPIC_OUT_PREFIX: "", + CONF_RETAIN: False, + CONF_PERSISTENCE_FILE: "mysensors1.pickle", + } + ], ), ( { @@ -125,13 +144,19 @@ from homeassistant.setup import async_setup_component }, 1, True, - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, - CONF_DEVICE: "mqtt", - CONF_VERSION: DEFAULT_VERSION, - CONF_TOPIC_OUT_PREFIX: "outtopic", - CONF_TOPIC_IN_PREFIX: "intopic", - }, + [ + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, + CONF_DEVICE: "mqtt", + CONF_VERSION: DEFAULT_VERSION, + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_TOPIC_OUT_PREFIX: "outtopic", + CONF_TOPIC_IN_PREFIX: "intopic", + CONF_RETAIN: False, + CONF_PERSISTENCE_FILE: "mysensors1.pickle", + } + ], ), ( { @@ -149,7 +174,7 @@ from homeassistant.setup import async_setup_component }, 0, True, - {}, + [{}], ), ( { @@ -177,7 +202,30 @@ from homeassistant.setup import async_setup_component }, 2, True, - {}, + [ + { + CONF_DEVICE: "mqtt", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_TOPIC_OUT_PREFIX: "out", + CONF_TOPIC_IN_PREFIX: "in", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_VERSION: "2.4", + CONF_RETAIN: False, + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, + }, + { + CONF_DEVICE: "COM6", + CONF_PERSISTENCE_FILE: "bla2.json", + CONF_TOPIC_OUT_PREFIX: "", + CONF_TOPIC_IN_PREFIX: "", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_VERSION: "2.4", + CONF_RETAIN: False, + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, + }, + ], ), ( { @@ -203,7 +251,7 @@ from homeassistant.setup import async_setup_component }, 0, False, - {}, + [{}], ), ( { @@ -223,7 +271,47 @@ from homeassistant.setup import async_setup_component }, 0, True, - {}, + [{}], + ), + ( + { + DOMAIN: { + CONF_GATEWAYS: [ + { + CONF_DEVICE: "COM1", + }, + { + CONF_DEVICE: "COM2", + }, + ], + } + }, + 2, + True, + [ + { + CONF_DEVICE: "COM1", + CONF_PERSISTENCE_FILE: "mysensors1.pickle", + CONF_TOPIC_OUT_PREFIX: "", + CONF_TOPIC_IN_PREFIX: "", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_VERSION: "1.4", + CONF_RETAIN: True, + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, + }, + { + CONF_DEVICE: "COM2", + CONF_PERSISTENCE_FILE: "mysensors2.pickle", + CONF_TOPIC_OUT_PREFIX: "", + CONF_TOPIC_IN_PREFIX: "", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_VERSION: "1.4", + CONF_RETAIN: True, + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, + }, + ], ), ], ) @@ -233,7 +321,7 @@ async def test_import( config: ConfigType, expected_calls: int, expected_to_succeed: bool, - expected_config_flow_user_input: dict[str, Any], + expected_config_entry_data: list[dict[str, Any]], ) -> None: """Test importing a gateway.""" await async_setup_component(hass, "persistent_notification", {}) @@ -249,8 +337,13 @@ async def test_import( assert len(mock_setup_entry.mock_calls) == expected_calls - if expected_calls > 0: - config_flow_user_input = mock_setup_entry.mock_calls[0][1][1].data - for key, value in expected_config_flow_user_input.items(): - assert key in config_flow_user_input - assert config_flow_user_input[key] == value + for idx in range(expected_calls): + config_entry = mock_setup_entry.mock_calls[idx][1][1] + expected_persistence_file = expected_config_entry_data[idx].pop( + CONF_PERSISTENCE_FILE + ) + expected_persistence_path = hass.config.path(expected_persistence_file) + config_entry_data = dict(config_entry.data) + persistence_path = config_entry_data.pop(CONF_PERSISTENCE_FILE) + assert persistence_path == expected_persistence_path + assert config_entry_data == expected_config_entry_data[idx] From 5d5122c2a4f33fbdfd2b4f2d51d1044ab56102e4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 May 2021 14:21:03 +0200 Subject: [PATCH 222/852] Fix unique_id issue on onewire config entries (#50161) --- homeassistant/components/onewire/__init__.py | 4 ++-- homeassistant/components/onewire/binary_sensor.py | 2 +- homeassistant/components/onewire/sensor.py | 2 +- homeassistant/components/onewire/switch.py | 2 +- tests/components/onewire/test_init.py | 2 -- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 4bf0382a92c..fbcc5a5fe04 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): except CannotConnect as exc: raise ConfigEntryNotReady() from exc - hass.data[DOMAIN][config_entry.unique_id] = onewirehub + hass.data[DOMAIN][config_entry.entry_id] = onewirehub async def cleanup_registry() -> None: # Get registries @@ -71,5 +71,5 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): config_entry, PLATFORMS ) if unload_ok: - hass.data[DOMAIN].pop(config_entry.unique_id) + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 86b584c998c..4e25ba431c3 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up 1-Wire platform.""" # Only OWServer implementation works with binary sensors if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: - onewirehub = hass.data[DOMAIN][config_entry.unique_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job(get_entities, onewirehub) async_add_entities(entities, True) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index bc4dfc3dcf1..ef703c99c0f 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -250,7 +250,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up 1-Wire platform.""" - onewirehub = hass.data[DOMAIN][config_entry.unique_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job( get_entities, onewirehub, config_entry.data ) diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index da1ed01a980..1753800fbf0 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -144,7 +144,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up 1-Wire platform.""" # Only OWServer implementation works with switches if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: - onewirehub = hass.data[DOMAIN][config_entry.unique_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job(get_entities, onewirehub) async_add_entities(entities, True) diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 4a3724c5dd0..c715adcc16b 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -35,7 +35,6 @@ async def test_owserver_connect_failure(hass): CONF_HOST: "1.2.3.4", CONF_PORT: "1234", }, - unique_id=f"{CONF_TYPE_OWSERVER}:1.2.3.4:1234", options={}, entry_id="2", ) @@ -63,7 +62,6 @@ async def test_failed_owserver_listing(hass): CONF_HOST: "1.2.3.4", CONF_PORT: "1234", }, - unique_id=f"{CONF_TYPE_OWSERVER}:1.2.3.4:1234", options={}, entry_id="2", ) From 17fc962a87fc2f450fc97743da4a8da81ba029c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 May 2021 05:24:47 -0700 Subject: [PATCH 223/852] Bump aiohue to 2.3.0 (#50217) Co-authored-by: Franck Nijhof --- homeassistant/components/hue/binary_sensor.py | 12 ++++++++---- homeassistant/components/hue/bridge.py | 7 ++++++- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hue/sensor.py | 9 ++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index e408e995ad4..d5c6953700d 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -1,5 +1,4 @@ """Hue binary sensor entities.""" - from aiohue.sensors import TYPE_ZLL_PRESENCE from homeassistant.components.binary_sensor import ( @@ -15,9 +14,14 @@ PRESENCE_NAME_FORMAT = "{} motion" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer binary sensor setup to the shared sensor module.""" - await hass.data[HUE_DOMAIN][ - config_entry.entry_id - ].sensor_manager.async_register_component("binary_sensor", async_add_entities) + bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + + if not bridge.sensor_manager: + return + + await bridge.sensor_manager.async_register_component( + "binary_sensor", async_add_entities + ) class HuePresence(GenericZLLSensor, BinarySensorEntity): diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 698ad9e18e3..776ebbeb1f6 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -102,7 +102,8 @@ class HueBridge: return False self.api = bridge - self.sensor_manager = SensorManager(self) + if bridge.sensors is not None: + self.sensor_manager = SensorManager(self) hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) @@ -178,6 +179,10 @@ class HueBridge: async def hue_activate_scene(self, data, skip_reload=False, hide_warnings=False): """Service to call directly into bridge to set scenes.""" + if self.api.scenes is None: + _LOGGER.warning("Hub %s does not support scenes", self.api.host) + return + group_name = data[ATTR_GROUP_NAME] scene_name = data[ATTR_SCENE_NAME] transition = data.get(ATTR_TRANSITION) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index b86bcd61790..de00d31f2c7 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.1.0"], + "requirements": ["aiohue==2.3.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 6ac8d134327..3cd3b002f98 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -26,9 +26,12 @@ TEMPERATURE_NAME_FORMAT = "{} temperature" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer sensor setup to the shared sensor module.""" - await hass.data[HUE_DOMAIN][ - config_entry.entry_id - ].sensor_manager.async_register_component("sensor", async_add_entities) + bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + + if not bridge.sensor_manager: + return + + await bridge.sensor_manager.async_register_component("sensor", async_add_entities) class GenericHueGaugeSensorEntity(GenericZLLSensor, SensorEntity): diff --git a/requirements_all.txt b/requirements_all.txt index 3a4ea78760a..c7a24db3b5d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.1.0 +aiohue==2.3.0 # homeassistant.components.imap aioimaplib==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8095e104a6..57dfc7c352e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.1.0 +aiohue==2.3.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 55050bdd2a3851b7cee4139c4e36c7513a44cc18 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 7 May 2021 08:31:16 -0400 Subject: [PATCH 224/852] support more alarm panels (#50235) --- homeassistant/components/zha/api.py | 4 ++-- homeassistant/components/zha/core/helpers.py | 7 +++++-- homeassistant/components/zha/core/registries.py | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 2b41deaab6b..053162010e8 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -61,7 +61,7 @@ from .core.const import ( ) from .core.group import GroupMember from .core.helpers import ( - async_input_cluster_exists, + async_cluster_exists, async_is_bindable_target, convert_install_code, get_matched_clusters, @@ -897,7 +897,7 @@ async def websocket_get_configuration(hass, connection, msg): data = {"schemas": {}, "data": {}} for section, schema in ZHA_CONFIG_SCHEMAS.items(): - if section == ZHA_ALARM_OPTIONS and not async_input_cluster_exists( + if section == ZHA_ALARM_OPTIONS and not async_cluster_exists( hass, IasAce.cluster_id ): continue diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 84088148a8e..34359c19420 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -139,14 +139,17 @@ def async_get_zha_config_value(config_entry, section, config_key, default): ) -def async_input_cluster_exists(hass, cluster_id): +def async_cluster_exists(hass, cluster_id): """Determine if a device containing the specified in cluster is paired.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] zha_devices = zha_gateway.devices.values() for zha_device in zha_devices: clusters_by_endpoint = zha_device.async_get_clusters() for clusters in clusters_by_endpoint.values(): - if cluster_id in clusters[CLUSTER_TYPE_IN]: + if ( + cluster_id in clusters[CLUSTER_TYPE_IN] + or cluster_id in clusters[CLUSTER_TYPE_OUT] + ): return True return False diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 42f09d5323f..5fe7f806355 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -83,7 +83,8 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { } SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = { - zcl.clusters.general.OnOff.cluster_id: BINARY_SENSOR + zcl.clusters.general.OnOff.cluster_id: BINARY_SENSOR, + zcl.clusters.security.IasAce.cluster_id: ALARM, } BINDABLE_CLUSTERS = SetRegistry() From a7ef3ec947f346c68fb11bd4d558541148b520ac Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 7 May 2021 09:47:51 -0300 Subject: [PATCH 225/852] Fix RM pro temperature sensor (#50098) --- homeassistant/components/broadlink/sensor.py | 2 +- homeassistant/components/broadlink/updater.py | 16 +++++++++- tests/components/broadlink/test_device.py | 5 ++- tests/components/broadlink/test_sensors.py | 32 +++++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 0e42d8c438f..3f4a1e861b3 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [ BroadlinkSensor(device, monitored_condition) for monitored_condition in sensor_data - if sensor_data[monitored_condition] or device.api.type == "A1" + if sensor_data[monitored_condition] != 0 or device.api.type == "A1" ] async_add_entities(sensors) diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 8401dba8c0d..a84eec07d68 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -117,11 +117,25 @@ class BroadlinkRMUpdateManager(BroadlinkUpdateManager): device = self.device if hasattr(device.api, "check_sensors"): - return await device.async_request(device.api.check_sensors) + data = await device.async_request(device.api.check_sensors) + return self.normalize(data, self.coordinator.data) await device.async_request(device.api.update) return {} + @staticmethod + def normalize(data, previous_data): + """Fix firmware issue. + + See https://github.com/home-assistant/core/issues/42100. + """ + if data["temperature"] == -7: + if previous_data is None or previous_data["temperature"] is None: + data["temperature"] = None + elif abs(previous_data["temperature"] - data["temperature"]) > 3: + data["temperature"] = previous_data["temperature"] + return data + class BroadlinkSP1UpdateManager(BroadlinkUpdateManager): """Manages updates for Broadlink SP1 devices.""" diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index df22bcaffcb..8e53fd74c1c 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -144,7 +144,10 @@ async def test_device_setup_update_authorization_error(hass): """Test we handle an authorization error in the update step.""" device = get_device("Office") mock_api = device.get_mock_api() - mock_api.check_sensors.side_effect = (blke.AuthorizationError(), None) + mock_api.check_sensors.side_effect = ( + blke.AuthorizationError(), + {"temperature": 30}, + ) with patch.object( hass.config_entries, "async_forward_entry_setup" diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index de0cd88f288..e5d31705a4f 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -143,6 +143,38 @@ async def test_rm_pro_sensor_update(hass): assert sensors_and_states == {(f"{device.name} Temperature", "25.8")} +async def test_rm_pro_filter_crazy_temperature(hass): + """Test we filter a crazy temperature variation. + + Firmware issue. See https://github.com/home-assistant/core/issues/42100. + """ + device = get_device("Office") + mock_api = device.get_mock_api() + mock_api.check_sensors.return_value = {"temperature": 22.9} + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + assert len(sensors) == 1 + + mock_api.check_sensors.return_value = {"temperature": -7} + await hass.helpers.entity_component.async_update_entity( + next(iter(sensors)).entity_id + ) + assert mock_api.check_sensors.call_count == 2 + + sensors_and_states = { + (sensor.original_name, hass.states.get(sensor.entity_id).state) + for sensor in sensors + } + assert sensors_and_states == {(f"{device.name} Temperature", "22.9")} + + async def test_rm_mini3_no_sensor(hass): """Test we do not set up sensors for RM mini 3.""" device = get_device("Entrance") From 86393bdbba9661527416b4f22da0f3315fc35cf0 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 7 May 2021 15:10:46 +0200 Subject: [PATCH 226/852] Fix Netatmo climate (#50238) --- homeassistant/components/netatmo/climate.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index e53b060d7cf..1fb49d0c90d 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -651,8 +651,5 @@ def get_all_home_ids(home_data: pyatmo.HomeData) -> list[str]: return [ home_data.homes[home_id]["id"] for home_id in home_data.homes - if ( - "therm_schedules" in home_data.homes[home_id] - and "modules" in home_data.homes[home_id] - ) + if "modules" in home_data.homes[home_id] ] From d4601e00fdccf5fcbb4cebcc28632d23aeb42722 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 7 May 2021 07:41:37 -0600 Subject: [PATCH 227/852] Remove simplisafe websocket (#50213) --- .../components/simplisafe/__init__.py | 212 +----------------- .../simplisafe/alarm_control_panel.py | 58 ----- homeassistant/components/simplisafe/lock.py | 12 - .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 4 insertions(+), 284 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 4f9b10abb8c..c49aeb065e4 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -3,18 +3,7 @@ import asyncio from uuid import UUID from simplipy import API -from simplipy.entity import EntityTypes from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError -from simplipy.websocket import ( - EVENT_CAMERA_MOTION_DETECTED, - EVENT_CONNECTION_LOST, - EVENT_CONNECTION_RESTORED, - EVENT_DOORBELL_DETECTED, - EVENT_ENTRY_DELAY, - EVENT_LOCK_LOCKED, - EVENT_LOCK_UNLOCKED, - EVENT_SECRET_ALERT_TRIGGERED, -) import voluptuous as vol from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME @@ -25,10 +14,6 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.service import ( async_register_admin_service, verify_domain_control, @@ -57,9 +42,7 @@ from .const import ( ) DATA_LISTENER = "listener" -TOPIC_UPDATE_WEBSOCKET = "simplisafe_update_websocket_{0}" -EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT" EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" DEFAULT_SOCKET_MIN_RETRY = 15 @@ -71,23 +54,7 @@ PLATFORMS = ( "sensor", ) -WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] -WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT = [ - EVENT_CAMERA_MOTION_DETECTED, - EVENT_DOORBELL_DETECTED, - EVENT_ENTRY_DELAY, - EVENT_SECRET_ALERT_TRIGGERED, -] - ATTR_CATEGORY = "category" -ATTR_LAST_EVENT_CHANGED_BY = "last_event_changed_by" -ATTR_LAST_EVENT_INFO = "last_event_info" -ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" -ATTR_LAST_EVENT_SENSOR_SERIAL = "last_event_sensor_serial" -ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" -ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" -ATTR_LAST_EVENT_TYPE = "last_event_type" -ATTR_LAST_EVENT_TYPE = "last_event_type" ATTR_MESSAGE = "message" ATTR_PIN_LABEL = "label" ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" @@ -337,66 +304,6 @@ async def async_reload_entry(hass, config_entry): await hass.config_entries.async_reload(config_entry.entry_id) -class SimpliSafeWebsocket: - """Define a SimpliSafe websocket "manager" object.""" - - def __init__(self, hass, websocket): - """Initialize.""" - self._hass = hass - self._websocket = websocket - - @staticmethod - def _on_connect(): - """Define a handler to fire when the websocket is connected.""" - LOGGER.info("Connected to websocket") - - @staticmethod - def _on_disconnect(): - """Define a handler to fire when the websocket is disconnected.""" - LOGGER.info("Disconnected from websocket") - - def _on_event(self, event): - """Define a handler to fire when a new SimpliSafe event arrives.""" - LOGGER.debug("New websocket event: %s", event) - async_dispatcher_send( - self._hass, TOPIC_UPDATE_WEBSOCKET.format(event.system_id), event - ) - - if event.event_type not in WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT: - return - - if event.sensor_type: - sensor_type = event.sensor_type.name - else: - sensor_type = None - - self._hass.bus.async_fire( - EVENT_SIMPLISAFE_EVENT, - event_data={ - ATTR_LAST_EVENT_CHANGED_BY: event.changed_by, - ATTR_LAST_EVENT_TYPE: event.event_type, - ATTR_LAST_EVENT_INFO: event.info, - ATTR_LAST_EVENT_SENSOR_NAME: event.sensor_name, - ATTR_LAST_EVENT_SENSOR_SERIAL: event.sensor_serial, - ATTR_LAST_EVENT_SENSOR_TYPE: sensor_type, - ATTR_SYSTEM_ID: event.system_id, - ATTR_LAST_EVENT_TIMESTAMP: event.timestamp, - }, - ) - - async def async_connect(self): - """Register handlers and connect to the websocket.""" - self._websocket.on_connect(self._on_connect) - self._websocket.on_disconnect(self._on_disconnect) - self._websocket.on_event(self._on_event) - - await self._websocket.async_connect() - - async def async_disconnect(self): - """Disconnect from the websocket.""" - await self._websocket.async_disconnect() - - class SimpliSafe: """Define a SimpliSafe data object.""" @@ -408,9 +315,7 @@ class SimpliSafe: self._system_notifications = {} self.config_entry = config_entry self.coordinator = None - self.initial_event_to_use = {} self.systems = {} - self.websocket = SimpliSafeWebsocket(hass, api.websocket) @callback def _async_process_new_notifications(self, system): @@ -451,22 +356,6 @@ class SimpliSafe: async def async_init(self): """Initialize the data class.""" - # 2021-04-29: Disabling connection to the websocket due to the SimpliSafe cloud - # removing it (and not providing a clear alternative). - # asyncio.create_task(self.websocket.async_connect()) - - # async def async_websocket_disconnect(_): - # """Define an event handler to disconnect from the websocket.""" - # await self.websocket.async_disconnect() - - # 2021-04-29: Disabling disconnection from the websocket due to the SimpliSafe - # cloud removing it (and not providing a clear alternative). - # self._hass.data[DOMAIN][DATA_LISTENER][self.config_entry.entry_id].append( - # self._hass.bus.async_listen_once( - # EVENT_HOMEASSISTANT_STOP, async_websocket_disconnect - # ) - # ) - self.systems = await self._api.get_systems() for system in self.systems.values(): self._system_notifications[system.system_id] = set() @@ -477,17 +366,6 @@ class SimpliSafe: ) ) - # Future events will come from the websocket, but since subscription to the - # websocket doesn't provide the most recent event, we grab it from the REST - # API to ensure event-related attributes aren't empty on startup: - try: - self.initial_event_to_use[ - system.system_id - ] = await system.get_latest_event() - except SimplipyError as err: - LOGGER.error("Error while fetching initial event: %s", err) - self.initial_event_to_use[system.system_id] = {} - self.coordinator = DataUpdateCoordinator( self._hass, LOGGER, @@ -557,36 +435,13 @@ class SimpliSafeEntity(CoordinatorEntity): self._online = True self._simplisafe = simplisafe self._system = system - self.websocket_events_to_listen_for = [ - EVENT_CONNECTION_LOST, - EVENT_CONNECTION_RESTORED, - ] if serial: self._serial = serial else: self._serial = system.serial - try: - sensor_type = EntityTypes( - simplisafe.initial_event_to_use[system.system_id].get("sensorType") - ) - except ValueError: - sensor_type = EntityTypes.unknown - - self._attrs = { - ATTR_LAST_EVENT_INFO: simplisafe.initial_event_to_use[system.system_id].get( - "info" - ), - ATTR_LAST_EVENT_SENSOR_NAME: simplisafe.initial_event_to_use[ - system.system_id - ].get("sensorName"), - ATTR_LAST_EVENT_SENSOR_TYPE: sensor_type.name, - ATTR_LAST_EVENT_TIMESTAMP: simplisafe.initial_event_to_use[ - system.system_id - ].get("eventTimestamp"), - ATTR_SYSTEM_ID: system.system_id, - } + self._attrs = {ATTR_SYSTEM_ID: system.system_id} self._device_info = { "identifiers": {(DOMAIN, system.system_id)}, @@ -626,76 +481,15 @@ class SimpliSafeEntity(CoordinatorEntity): """Return the unique ID of the entity.""" return self._serial - @callback - def _async_internal_update_from_websocket_event(self, event): - """Perform internal websocket handling prior to handing off.""" - if event.event_type == EVENT_CONNECTION_LOST: - self._online = False - elif event.event_type == EVENT_CONNECTION_RESTORED: - self._online = True - - # It's uncertain whether SimpliSafe events will still propagate down the - # websocket when the base station is offline. Just in case, we guard against - # further action until connection is restored: - if not self._online: - return - - if event.sensor_type: - sensor_type = event.sensor_type.name - else: - sensor_type = None - - self._attrs.update( - { - ATTR_LAST_EVENT_INFO: event.info, - ATTR_LAST_EVENT_SENSOR_NAME: event.sensor_name, - ATTR_LAST_EVENT_SENSOR_TYPE: sensor_type, - ATTR_LAST_EVENT_TIMESTAMP: event.timestamp, - } - ) - - self.async_update_from_websocket_event(event) - @callback def _handle_coordinator_update(self): """Update the entity with new REST API data.""" self.async_update_from_rest_api() self.async_write_ha_state() - @callback - def _handle_websocket_update(self, event): - """Update the entity with new websocket data.""" - # Ignore this event if it belongs to a system other than this one: - if event.system_id != self._system.system_id: - return - - # Ignore this event if this entity hasn't expressed interest in its type: - if event.event_type not in self.websocket_events_to_listen_for: - return - - # Ignore this event if it belongs to a entity with a different serial - # number from this one's: - if ( - event.event_type in WEBSOCKET_EVENTS_REQUIRING_SERIAL - and event.sensor_serial != self._serial - ): - return - - self._async_internal_update_from_websocket_event(event) - self.async_write_ha_state() - async def async_added_to_hass(self): """Register callbacks.""" await super().async_added_to_hass() - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - TOPIC_UPDATE_WEBSOCKET.format(self._system.system_id), - self._handle_websocket_update, - ) - ) - self.async_update_from_rest_api() @callback @@ -703,10 +497,6 @@ class SimpliSafeEntity(CoordinatorEntity): """Update the entity with the provided REST API data.""" raise NotImplementedError() - @callback - def async_update_from_websocket_event(self, event): - """Update the entity with the provided websocket event.""" - class SimpliSafeBaseSensor(SimpliSafeEntity): """Define a SimpliSafe base (binary) sensor.""" diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 8f394890ad4..1f224683a41 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -3,19 +3,6 @@ import re from simplipy.errors import SimplipyError from simplipy.system import SystemStates -from simplipy.websocket import ( - EVENT_ALARM_CANCELED, - EVENT_ALARM_TRIGGERED, - EVENT_ARMED_AWAY, - EVENT_ARMED_AWAY_BY_KEYPAD, - EVENT_ARMED_AWAY_BY_REMOTE, - EVENT_ARMED_HOME, - EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, - EVENT_AWAY_EXIT_DELAY_BY_REMOTE, - EVENT_DISARMED_BY_MASTER_PIN, - EVENT_DISARMED_BY_REMOTE, - EVENT_HOME_EXIT_DELAY, -) from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -96,21 +83,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): else: self._state = None - for event_type in ( - EVENT_ALARM_CANCELED, - EVENT_ALARM_TRIGGERED, - EVENT_ARMED_AWAY, - EVENT_ARMED_AWAY_BY_KEYPAD, - EVENT_ARMED_AWAY_BY_REMOTE, - EVENT_ARMED_HOME, - EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, - EVENT_AWAY_EXIT_DELAY_BY_REMOTE, - EVENT_DISARMED_BY_MASTER_PIN, - EVENT_DISARMED_BY_REMOTE, - EVENT_HOME_EXIT_DELAY, - ): - self.websocket_events_to_listen_for.append(event_type) - @property def changed_by(self): """Return info about who changed the alarm last.""" @@ -237,33 +209,3 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self._state = STATE_ALARM_DISARMED else: self._state = None - - @callback - def async_update_from_websocket_event(self, event): - """Update the entity with the provided websocket API event data.""" - if event.event_type in ( - EVENT_ALARM_CANCELED, - EVENT_DISARMED_BY_MASTER_PIN, - EVENT_DISARMED_BY_REMOTE, - ): - self._state = STATE_ALARM_DISARMED - elif event.event_type == EVENT_ALARM_TRIGGERED: - self._state = STATE_ALARM_TRIGGERED - elif event.event_type in ( - EVENT_ARMED_AWAY, - EVENT_ARMED_AWAY_BY_KEYPAD, - EVENT_ARMED_AWAY_BY_REMOTE, - ): - self._state = STATE_ALARM_ARMED_AWAY - elif event.event_type == EVENT_ARMED_HOME: - self._state = STATE_ALARM_ARMED_HOME - elif event.event_type in ( - EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, - EVENT_AWAY_EXIT_DELAY_BY_REMOTE, - EVENT_HOME_EXIT_DELAY, - ): - self._state = STATE_ALARM_ARMING - else: - self._state = None - - self._changed_by = event.changed_by diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index a4d823efe38..8bfda08c1a5 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -1,7 +1,6 @@ """Support for SimpliSafe locks.""" from simplipy.errors import SimplipyError from simplipy.lock import LockStates -from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED from homeassistant.components.lock import LockEntity from homeassistant.core import callback @@ -39,9 +38,6 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): self._lock = lock self._is_locked = None - for event_type in (EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED): - self.websocket_events_to_listen_for.append(event_type) - @property def is_locked(self): """Return true if the lock is locked.""" @@ -81,11 +77,3 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): ) self._is_locked = self._lock.state == LockStates.locked - - @callback - def async_update_from_websocket_event(self, event): - """Update the entity with the provided websocket event data.""" - if event.event_type == EVENT_LOCK_LOCKED: - self._is_locked = True - else: - self._is_locked = False diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index d1016934694..79e11828eaa 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.10"], + "requirements": ["simplisafe-python==10.0.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index c7a24db3b5d..19b8afc732c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2067,7 +2067,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.10 +simplisafe-python==10.0.0 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57dfc7c352e..09625c588b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1101,7 +1101,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.10 +simplisafe-python==10.0.0 # homeassistant.components.slack slackclient==2.5.0 From dc29087416381cfc02aefe92f71cbd4817de2948 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 May 2021 15:46:23 +0200 Subject: [PATCH 228/852] Deprecate onewire YAML configuration (#50151) --- homeassistant/components/onewire/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index ef703c99c0f..02de1ed463e 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -236,6 +236,11 @@ def get_sensor_types(device_sub_type): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up 1-Wire platform.""" + _LOGGER.warning( + "Loading 1-Wire via platform setup is deprecated. " + "Please remove it from your configuration" + ) + if config.get(CONF_HOST): config[CONF_TYPE] = CONF_TYPE_OWSERVER elif config[CONF_MOUNT_DIR] == DEFAULT_SYSBUS_MOUNT_DIR: From 0587f834dfb81889e7a72af0a5753a63d48a485a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 7 May 2021 15:59:29 +0200 Subject: [PATCH 229/852] Add Nettigo Air Monitor integration (#49099) --- .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/nam/__init__.py | 106 ++++++++ homeassistant/components/nam/air_quality.py | 94 +++++++ homeassistant/components/nam/config_flow.py | 121 +++++++++ homeassistant/components/nam/const.py | 130 ++++++++++ homeassistant/components/nam/manifest.json | 11 + homeassistant/components/nam/model.py | 14 ++ homeassistant/components/nam/sensor.py | 94 +++++++ homeassistant/components/nam/strings.json | 24 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 4 + mypy.ini | 13 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/nam/__init__.py | 60 +++++ tests/components/nam/test_air_quality.py | 148 +++++++++++ tests/components/nam/test_config_flow.py | 175 +++++++++++++ tests/components/nam/test_init.py | 57 +++++ tests/components/nam/test_sensor.py | 266 ++++++++++++++++++++ 20 files changed, 1326 insertions(+) create mode 100644 homeassistant/components/nam/__init__.py create mode 100644 homeassistant/components/nam/air_quality.py create mode 100644 homeassistant/components/nam/config_flow.py create mode 100644 homeassistant/components/nam/const.py create mode 100644 homeassistant/components/nam/manifest.json create mode 100644 homeassistant/components/nam/model.py create mode 100644 homeassistant/components/nam/sensor.py create mode 100644 homeassistant/components/nam/strings.json create mode 100644 tests/components/nam/__init__.py create mode 100644 tests/components/nam/test_air_quality.py create mode 100644 tests/components/nam/test_config_flow.py create mode 100644 tests/components/nam/test_init.py create mode 100644 tests/components/nam/test_sensor.py diff --git a/.strict-typing b/.strict-typing index 1838866aadd..ede89de467f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -25,6 +25,7 @@ homeassistant.components.light.* homeassistant.components.lock.* homeassistant.components.mailbox.* homeassistant.components.media_player.* +homeassistant.components.nam.* homeassistant.components.notify.* homeassistant.components.number.* homeassistant.components.persistent_notification.* diff --git a/CODEOWNERS b/CODEOWNERS index f241832fb4e..eb46da1353d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -308,6 +308,7 @@ homeassistant/components/my/* @home-assistant/core homeassistant/components/myq/* @bdraco homeassistant/components/mysensors/* @MartinHjelmare @functionpointer homeassistant/components/mystrom/* @fabaff +homeassistant/components/nam/* @bieniu homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/nederlandse_spoorwegen/* @YarmoM homeassistant/components/nello/* @pschmitt diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py new file mode 100644 index 00000000000..04458967bed --- /dev/null +++ b/homeassistant/components/nam/__init__.py @@ -0,0 +1,106 @@ +"""The Nettigo Air Monitor component.""" +from __future__ import annotations + +import logging +from typing import cast + +from aiohttp import ClientSession +from aiohttp.client_exceptions import ClientConnectorError +import async_timeout +from nettigo_air_monitor import ( + ApiError, + DictToObj, + InvalidSensorData, + NettigoAirMonitor, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_NAME, DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["air_quality", "sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Nettigo as config entry.""" + host = entry.data[CONF_HOST] + + websession = async_get_clientsession(hass) + + coordinator = NAMDataUpdateCoordinator(hass, websession, host, entry.unique_id) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class NAMDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Nettigo Air Monitor data.""" + + def __init__( + self, + hass: HomeAssistant, + session: ClientSession, + host: str, + unique_id: str | None, + ) -> None: + """Initialize.""" + self.host = host + self.nam = NettigoAirMonitor(session, host) + self._unique_id = unique_id + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL + ) + + async def _async_update_data(self) -> DictToObj: + """Update data via library.""" + try: + # Device firmware uses synchronous code and doesn't respond to http queries + # when reading data from sensors. The nettigo-air-quality library tries to + # get the data 4 times, so we use a longer than usual timeout here. + with async_timeout.timeout(30): + data = await self.nam.async_update() + except (ApiError, ClientConnectorError, InvalidSensorData) as error: + raise UpdateFailed(error) from error + + _LOGGER.debug(data) + + return data + + @property + def unique_id(self) -> str | None: + """Return a unique_id.""" + return self._unique_id + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, cast(str, self._unique_id))}, + "name": DEFAULT_NAME, + "sw_version": self.nam.software_version, + "manufacturer": MANUFACTURER, + } diff --git a/homeassistant/components/nam/air_quality.py b/homeassistant/components/nam/air_quality.py new file mode 100644 index 00000000000..7823ffb110e --- /dev/null +++ b/homeassistant/components/nam/air_quality.py @@ -0,0 +1,94 @@ +"""Support for the Nettigo Air Monitor air_quality service.""" +from __future__ import annotations + +from homeassistant.components.air_quality import AirQualityEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NAMDataUpdateCoordinator +from .const import AIR_QUALITY_SENSORS, DEFAULT_NAME, DOMAIN, SUFFIX_P1, SUFFIX_P2 + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add a Nettigo Air Monitor entities from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + entities = [] + for sensor in AIR_QUALITY_SENSORS: + if f"{sensor}{SUFFIX_P1}" in coordinator.data: + entities.append(NAMAirQuality(coordinator, sensor)) + + async_add_entities(entities, False) + + +class NAMAirQuality(CoordinatorEntity, AirQualityEntity): + """Define an Nettigo Air Monitor air quality.""" + + coordinator: NAMDataUpdateCoordinator + + def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None: + """Initialize.""" + super().__init__(coordinator) + self.sensor_type = sensor_type + + @property + def name(self) -> str: + """Return the name.""" + return f"{DEFAULT_NAME} {AIR_QUALITY_SENSORS[self.sensor_type]}" + + @property + def particulate_matter_2_5(self) -> StateType: + """Return the particulate matter 2.5 level.""" + return round_state( + getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}") + ) + + @property + def particulate_matter_10(self) -> StateType: + """Return the particulate matter 10 level.""" + return round_state( + getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P1}") + ) + + @property + def carbon_dioxide(self) -> StateType: + """Return the particulate matter 10 level.""" + return round_state(getattr(self.coordinator.data, "conc_co2_ppm", None)) + + @property + def unique_id(self) -> str: + """Return a unique_id for this entity.""" + return f"{self.coordinator.unique_id}-{self.sensor_type}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return self.coordinator.device_info + + @property + def available(self) -> bool: + """Return if entity is available.""" + available = super().available + + # For a short time after booting, the device does not return values for all + # sensors. For this reason, we mark entities for which data is missing as + # unavailable. + return available and bool( + getattr(self.coordinator.data, f"{self.sensor_type}_p2", None) + ) + + +def round_state(state: StateType) -> StateType: + """Round state.""" + if isinstance(state, float): + return round(state) + + return state diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py new file mode 100644 index 00000000000..ccb5e6e6e84 --- /dev/null +++ b/homeassistant/components/nam/config_flow.py @@ -0,0 +1,121 @@ +"""Adds config flow for Nettigo Air Monitor.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any, cast + +from aiohttp.client_exceptions import ClientConnectorError +import async_timeout +from nettigo_air_monitor import ApiError, CannotGetMac, NettigoAirMonitor +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.typing import DiscoveryInfoType + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Nettigo Air Monitor.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize flow.""" + self.host: str | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + self.host = user_input[CONF_HOST] + try: + mac = await self._async_get_mac(cast(str, self.host)) + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except CannotGetMac: + return self.async_abort(reason="device_unsupported") + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + + await self.async_set_unique_id(format_mac(mac)) + self._abort_if_unique_id_configured({CONF_HOST: self.host}) + + return self.async_create_entry( + title=cast(str, self.host), + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=""): str, + } + ), + errors=errors, + ) + + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle zeroconf discovery.""" + self.host = discovery_info[CONF_HOST] + + try: + mac = await self._async_get_mac(cast(str, self.host)) + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + return self.async_abort(reason="cannot_connect") + except CannotGetMac: + return self.async_abort(reason="device_unsupported") + + await self.async_set_unique_id(format_mac(mac)) + self._abort_if_unique_id_configured({CONF_HOST: self.host}) + + self.context["title_placeholders"] = { + ATTR_NAME: discovery_info[ATTR_NAME].split(".")[0] + } + + return await self.async_step_confirm_discovery() + + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle discovery confirm.""" + errors: dict = {} + + if user_input is not None: + return self.async_create_entry( + title=cast(str, self.host), + data={CONF_HOST: self.host}, + ) + + self._set_confirm_only() + + return self.async_show_form( + step_id="confirm_discovery", + description_placeholders={CONF_HOST: self.host}, + errors=errors, + ) + + async def _async_get_mac(self, host: str) -> str: + """Get device MAC address.""" + websession = async_get_clientsession(self.hass) + nam = NettigoAirMonitor(websession, host) + # Device firmware uses synchronous code and doesn't respond to http queries + # when reading data from sensors. The nettigo-air-monitor library tries to get + # the data 4 times, so we use a longer than usual timeout here. + with async_timeout.timeout(30): + return cast(str, await nam.async_get_mac_address()) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py new file mode 100644 index 00000000000..b14bcaa6fa1 --- /dev/null +++ b/homeassistant/components/nam/const.py @@ -0,0 +1,130 @@ +"""Constants for Nettigo Air Monitor integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Final + +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) + +from .model import SensorDescription + +DEFAULT_NAME: Final = "Nettigo Air Monitor" +DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6) +DOMAIN: Final = "nam" +MANUFACTURER: Final = "Nettigo" + +SUFFIX_P1: Final = "_p1" +SUFFIX_P2: Final = "_p2" + +AIR_QUALITY_SENSORS: Final[dict[str, str]] = {"sds": "SDS011", "sps30": "SPS30"} + +SENSORS: Final[dict[str, SensorDescription]] = { + "bme280_humidity": { + "label": f"{DEFAULT_NAME} BME280 Humidity", + "unit": PERCENTAGE, + "device_class": DEVICE_CLASS_HUMIDITY, + "icon": None, + "enabled": True, + }, + "bme280_pressure": { + "label": f"{DEFAULT_NAME} BME280 Pressure", + "unit": PRESSURE_HPA, + "device_class": DEVICE_CLASS_PRESSURE, + "icon": None, + "enabled": True, + }, + "bme280_temperature": { + "label": f"{DEFAULT_NAME} BME280 Temperature", + "unit": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "enabled": True, + }, + "bmp280_pressure": { + "label": f"{DEFAULT_NAME} BMP280 Pressure", + "unit": PRESSURE_HPA, + "device_class": DEVICE_CLASS_PRESSURE, + "icon": None, + "enabled": True, + }, + "bmp280_temperature": { + "label": f"{DEFAULT_NAME} BMP280 Temperature", + "unit": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "enabled": True, + }, + "heca_humidity": { + "label": f"{DEFAULT_NAME} HECA Humidity", + "unit": PERCENTAGE, + "device_class": DEVICE_CLASS_HUMIDITY, + "icon": None, + "enabled": True, + }, + "heca_temperature": { + "label": f"{DEFAULT_NAME} HECA Temperature", + "unit": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "enabled": True, + }, + "sht3x_humidity": { + "label": f"{DEFAULT_NAME} SHT3X Humidity", + "unit": PERCENTAGE, + "device_class": DEVICE_CLASS_HUMIDITY, + "icon": None, + "enabled": True, + }, + "sht3x_temperature": { + "label": f"{DEFAULT_NAME} SHT3X Temperature", + "unit": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "enabled": True, + }, + "sps30_p0": { + "label": f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "device_class": None, + "icon": "mdi:blur", + "enabled": True, + }, + "sps30_p4": { + "label": f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "device_class": None, + "icon": "mdi:blur", + "enabled": True, + }, + "humidity": { + "label": f"{DEFAULT_NAME} DHT22 Humidity", + "unit": PERCENTAGE, + "device_class": DEVICE_CLASS_HUMIDITY, + "icon": None, + "enabled": True, + }, + "signal": { + "label": f"{DEFAULT_NAME} Signal Strength", + "unit": SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + "device_class": DEVICE_CLASS_SIGNAL_STRENGTH, + "icon": None, + "enabled": False, + }, + "temperature": { + "label": f"{DEFAULT_NAME} DHT22 Temperature", + "unit": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "enabled": True, + }, +} diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json new file mode 100644 index 00000000000..80a31fe1596 --- /dev/null +++ b/homeassistant/components/nam/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "nam", + "name": "Nettigo Air Monitor", + "documentation": "https://www.home-assistant.io/integrations/nam", + "codeowners": ["@bieniu"], + "requirements": ["nettigo-air-monitor==0.2.5"], + "zeroconf": [{"type": "_http._tcp.local.", "name": "nam-*"}], + "config_flow": true, + "quality_scale": "platinum", + "iot_class": "local_polling" +} diff --git a/homeassistant/components/nam/model.py b/homeassistant/components/nam/model.py new file mode 100644 index 00000000000..8d1bfe29a4a --- /dev/null +++ b/homeassistant/components/nam/model.py @@ -0,0 +1,14 @@ +"""Type definitions for Nettig Air Monitor integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class SensorDescription(TypedDict): + """Sensor description class.""" + + label: str + unit: str | None + device_class: str | None + icon: str | None + enabled: bool diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py new file mode 100644 index 00000000000..39da7742bed --- /dev/null +++ b/homeassistant/components/nam/sensor.py @@ -0,0 +1,94 @@ +"""Support for the Nettigo Air Monitor service.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NAMDataUpdateCoordinator +from .const import DOMAIN, SENSORS + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add a Nettigo Air Monitor entities from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + sensors = [] + for sensor in SENSORS: + if sensor in coordinator.data: + sensors.append(NAMSensor(coordinator, sensor)) + + async_add_entities(sensors, False) + + +class NAMSensor(CoordinatorEntity, SensorEntity): + """Define an Nettigo Air Monitor sensor.""" + + coordinator: NAMDataUpdateCoordinator + + def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None: + """Initialize.""" + super().__init__(coordinator) + self.sensor_type = sensor_type + self._description = SENSORS[self.sensor_type] + + @property + def name(self) -> str: + """Return the name.""" + return self._description["label"] + + @property + def state(self) -> Any: + """Return the state.""" + return getattr(self.coordinator.data, self.sensor_type) + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit the value is expressed in.""" + return self._description["unit"] + + @property + def device_class(self) -> str | None: + """Return the class of this sensor.""" + return self._description["device_class"] + + @property + def icon(self) -> str | None: + """Return the icon.""" + return self._description["icon"] + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._description["enabled"] + + @property + def unique_id(self) -> str: + """Return a unique_id for this entity.""" + return f"{self.coordinator.unique_id}-{self.sensor_type}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return self.coordinator.device_info + + @property + def available(self) -> bool: + """Return if entity is available.""" + available = super().available + + # For a short time after booting, the device does not return values for all + # sensors. For this reason, we mark entities for which data is missing as + # unavailable. + return available and bool( + getattr(self.coordinator.data, self.sensor_type, None) + ) diff --git a/homeassistant/components/nam/strings.json b/homeassistant/components/nam/strings.json new file mode 100644 index 00000000000..e8994a346bf --- /dev/null +++ b/homeassistant/components/nam/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Set up Nettigo Air Monitor integration.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "confirm_discovery": { + "description": "Do you want to set up Nettigo Air Monitor at {host}?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "device_unsupported": "The device is unsupported." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 35b48cf4cb5..6adcb16cc15 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -159,6 +159,7 @@ FLOWS = [ "mutesync", "myq", "mysensors", + "nam", "neato", "nest", "netatmo", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 0b1c0adb9c6..d4e490170d0 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -94,6 +94,10 @@ ZEROCONF = { } ], "_http._tcp.local.": [ + { + "domain": "nam", + "name": "nam-*" + }, { "domain": "rachio", "name": "rachio*" diff --git a/mypy.ini b/mypy.ini index e457c331199..5b757c9eb1d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -334,6 +334,19 @@ warn_return_any = true warn_unreachable = true warn_unused_ignores = true +[mypy-homeassistant.components.nam.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + [mypy-homeassistant.components.notify.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 19b8afc732c..559658659a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -987,6 +987,9 @@ netdata==0.2.0 # homeassistant.components.ssdp netdisco==2.8.3 +# homeassistant.components.nam +nettigo-air-monitor==0.2.5 + # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09625c588b3..01173646f32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -535,6 +535,9 @@ nessclient==0.9.15 # homeassistant.components.ssdp netdisco==2.8.3 +# homeassistant.components.nam +nettigo-air-monitor==0.2.5 + # homeassistant.components.nexia nexia==0.9.6 diff --git a/tests/components/nam/__init__.py b/tests/components/nam/__init__.py new file mode 100644 index 00000000000..1b6f89b76df --- /dev/null +++ b/tests/components/nam/__init__.py @@ -0,0 +1,60 @@ +"""Tests for the Nettigo Air Monitor integration.""" +from unittest.mock import patch + +from homeassistant.components.nam.const import DOMAIN + +from tests.common import MockConfigEntry + +INCOMPLETE_NAM_DATA = { + "software_version": "NAMF-2020-36", + "sensordatavalues": [], +} + +nam_data = { + "software_version": "NAMF-2020-36", + "sensordatavalues": [ + {"value_type": "SDS_P1", "value": "18.65"}, + {"value_type": "SDS_P2", "value": "11.03"}, + {"value_type": "SPS30_P0", "value": "31.23"}, + {"value_type": "SPS30_P1", "value": "21.23"}, + {"value_type": "SPS30_P2", "value": "34.32"}, + {"value_type": "SPS30_P4", "value": "24.72"}, + {"value_type": "conc_co2_ppm", "value": "865"}, + {"value_type": "BME280_temperature", "value": "7.56"}, + {"value_type": "BME280_humidity", "value": "45.69"}, + {"value_type": "BME280_pressure", "value": "101101.17"}, + {"value_type": "BMP280_temperature", "value": "5.56"}, + {"value_type": "BMP280_pressure", "value": "102201.18"}, + {"value_type": "SHT3X_temperature", "value": "6.28"}, + {"value_type": "SHT3X_humidity", "value": "34.69"}, + {"value_type": "humidity", "value": "46.23"}, + {"value_type": "temperature", "value": "6.26"}, + {"value_type": "HECA_temperature", "value": "7.95"}, + {"value_type": "HECA_humidity", "value": "49.97"}, + {"value_type": "signal", "value": "-72"}, + ], +} + + +async def init_integration(hass, co2_sensor=True) -> MockConfigEntry: + """Set up the Nettigo Air Monitor integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={"host": "10.10.2.3"}, + ) + + if not co2_sensor: + # Remove conc_co2_ppm value + nam_data["sensordatavalues"].pop(6) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=nam_data, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/nam/test_air_quality.py b/tests/components/nam/test_air_quality.py new file mode 100644 index 00000000000..f9a213cec3e --- /dev/null +++ b/tests/components/nam/test_air_quality.py @@ -0,0 +1,148 @@ +"""Test air_quality of Nettigo Air Monitor integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nettigo_air_monitor import ApiError + +from homeassistant.components.air_quality import ATTR_CO2, ATTR_PM_2_5, ATTR_PM_10 +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + STATE_UNAVAILABLE, +) +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from . import INCOMPLETE_NAM_DATA, nam_data + +from tests.common import async_fire_time_changed +from tests.components.nam import init_integration + + +async def test_air_quality(hass): + """Test states of the air_quality.""" + await init_integration(hass) + registry = er.async_get(hass) + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state == "11" + assert state.attributes.get(ATTR_PM_10) == 19 + assert state.attributes.get(ATTR_PM_2_5) == 11 + assert state.attributes.get(ATTR_CO2) == 865 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + entry = registry.async_get("air_quality.nettigo_air_monitor_sds011") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds" + + state = hass.states.get("air_quality.nettigo_air_monitor_sps30") + assert state + assert state.state == "34" + assert state.attributes.get(ATTR_PM_10) == 21 + assert state.attributes.get(ATTR_PM_2_5) == 34 + assert state.attributes.get(ATTR_CO2) == 865 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + entry = registry.async_get("air_quality.nettigo_air_monitor_sps30") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30" + + +async def test_air_quality_without_co2_value(hass): + """Test states of the air_quality.""" + await init_integration(hass, co2_sensor=False) + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.attributes.get(ATTR_CO2) is None + + +async def test_incompleta_data_after_device_restart(hass): + """Test states of the air_quality after device restart.""" + await init_integration(hass) + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state == "11" + assert state.attributes.get(ATTR_PM_10) == 19 + assert state.attributes.get(ATTR_PM_2_5) == 11 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + future = utcnow() + timedelta(minutes=6) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=INCOMPLETE_NAM_DATA, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state == STATE_UNAVAILABLE + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when device causes an error.""" + await init_integration(hass) + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "11" + + future = utcnow() + timedelta(minutes=6) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=12) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=nam_data, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "11" + + +async def test_manual_update_entity(hass): + """Test manual update entity via service homeasasistant/update_entity.""" + await init_integration(hass) + + await async_setup_component(hass, "homeassistant", {}) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=nam_data, + ) as mock_get_data: + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["air_quality.nettigo_air_monitor_sds011"]}, + blocking=True, + ) + + assert mock_get_data.call_count == 1 diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py new file mode 100644 index 00000000000..99a252ada0a --- /dev/null +++ b/tests/components/nam/test_config_flow.py @@ -0,0 +1,175 @@ +"""Define tests for the Nettigo Air Monitor config flow.""" +import asyncio +from unittest.mock import patch + +from nettigo_air_monitor import ApiError, CannotGetMac +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.nam.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF + +from tests.common import MockConfigEntry + +DISCOVERY_INFO = {"host": "10.10.2.3", "name": "NAM-12345"} +VALID_CONFIG = {"host": "10.10.2.3"} + + +async def test_form_create_entry(hass): + """Test that the user step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), patch( + "homeassistant.components.nam.async_setup_entry", return_value=True + ) as mock_setup_entry: + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "10.10.2.3" + assert result["data"]["host"] == "10.10.2.3" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "error", + [ + (ApiError("Invalid response from device 10.10.2.3: 404"), "cannot_connect"), + (asyncio.TimeoutError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_form_errors(hass, error): + """Test we handle errors.""" + exc, base_error = error + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + side_effect=exc, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["errors"] == {"base": base_error} + + +async def test_form_abort(hass): + """Test we handle abort after error.""" + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + side_effect=CannotGetMac("Cannot get MAC address from device"), + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "device_unsupported" + + +async def test_form_already_configured(hass): + """Test that errors are shown when duplicates are added.""" + entry = MockConfigEntry( + domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=VALID_CONFIG + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Test config entry got updated with latest IP + assert entry.data["host"] == "1.1.1.1" + + +async def test_zeroconf(hass): + """Test we get the form.""" + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": SOURCE_ZEROCONF}, + ) + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + assert context["title_placeholders"]["name"] == "NAM-12345" + assert context["confirm_only"] is True + + with patch( + "homeassistant.components.nam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "10.10.2.3" + assert result["data"] == {"host": "10.10.2.3"} + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "error", + [ + (ApiError("Invalid response from device 10.10.2.3: 404"), "cannot_connect"), + (CannotGetMac("Cannot get MAC address from device"), "device_unsupported"), + ], +) +async def test_zeroconf_errors(hass, error): + """Test we handle errors.""" + exc, reason = error + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + side_effect=exc, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": SOURCE_ZEROCONF}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == reason diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py new file mode 100644 index 00000000000..01cf97fa6ab --- /dev/null +++ b/tests/components/nam/test_init.py @@ -0,0 +1,57 @@ +"""Test init of Nettigo Air Monitor integration.""" +from unittest.mock import patch + +from nettigo_air_monitor import ApiError + +from homeassistant.components.nam.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import STATE_UNAVAILABLE + +from tests.common import MockConfigEntry +from tests.components.nam import init_integration + + +async def test_async_setup_entry(hass): + """Test a successful setup entry.""" + await init_integration(hass) + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "11" + + +async def test_config_not_ready(hass): + """Test for setup failure if the connection to the device fails.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={"host": "10.10.2.3"}, + ) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + side_effect=ApiError("API Error"), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py new file mode 100644 index 00000000000..148b048da90 --- /dev/null +++ b/tests/components/nam/test_sensor.py @@ -0,0 +1,266 @@ +"""Test sensor of Nettigo Air Monitor integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nettigo_air_monitor import ApiError + +from homeassistant.components.nam.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + STATE_UNAVAILABLE, + TEMP_CELSIUS, +) +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from . import INCOMPLETE_NAM_DATA, nam_data + +from tests.common import async_fire_time_changed +from tests.components.nam import init_integration + + +async def test_sensor(hass): + """Test states of the air_quality.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aa:bb:cc:dd:ee:ff-signal", + suggested_object_id="nettigo_air_monitor_signal_strength", + disabled_by=None, + ) + + await init_integration(hass) + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_humidity") + assert state + assert state.state == "45.7" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.nettigo_air_monitor_bme280_humidity") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_humidity" + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") + assert state + assert state.state == "7.6" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_bme280_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_pressure") + assert state + assert state.state == "1011" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + + entry = registry.async_get("sensor.nettigo_air_monitor_bme280_pressure") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_pressure" + + state = hass.states.get("sensor.nettigo_air_monitor_bmp280_temperature") + assert state + assert state.state == "5.6" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp280_temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_bmp280_pressure") + assert state + assert state.state == "1022" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + + entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_pressure") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp280_pressure" + + state = hass.states.get("sensor.nettigo_air_monitor_sht3x_humidity") + assert state + assert state.state == "34.7" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_humidity") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sht3x_humidity" + + state = hass.states.get("sensor.nettigo_air_monitor_sht3x_temperature") + assert state + assert state.state == "6.3" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sht3x_temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_dht22_humidity") + assert state + assert state.state == "46.2" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-humidity" + + state = hass.states.get("sensor.nettigo_air_monitor_dht22_temperature") + assert state + assert state.state == "6.3" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_heca_humidity") + assert state + assert state.state == "50.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.nettigo_air_monitor_heca_humidity") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-heca_humidity" + + state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") + assert state + assert state.state == "8.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_heca_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-heca_temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_signal_strength") + assert state + assert state.state == "-72" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) + + entry = registry.async_get("sensor.nettigo_air_monitor_signal_strength") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-signal" + + +async def test_sensor_disabled(hass): + """Test sensor disabled by default.""" + await init_integration(hass) + registry = er.async_get(hass) + + entry = registry.async_get("sensor.nettigo_air_monitor_signal_strength") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-signal" + assert entry.disabled + assert entry.disabled_by == er.DISABLED_INTEGRATION + + # Test enabling entity + updated_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + + assert updated_entry != entry + assert updated_entry.disabled is False + + +async def test_incompleta_data_after_device_restart(hass): + """Test states of the air_quality after device restart.""" + await init_integration(hass) + + state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") + assert state + assert state.state == "8.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + future = utcnow() + timedelta(minutes=6) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=INCOMPLETE_NAM_DATA, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") + assert state + assert state.state == STATE_UNAVAILABLE + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when device causes an error.""" + await init_integration(hass) + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "7.6" + + future = utcnow() + timedelta(minutes=6) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=12) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=nam_data, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "7.6" + + +async def test_manual_update_entity(hass): + """Test manual update entity via service homeasasistant/update_entity.""" + await init_integration(hass) + + await async_setup_component(hass, "homeassistant", {}) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=nam_data, + ) as mock_get_data: + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.nettigo_air_monitor_bme280_temperature"]}, + blocking=True, + ) + + assert mock_get_data.call_count == 1 From 4d0955bae142421887df47483aff18d1b9e4ef5e Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 7 May 2021 16:05:16 +0200 Subject: [PATCH 230/852] Add Fritz sensors (#50055) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/fritz/const.py | 4 +- homeassistant/components/fritz/sensor.py | 141 +++++++++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fritz/sensor.py diff --git a/.coveragerc b/.coveragerc index 8e0b7dba206..f86ea86d2d1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -334,6 +334,7 @@ omit = homeassistant/components/fritz/common.py homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py + homeassistant/components/fritz/sensor.py homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/base.py diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index fff55a276e1..0bc33786a0f 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -2,7 +2,7 @@ DOMAIN = "fritz" -PLATFORMS = ["binary_sensor", "device_tracker"] +PLATFORMS = ["binary_sensor", "device_tracker", "sensor"] DATA_FRITZ = "fritz_data" @@ -17,3 +17,5 @@ ERROR_CONNECTION_ERROR = "connection_error" ERROR_UNKNOWN = "unknown_error" TRACKER_SCAN_INTERVAL = 30 + +UPTIME_DEVIATION = 5 diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py new file mode 100644 index 00000000000..21c121ea295 --- /dev/null +++ b/homeassistant/components/fritz/sensor.py @@ -0,0 +1,141 @@ +"""AVM FRITZ!Box binary sensors.""" +from __future__ import annotations + +import datetime +import logging + +from fritzconnection.core.exceptions import FritzConnectionException + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from .common import FritzBoxBaseEntity, FritzBoxTools +from .const import DOMAIN, UPTIME_DEVIATION + +_LOGGER = logging.getLogger(__name__) + + +def _retrieve_uptime_state(status, last_value): + """Return uptime from device.""" + delta_uptime = utcnow() - datetime.timedelta(seconds=status.uptime) + + if ( + not last_value + or abs( + (delta_uptime - datetime.datetime.fromisoformat(last_value)).total_seconds() + ) + > UPTIME_DEVIATION + ): + return delta_uptime.replace(microsecond=0).isoformat() + + return last_value + + +def _retrieve_external_ip_state(status, last_value): + """Return external ip from device.""" + return status.external_ip + + +SENSOR_NAME = 0 +SENSOR_DEVICE_CLASS = 1 +SENSOR_ICON = 2 +SENSOR_STATE_PROVIDER = 3 + +# sensor_type: [name, device_class, icon, state_provider] +SENSOR_DATA = { + "external_ip": [ + "External IP", + None, + "mdi:earth", + _retrieve_external_ip_state, + ], + "uptime": ["Uptime", DEVICE_CLASS_TIMESTAMP, None, _retrieve_uptime_state], +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up entry.""" + _LOGGER.debug("Setting up FRITZ!Box sensors") + fritzbox_tools = hass.data[DOMAIN][entry.entry_id] + + if "WANIPConn1" not in fritzbox_tools.connection.services: + # Only routers are supported at the moment + return + + for sensor_type in SENSOR_DATA: + async_add_entities( + [FritzBoxSensor(fritzbox_tools, entry.title, sensor_type)], + True, + ) + + +class FritzBoxSensor(FritzBoxBaseEntity, BinarySensorEntity): + """Define FRITZ!Box connectivity class.""" + + def __init__( + self, fritzbox_tools: FritzBoxTools, device_friendlyname: str, sensor_type: str + ) -> None: + """Init FRITZ!Box connectivity class.""" + self._sensor_data = SENSOR_DATA[sensor_type] + self._unique_id = f"{fritzbox_tools.unique_id}-{sensor_type}" + self._name = f"{device_friendlyname} {self._sensor_data[SENSOR_NAME]}" + self._is_available = True + self._last_value: str | None = None + self._state: str | None = None + super().__init__(fritzbox_tools, device_friendlyname) + + @property + def _state_provider(self): + """Return the state provider for the binary sensor.""" + return self._sensor_data[SENSOR_STATE_PROVIDER] + + @property + def name(self): + """Return name.""" + return self._name + + @property + def device_class(self) -> str | None: + """Return device class.""" + return self._sensor_data[SENSOR_DEVICE_CLASS] + + @property + def icon(self): + """Return icon.""" + return self._sensor_data[SENSOR_ICON] + + @property + def unique_id(self): + """Return unique id.""" + return self._unique_id + + @property + def state(self) -> str | None: + """Return the state of the sensor.""" + return self._state + + @property + def available(self) -> bool: + """Return availability.""" + return self._is_available + + def update(self) -> None: + """Update data.""" + _LOGGER.debug("Updating FRITZ!Box sensors") + + try: + status = self._fritzbox_tools.fritzstatus + self._is_available = True + + self._state = self._last_value = self._state_provider( + status, self._last_value + ) + + except FritzConnectionException: + _LOGGER.error("Error getting the state from the FRITZ!Box", exc_info=True) + self._is_available = False From 6df0190aeb944373c7abc15de04947b7fcc8fc68 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 7 May 2021 16:47:52 +0200 Subject: [PATCH 231/852] Improve type annotations for Airly integration (#49898) --- .strict-typing | 1 + homeassistant/components/airly/__init__.py | 63 ++++++---- homeassistant/components/airly/air_quality.py | 71 ++++++----- homeassistant/components/airly/config_flow.py | 18 ++- homeassistant/components/airly/const.py | 89 ++++++++++---- homeassistant/components/airly/model.py | 13 +++ homeassistant/components/airly/sensor.py | 110 +++++++----------- .../components/airly/system_health.py | 6 +- mypy.ini | 16 ++- script/hassfest/mypy_config.py | 1 - tests/components/airly/test_init.py | 38 +++++- 11 files changed, 268 insertions(+), 158 deletions(-) create mode 100644 homeassistant/components/airly/model.py diff --git a/.strict-typing b/.strict-typing index ede89de467f..175142e217e 100644 --- a/.strict-typing +++ b/.strict-typing @@ -3,6 +3,7 @@ # to enable strict mypy checks. homeassistant.components +homeassistant.components.airly.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* homeassistant.components.bond.* diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index f855b30db48..6fa8914e822 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,15 +1,21 @@ """The Airly integration.""" +from __future__ import annotations + from datetime import timedelta import logging from math import ceil +from aiohttp import ClientSession from aiohttp.client_exceptions import ClientConnectorError from airly import Airly from airly.exceptions import AirlyError import async_timeout +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -30,7 +36,7 @@ PLATFORMS = ["air_quality", "sensor"] _LOGGER = logging.getLogger(__name__) -def set_update_interval(instances, requests_remaining): +def set_update_interval(instances_count: int, requests_remaining: int) -> timedelta: """ Return data update interval. @@ -46,7 +52,7 @@ def set_update_interval(instances, requests_remaining): interval = timedelta( minutes=min( max( - ceil(minutes_to_midnight / requests_remaining * instances), + ceil(minutes_to_midnight / requests_remaining * instances_count), MIN_UPDATE_INTERVAL, ), MAX_UPDATE_INTERVAL, @@ -58,19 +64,28 @@ def set_update_interval(instances, requests_remaining): return interval -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Airly as config entry.""" - api_key = config_entry.data[CONF_API_KEY] - latitude = config_entry.data[CONF_LATITUDE] - longitude = config_entry.data[CONF_LONGITUDE] - use_nearest = config_entry.data.get(CONF_USE_NEAREST, False) + api_key = entry.data[CONF_API_KEY] + latitude = entry.data[CONF_LATITUDE] + longitude = entry.data[CONF_LONGITUDE] + use_nearest = entry.data.get(CONF_USE_NEAREST, False) # For backwards compat, set unique ID - if config_entry.unique_id is None: + if entry.unique_id is None: hass.config_entries.async_update_entry( - config_entry, unique_id=f"{latitude}-{longitude}" + entry, unique_id=f"{latitude}-{longitude}" ) + # identifiers in device_info should use Tuple[str, str, str] type, but latitude and + # longitude are float, so we convert old device entries to use correct types + device_registry = await async_get_registry(hass) + old_ids = (DOMAIN, latitude, longitude) + device_entry = device_registry.async_get_device({old_ids}) + if device_entry and entry.entry_id in device_entry.config_entries: + new_ids = (DOMAIN, str(latitude), str(longitude)) + device_registry.async_update_device(device_entry.id, new_identifiers={new_ids}) + websession = async_get_clientsession(hass) update_interval = timedelta(minutes=MIN_UPDATE_INTERVAL) @@ -81,21 +96,19 @@ async def async_setup_entry(hass, config_entry): await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = coordinator + hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -105,13 +118,13 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, - hass, - session, - api_key, - latitude, - longitude, - update_interval, - use_nearest, + hass: HomeAssistant, + session: ClientSession, + api_key: str, + latitude: float, + longitude: float, + update_interval: timedelta, + use_nearest: bool, ): """Initialize.""" self.latitude = latitude @@ -121,9 +134,9 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - async def _async_update_data(self): + async def _async_update_data(self) -> dict[str, str | float | int]: """Update data via library.""" - data = {} + data: dict[str, str | float | int] = {} if self.use_nearest: measurements = self.airly.create_measurements_session_nearest( self.latitude, self.longitude, max_distance_km=5 diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index f89a804ab3b..190bc326d0c 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -1,13 +1,22 @@ """Support for the Airly air_quality service.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components.air_quality import ( ATTR_AQI, ATTR_PM_2_5, ATTR_PM_10, AirQualityEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import AirlyDataUpdateCoordinator from .const import ( ATTR_API_ADVICE, ATTR_API_CAQI, @@ -36,80 +45,73 @@ LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" PARALLEL_UPDATES = 1 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Airly air_quality entity based on a config entry.""" - name = config_entry.data[CONF_NAME] + name = entry.data[CONF_NAME] - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities([AirlyAirQuality(coordinator, name)], False) -def round_state(func): - """Round state.""" - - def _decorator(self): - res = func(self) - if isinstance(res, float): - return round(res) - return res - - return _decorator - - class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): """Define an Airly air quality.""" - def __init__(self, coordinator, name): + coordinator: AirlyDataUpdateCoordinator + + def __init__(self, coordinator: AirlyDataUpdateCoordinator, name: str) -> None: """Initialize.""" super().__init__(coordinator) self._name = name self._icon = "mdi:blur" @property - def name(self): + def name(self) -> str: """Return the name.""" return self._name @property - def icon(self): + def icon(self) -> str: """Return the icon.""" return self._icon @property - @round_state - def air_quality_index(self): + def air_quality_index(self) -> float | None: """Return the air quality index.""" - return self.coordinator.data[ATTR_API_CAQI] + return round_state(self.coordinator.data[ATTR_API_CAQI]) @property - @round_state - def particulate_matter_2_5(self): + def particulate_matter_2_5(self) -> float | None: """Return the particulate matter 2.5 level.""" - return self.coordinator.data.get(ATTR_API_PM25) + return round_state(self.coordinator.data.get(ATTR_API_PM25)) @property - @round_state - def particulate_matter_10(self): + def particulate_matter_10(self) -> float | None: """Return the particulate matter 10 level.""" - return self.coordinator.data.get(ATTR_API_PM10) + return round_state(self.coordinator.data.get(ATTR_API_PM10)) @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return ATTRIBUTION @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" return f"{self.coordinator.latitude}-{self.coordinator.longitude}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "identifiers": { - (DOMAIN, self.coordinator.latitude, self.coordinator.longitude) + ( + DOMAIN, + str(self.coordinator.latitude), + str(self.coordinator.longitude), + ) }, "name": DEFAULT_NAME, "manufacturer": MANUFACTURER, @@ -117,7 +119,7 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): } @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" attrs = { LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION], @@ -135,3 +137,8 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): self.coordinator.data[ATTR_API_PM10_PERCENT] ) return attrs + + +def round_state(state: float | None) -> float | None: + """Round state.""" + return round(state) if state else state diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index e1b1743ae4e..598aa15b9b6 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -1,4 +1,9 @@ """Adds config flow for Airly.""" +from __future__ import annotations + +from typing import Any + +from aiohttp import ClientSession from airly import Airly from airly.exceptions import AirlyError import async_timeout @@ -13,6 +18,7 @@ from homeassistant.const import ( HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -24,7 +30,9 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} use_nearest = False @@ -84,7 +92,13 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -async def test_location(client, api_key, latitude, longitude, use_nearest=False): +async def test_location( + client: ClientSession, + api_key: str, + latitude: float, + longitude: float, + use_nearest: bool = False, +) -> bool: """Return true if location is valid.""" airly = Airly(api_key, client) if use_nearest: diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index df4818ef949..5136f54d6f2 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -1,29 +1,68 @@ """Constants for Airly integration.""" +from __future__ import annotations -ATTR_API_ADVICE = "ADVICE" -ATTR_API_CAQI = "CAQI" -ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION" -ATTR_API_CAQI_LEVEL = "LEVEL" -ATTR_API_HUMIDITY = "HUMIDITY" -ATTR_API_PM1 = "PM1" -ATTR_API_PM10 = "PM10" -ATTR_API_PM10_LIMIT = "PM10_LIMIT" -ATTR_API_PM10_PERCENT = "PM10_PERCENT" -ATTR_API_PM25 = "PM25" -ATTR_API_PM25_LIMIT = "PM25_LIMIT" -ATTR_API_PM25_PERCENT = "PM25_PERCENT" -ATTR_API_PRESSURE = "PRESSURE" -ATTR_API_TEMPERATURE = "TEMPERATURE" +from typing import Final -ATTR_LABEL = "label" -ATTR_UNIT = "unit" +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + PRESSURE_HPA, + TEMP_CELSIUS, +) -ATTRIBUTION = "Data provided by Airly" -CONF_USE_NEAREST = "use_nearest" -DEFAULT_NAME = "Airly" -DOMAIN = "airly" -LABEL_ADVICE = "advice" -MANUFACTURER = "Airly sp. z o.o." -MAX_UPDATE_INTERVAL = 90 -MIN_UPDATE_INTERVAL = 5 -NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." +from .model import SensorDescription + +ATTR_API_ADVICE: Final = "ADVICE" +ATTR_API_CAQI: Final = "CAQI" +ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION" +ATTR_API_CAQI_LEVEL: Final = "LEVEL" +ATTR_API_HUMIDITY: Final = "HUMIDITY" +ATTR_API_PM1: Final = "PM1" +ATTR_API_PM10: Final = "PM10" +ATTR_API_PM10_LIMIT: Final = "PM10_LIMIT" +ATTR_API_PM10_PERCENT: Final = "PM10_PERCENT" +ATTR_API_PM25: Final = "PM25" +ATTR_API_PM25_LIMIT: Final = "PM25_LIMIT" +ATTR_API_PM25_PERCENT: Final = "PM25_PERCENT" +ATTR_API_PRESSURE: Final = "PRESSURE" +ATTR_API_TEMPERATURE: Final = "TEMPERATURE" + +ATTRIBUTION: Final = "Data provided by Airly" +CONF_USE_NEAREST: Final = "use_nearest" +DEFAULT_NAME: Final = "Airly" +DOMAIN: Final = "airly" +LABEL_ADVICE: Final = "advice" +MANUFACTURER: Final = "Airly sp. z o.o." +MAX_UPDATE_INTERVAL: Final = 90 +MIN_UPDATE_INTERVAL: Final = 5 +NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." + +SENSOR_TYPES: dict[str, SensorDescription] = { + ATTR_API_PM1: { + "device_class": None, + "icon": "mdi:blur", + "label": ATTR_API_PM1, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + ATTR_API_HUMIDITY: { + "device_class": DEVICE_CLASS_HUMIDITY, + "icon": None, + "label": ATTR_API_HUMIDITY.capitalize(), + "unit": PERCENTAGE, + }, + ATTR_API_PRESSURE: { + "device_class": DEVICE_CLASS_PRESSURE, + "icon": None, + "label": ATTR_API_PRESSURE.capitalize(), + "unit": PRESSURE_HPA, + }, + ATTR_API_TEMPERATURE: { + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": ATTR_API_TEMPERATURE.capitalize(), + "unit": TEMP_CELSIUS, + }, +} diff --git a/homeassistant/components/airly/model.py b/homeassistant/components/airly/model.py new file mode 100644 index 00000000000..42091d449e3 --- /dev/null +++ b/homeassistant/components/airly/model.py @@ -0,0 +1,13 @@ +"""Type definitions for Airly integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class SensorDescription(TypedDict): + """Sensor description class.""" + + device_class: str | None + icon: str | None + label: str + unit: str diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 2a52050db15..a3d9a2981d2 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -1,68 +1,38 @@ """Support for the Airly sensor service.""" +from __future__ import annotations + +from typing import Any, cast + from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, - ATTR_ICON, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONF_NAME, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - PRESSURE_HPA, - TEMP_CELSIUS, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import AirlyDataUpdateCoordinator from .const import ( - ATTR_API_HUMIDITY, ATTR_API_PM1, ATTR_API_PRESSURE, - ATTR_API_TEMPERATURE, - ATTR_LABEL, - ATTR_UNIT, ATTRIBUTION, DEFAULT_NAME, DOMAIN, MANUFACTURER, + SENSOR_TYPES, ) PARALLEL_UPDATES = 1 -SENSOR_TYPES = { - ATTR_API_PM1: { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:blur", - ATTR_LABEL: ATTR_API_PM1, - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - ATTR_API_HUMIDITY: { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_ICON: None, - ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(), - ATTR_UNIT: PERCENTAGE, - }, - ATTR_API_PRESSURE: { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - ATTR_ICON: None, - ATTR_LABEL: ATTR_API_PRESSURE.capitalize(), - ATTR_UNIT: PRESSURE_HPA, - }, - ATTR_API_TEMPERATURE: { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(), - ATTR_UNIT: TEMP_CELSIUS, - }, -} - -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Airly sensor entities based on a config entry.""" - name = config_entry.data[CONF_NAME] + name = entry.data[CONF_NAME] - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = hass.data[DOMAIN][entry.entry_id] sensors = [] for sensor in SENSOR_TYPES: @@ -76,59 +46,63 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AirlySensor(CoordinatorEntity, SensorEntity): """Define an Airly sensor.""" - def __init__(self, coordinator, name, kind): + coordinator: AirlyDataUpdateCoordinator + + def __init__( + self, coordinator: AirlyDataUpdateCoordinator, name: str, kind: str + ) -> None: """Initialize.""" super().__init__(coordinator) self._name = name + self._description = SENSOR_TYPES[kind] self.kind = kind - self._device_class = None self._state = None - self._icon = None self._unit_of_measurement = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} @property - def name(self): + def name(self) -> str: """Return the name.""" - return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + return f"{self._name} {self._description['label']}" @property - def state(self): + def state(self) -> StateType: """Return the state.""" self._state = self.coordinator.data[self.kind] if self.kind in [ATTR_API_PM1, ATTR_API_PRESSURE]: - self._state = round(self._state) - if self.kind in [ATTR_API_TEMPERATURE, ATTR_API_HUMIDITY]: - self._state = round(self._state, 1) - return self._state + return round(cast(float, self._state)) + return round(cast(float, self._state), 1) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return self._attrs @property - def icon(self): + def icon(self) -> str | None: """Return the icon.""" - self._icon = SENSOR_TYPES[self.kind][ATTR_ICON] - return self._icon + return self._description["icon"] @property - def device_class(self): + def device_class(self) -> str | None: """Return the device_class.""" - return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + return self._description["device_class"] @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "identifiers": { - (DOMAIN, self.coordinator.latitude, self.coordinator.longitude) + ( + DOMAIN, + str(self.coordinator.latitude), + str(self.coordinator.longitude), + ) }, "name": DEFAULT_NAME, "manufacturer": MANUFACTURER, @@ -136,6 +110,6 @@ class AirlySensor(CoordinatorEntity, SensorEntity): } @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" - return SENSOR_TYPES[self.kind][ATTR_UNIT] + return self._description["unit"] diff --git a/homeassistant/components/airly/system_health.py b/homeassistant/components/airly/system_health.py index 3f2ed8e8d65..b1f6bc36c91 100644 --- a/homeassistant/components/airly/system_health.py +++ b/homeassistant/components/airly/system_health.py @@ -1,4 +1,8 @@ """Provide info to system health.""" +from __future__ import annotations + +from typing import Any + from airly import Airly from homeassistant.components import system_health @@ -15,7 +19,7 @@ def async_register( register.async_register_info(system_health_info) -async def system_health_info(hass): +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" requests_remaining = list(hass.data[DOMAIN].values())[0].airly.requests_remaining requests_per_day = list(hass.data[DOMAIN].values())[0].airly.requests_per_day diff --git a/mypy.ini b/mypy.ini index 5b757c9eb1d..7637ffc4d6a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -48,6 +48,19 @@ warn_return_any = true warn_unreachable = true warn_unused_ignores = true +[mypy-homeassistant.components.airly.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + [mypy-homeassistant.components.automation.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -652,9 +665,6 @@ ignore_errors = true [mypy-homeassistant.components.aemet.*] ignore_errors = true -[mypy-homeassistant.components.airly.*] -ignore_errors = true - [mypy-homeassistant.components.alarmdecoder.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a1761f2847f..f7472d791f3 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,7 +16,6 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.adguard.*", "homeassistant.components.aemet.*", - "homeassistant.components.airly.*", "homeassistant.components.alarmdecoder.*", "homeassistant.components.alexa.*", "homeassistant.components.almond.*", diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index c2785d6f3e7..5dcd465774d 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -13,7 +13,12 @@ from homeassistant.util.dt import utcnow from . import API_POINT_URL -from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + mock_device_registry, +) from tests.components.airly import init_integration @@ -181,3 +186,34 @@ async def test_unload_entry(hass, aioclient_mock): assert entry.state == ENTRY_STATE_NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_migrate_device_entry(hass, aioclient_mock): + """Test device_info identifiers migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id="123-456", + data={ + "api_key": "foo", + "latitude": 123, + "longitude": 456, + "name": "Home", + }, + ) + + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + config_entry.add_to_hass(hass) + + device_reg = mock_device_registry(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, 123, 456)} + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + migrated_device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "123", "456")} + ) + assert device_entry.id == migrated_device_entry.id From 80b05c39cc5d8645a8686fec9777c2d330b2f4d6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 May 2021 17:08:46 +0200 Subject: [PATCH 232/852] Fix light turn_on color conversion (#50251) --- homeassistant/components/light/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 97aa2468145..0bc68702467 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -343,7 +343,7 @@ async def async_setup(hass, config): # noqa: C901 rgb_color = params.pop(ATTR_RGB_COLOR) if COLOR_MODE_RGBW in supported_color_modes: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) - if COLOR_MODE_RGBWW in supported_color_modes: + elif COLOR_MODE_RGBWW in supported_color_modes: params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( *rgb_color, light.min_mireds, light.max_mireds ) From 3a36a976ee8a95047e3098dc09540a7106a19fb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 May 2021 13:07:06 -0500 Subject: [PATCH 233/852] Small cleanups to rainmachine get_client_controller (#50250) --- homeassistant/components/rainmachine/config_flow.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 8ef21fb185e..9febb2b3a92 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -22,9 +22,8 @@ DATA_SCHEMA = vol.Schema( def get_client_controller(client): - """Enumerate controllers to find the first mac.""" - for controller in client.controllers.values(): - return controller + """Return the first local controller.""" + return next(iter(client.controllers.values())) async def async_get_controller(hass, ip_address, password, port, ssl): From 934d241b70ac30a62a5d38b73954ac74635b0aec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 May 2021 21:59:51 +0200 Subject: [PATCH 234/852] Improve Google Cast options flow (#50028) --- homeassistant/components/cast/config_flow.py | 74 ++++++++++++------- homeassistant/components/cast/strings.json | 23 ++++-- .../components/cast/translations/en.json | 24 +++--- tests/components/cast/test_config_flow.py | 56 ++++++++++---- 4 files changed, 121 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index 84420fabd05..bb316f2b511 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -107,53 +107,77 @@ class CastOptionsFlowHandler(config_entries.OptionsFlow): """Handle Google Cast options.""" def __init__(self, config_entry): - """Initialize MQTT options flow.""" + """Initialize Google Cast options flow.""" self.config_entry = config_entry - self.broker_config = {} - self.options = dict(config_entry.options) + self.updated_config = {} async def async_step_init(self, user_input=None): - """Manage the Cast options.""" - return await self.async_step_options() + """Manage the Google Cast options.""" + return await self.async_step_basic_options() - async def async_step_options(self, user_input=None): - """Manage the MQTT options.""" + async def async_step_basic_options(self, user_input=None): + """Manage the Google Cast options.""" errors = {} current_config = self.config_entry.data if user_input is not None: - bad_cec, ignore_cec = _string_to_list( - user_input.get(CONF_IGNORE_CEC, ""), IGNORE_CEC_SCHEMA - ) bad_hosts, known_hosts = _string_to_list( user_input.get(CONF_KNOWN_HOSTS, ""), KNOWN_HOSTS_SCHEMA ) - bad_uuid, wanted_uuid = _string_to_list( - user_input.get(CONF_UUID, ""), WANTED_UUID_SCHEMA - ) - if not bad_cec and not bad_hosts and not bad_uuid: - updated_config = dict(current_config) - updated_config[CONF_IGNORE_CEC] = ignore_cec - updated_config[CONF_KNOWN_HOSTS] = known_hosts - updated_config[CONF_UUID] = wanted_uuid + if not bad_hosts: + self.updated_config = dict(current_config) + self.updated_config[CONF_KNOWN_HOSTS] = known_hosts + + if self.show_advanced_options: + return await self.async_step_advanced_options() + self.hass.config_entries.async_update_entry( - self.config_entry, data=updated_config + self.config_entry, data=self.updated_config ) return self.async_create_entry(title="", data=None) fields = {} suggested_value = _list_to_string(current_config.get(CONF_KNOWN_HOSTS)) _add_with_suggestion(fields, CONF_KNOWN_HOSTS, suggested_value) - if self.show_advanced_options: - suggested_value = _list_to_string(current_config.get(CONF_UUID)) - _add_with_suggestion(fields, CONF_UUID, suggested_value) - suggested_value = _list_to_string(current_config.get(CONF_IGNORE_CEC)) - _add_with_suggestion(fields, CONF_IGNORE_CEC, suggested_value) return self.async_show_form( - step_id="options", + step_id="basic_options", data_schema=vol.Schema(fields), errors=errors, + last_step=not self.show_advanced_options, + ) + + async def async_step_advanced_options(self, user_input=None): + """Manage the Google Cast options.""" + errors = {} + if user_input is not None: + bad_cec, ignore_cec = _string_to_list( + user_input.get(CONF_IGNORE_CEC, ""), IGNORE_CEC_SCHEMA + ) + bad_uuid, wanted_uuid = _string_to_list( + user_input.get(CONF_UUID, ""), WANTED_UUID_SCHEMA + ) + + if not bad_cec and not bad_uuid: + self.updated_config[CONF_IGNORE_CEC] = ignore_cec + self.updated_config[CONF_UUID] = wanted_uuid + self.hass.config_entries.async_update_entry( + self.config_entry, data=self.updated_config + ) + return self.async_create_entry(title="", data=None) + + fields = {} + current_config = self.config_entry.data + suggested_value = _list_to_string(current_config.get(CONF_UUID)) + _add_with_suggestion(fields, CONF_UUID, suggested_value) + suggested_value = _list_to_string(current_config.get(CONF_IGNORE_CEC)) + _add_with_suggestion(fields, CONF_IGNORE_CEC, suggested_value) + + return self.async_show_form( + step_id="advanced_options", + data_schema=vol.Schema(fields), + errors=errors, + last_step=True, ) diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json index 33ce4b6941e..02bfeccf794 100644 --- a/homeassistant/components/cast/strings.json +++ b/homeassistant/components/cast/strings.json @@ -5,10 +5,10 @@ "description": "[%key:common::config_flow::description::confirm_setup%]" }, "config": { - "title": "Google Cast", - "description": "Please enter the Google Cast configuration.", + "title": "Google Cast configuration", + "description": "Known Hosts - A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.", "data": { - "known_hosts": "Optional list of known hosts if mDNS discovery is not working." + "known_hosts": "Known hosts" } } }, @@ -21,12 +21,19 @@ }, "options": { "step": { - "options": { - "description": "Please enter the Google Cast configuration.", + "basic_options": { + "title": "Google Cast configuration", + "description": "Known Hosts - A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.", "data": { - "ignore_cec": "Optional list which will be passed to pychromecast.IGNORE_CEC.", - "known_hosts": "Optional list of known hosts if mDNS discovery is not working.", - "uuid": "Optional list of UUIDs. Casts not listed will not be added." + "known_hosts": "Known hosts" + } + }, + "advanced_options": { + "title": "Advanced Google Cast configuration", + "description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don’t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be will be passed to pychromecast.IGNORE_CEC.", + "data": { + "ignore_cec": "Ignore CEC", + "uuid": "Allowed UUIDs" } } }, diff --git a/homeassistant/components/cast/translations/en.json b/homeassistant/components/cast/translations/en.json index c2c2460cc9c..a1aceb5f029 100644 --- a/homeassistant/components/cast/translations/en.json +++ b/homeassistant/components/cast/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { @@ -10,10 +9,10 @@ "step": { "config": { "data": { - "known_hosts": "Optional list of known hosts if mDNS discovery is not working." + "known_hosts": "Known hosts" }, - "description": "Please enter the Google Cast configuration.", - "title": "Google Cast" + "description": "### Known Hosts \n A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.", + "title": "Google Cast configuration" }, "confirm": { "description": "Do you want to start set up?" @@ -25,13 +24,20 @@ "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." }, "step": { - "options": { + "advanced_options": { "data": { - "ignore_cec": "Optional list which will be passed to pychromecast.IGNORE_CEC.", - "known_hosts": "Optional list of known hosts if mDNS discovery is not working.", - "uuid": "Optional list of UUIDs. Casts not listed will not be added." + "ignore_cec": "Ignore CEC", + "uuid": "Allowed UUIDs" }, - "description": "Please enter the Google Cast configuration." + "description": "### Allowed UUIDs\n A comma-separated list of UUIDs of Cast devices to add to Home Assistant. **Use only if you don\u2019t want to add all available cast devices.** \n ### Ignore CEC \n A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be will be passed to pychromecast.IGNORE_CEC. [See the upstream documentation for more information](https://github.com/balloob/pychromecast#ignoring-cec-data).", + "title": "Advanced Google Cast configuration" + }, + "basic_options": { + "data": { + "known_hosts": "Known hosts" + }, + "description": "### Known Hosts \n A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.", + "title": "Google Cast configuration" } } } diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index cc67d585022..7c3fb774722 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -153,7 +153,8 @@ def get_suggested(schema, key): ) async def test_option_flow(hass, parameter_data): """Test config flow options.""" - all_parameters = ["ignore_cec", "known_hosts", "uuid"] + basic_parameters = ["known_hosts"] + advanced_parameters = ["ignore_cec", "uuid"] parameter, initial, suggested, user_input, updated = parameter_data data = { @@ -170,32 +171,61 @@ async def test_option_flow(hass, parameter_data): # Test ignore_cec and uuid options are hidden if advanced options are disabled result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "options" + assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema assert set(data_schema) == {"known_hosts"} orig_data = dict(config_entry.data) - # Reconfigure ignore_cec, known_hosts, uuid + # Reconfigure known_hosts context = {"source": config_entries.SOURCE_USER, "show_advanced_options": True} result = await hass.config_entries.options.async_init( config_entry.entry_id, context=context ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "options" + assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema - for other_param in all_parameters: + for other_param in basic_parameters: if other_param == parameter: continue assert get_suggested(data_schema, other_param) == "" - assert get_suggested(data_schema, parameter) == suggested + if parameter in basic_parameters: + assert get_suggested(data_schema, parameter) == suggested + user_input_dict = {} + if parameter in basic_parameters: + user_input_dict[parameter] = user_input result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={parameter: user_input}, + user_input=user_input_dict, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "advanced_options" + for other_param in basic_parameters: + if other_param == parameter: + continue + assert config_entry.data[other_param] == [] + # No update yet + assert config_entry.data[parameter] == initial + + # Reconfigure ignore_cec, uuid + data_schema = result["data_schema"].schema + for other_param in advanced_parameters: + if other_param == parameter: + continue + assert get_suggested(data_schema, other_param) == "" + if parameter in advanced_parameters: + assert get_suggested(data_schema, parameter) == suggested + + user_input_dict = {} + if parameter in advanced_parameters: + user_input_dict[parameter] = user_input + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=user_input_dict, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] is None - for other_param in all_parameters: + for other_param in advanced_parameters: if other_param == parameter: continue assert config_entry.data[other_param] == [] @@ -209,12 +239,10 @@ async def test_option_flow(hass, parameter_data): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] is None - assert config_entry.data == { - **orig_data, - "ignore_cec": [], - "known_hosts": [], - "uuid": [], - } + expected_data = {**orig_data, "known_hosts": []} + if parameter in advanced_parameters: + expected_data[parameter] = updated + assert dict(config_entry.data) == expected_data async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock): From 552ca1c57b60da75f34b29402daee9b726b73eca Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 7 May 2021 22:12:13 +0200 Subject: [PATCH 235/852] Fix modbus switch problems (#50117) --- homeassistant/components/modbus/__init__.py | 28 +++++++++++---------- homeassistant/components/modbus/switch.py | 11 +++++--- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 4ccf8d607ca..2ebab4635b3 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -183,19 +183,21 @@ SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( ), vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int, vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int, - vol.Optional(CONF_VERIFY): { - vol.Optional(CONF_ADDRESS): cv.positive_int, - vol.Optional(CONF_INPUT_TYPE): vol.In( - [ - CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_DISCRETE, - CALL_TYPE_REGISTER_INPUT, - CALL_TYPE_COIL, - ] - ), - vol.Optional(CONF_STATE_OFF): cv.positive_int, - vol.Optional(CONF_STATE_ON): cv.positive_int, - }, + vol.Optional(CONF_VERIFY): vol.Maybe( + { + vol.Optional(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_INPUT_TYPE): vol.In( + [ + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_COIL, + ] + ), + vol.Optional(CONF_STATE_OFF): cv.positive_int, + vol.Optional(CONF_STATE_ON): cv.positive_int, + } + ), } ) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 6fff8c1d373..c449c25bb22 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -70,6 +70,8 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): self._command_on = config[CONF_COMMAND_ON] self._command_off = config[CONF_COMMAND_OFF] if CONF_VERIFY in config: + if config[CONF_VERIFY] is None: + config[CONF_VERIFY] = {} self._verify_active = True self._verify_address = config[CONF_VERIFY].get( CONF_ADDRESS, config[CONF_ADDRESS] @@ -97,10 +99,9 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): if state: self._is_on = state.state == STATE_ON - if self._verify_active: - async_track_time_interval( - self.hass, lambda arg: self.update(), self._scan_interval - ) + async_track_time_interval( + self.hass, lambda arg: self.update(), self._scan_interval + ) @property def is_on(self): @@ -154,6 +155,8 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): def update(self): """Update the entity state.""" if not self._verify_active: + self._available = True + self.schedule_update_ha_state() return result = self._read_func(self._slave, self._verify_address, 1) From cf96d86985d9771876fa7aed9a7a72356131ce04 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 7 May 2021 22:23:29 +0200 Subject: [PATCH 236/852] Add yaml key to Shelly to allow CoAP port customization (#48729) Co-authored-by: Paulus Schoutsen --- homeassistant/components/shelly/__init__.py | 16 ++++++++++++++++ homeassistant/components/shelly/const.py | 3 +++ homeassistant/components/shelly/utils.py | 10 +++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 29eb07b3a90..2da2c5a6ea5 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -5,6 +5,7 @@ import logging import aioshelly import async_timeout +import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -17,6 +18,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator +import homeassistant.helpers.config_validation as cv from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, @@ -25,7 +27,9 @@ from .const import ( ATTR_DEVICE, BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, COAP, + CONF_COAP_PORT, DATA_CONFIG_ENTRY, + DEFAULT_COAP_PORT, DEVICE, DOMAIN, EVENT_SHELLY_CLICK, @@ -43,10 +47,22 @@ PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"] SLEEPING_PLATFORMS = ["binary_sensor", "sensor"] _LOGGER = logging.getLogger(__name__) +COAP_SCHEMA = vol.Schema( + { + vol.Optional(CONF_COAP_PORT, default=DEFAULT_COAP_PORT): cv.port, + } +) +CONFIG_SCHEMA = vol.Schema({DOMAIN: COAP_SCHEMA}, extra=vol.ALLOW_EXTRA) + async def async_setup(hass: HomeAssistant, config: dict): """Set up the Shelly component.""" hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} + + conf = config.get(DOMAIN) + if conf is not None: + hass.data[DOMAIN][CONF_COAP_PORT] = conf[CONF_COAP_PORT] + return True diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 2609b7cd57f..bff82057120 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -6,6 +6,9 @@ DEVICE = "device" DOMAIN = "shelly" REST = "rest" +CONF_COAP_PORT = "coap_port" +DEFAULT_COAP_PORT = 5683 + # Used in "_async_update_data" as timeout for polling data from devices. POLLING_TIMEOUT_SEC = 18 diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 39134957fb9..7fabdb9bf8b 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -14,7 +14,9 @@ from homeassistant.util.dt import parse_datetime, utcnow from .const import ( BASIC_INPUTS_EVENTS_TYPES, COAP, + CONF_COAP_PORT, DATA_CONFIG_ENTRY, + DEFAULT_COAP_PORT, DOMAIN, SHBTN_INPUTS_EVENTS_TYPES, SHBTN_MODELS, @@ -190,7 +192,13 @@ def get_device_wrapper(hass: HomeAssistant, device_id: str): async def get_coap_context(hass): """Get CoAP context to be used in all Shelly devices.""" context = aioshelly.COAP() - await context.initialize() + port = DEFAULT_COAP_PORT + if DOMAIN in hass.data: + port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT) + else: + port = DEFAULT_COAP_PORT + _LOGGER.info("Starting CoAP context with UDP port %s", port) + await context.initialize(port) @callback def shutdown_listener(ev): From ba284c0d27ece618ca75888fb47790c8a786518e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 May 2021 23:04:00 +0200 Subject: [PATCH 237/852] Add sensor state_class property (#50063) * Add sensor state_class property * STATE_CLASS_LATEST -> STATE_CLASS_MEASUREMENT * Export sensor.state_class in capability_attributes * Add STATE_CLASS_UNKNOWN * Fix typing * Update tests * STATE_CLASS_UNKNOWN -> STATE_CLASS_OTHER * Update homeassistant/components/sensor/__init__.py Co-authored-by: Paulus Schoutsen * Remove STATE_CLASS_OTHER * Update tests * Revert test changes Co-authored-by: Paulus Schoutsen --- homeassistant/components/hue/sensor_base.py | 6 +++ homeassistant/components/sensor/__init__.py | 42 +++++++++++++++++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 9f764e04d28..b8e2af138b2 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -6,6 +6,7 @@ from aiohue import AiohueException, Unauthorized from aiohue.sensors import TYPE_ZLL_PRESENCE import async_timeout +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.core import callback from homeassistant.helpers import debounce, entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -177,6 +178,11 @@ class GenericHueSensor(GenericHueDevice, entity.Entity): or self.sensor.config.get("reachable", True) ) + @property + def state_class(self): + """Return the state class of this entity, from STATE_CLASSES, if any.""" + return STATE_CLASS_MEASUREMENT + async def async_added_to_hass(self): """When entity is added to hass.""" self.async_on_remove( diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 0012b1a3aa2..ed88ca55ceb 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -1,10 +1,14 @@ """Component to interface with various sensors that can be monitored.""" +from __future__ import annotations +from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any, cast import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_CO, @@ -21,17 +25,19 @@ from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent - -# mypy: allow-untyped-defs, no-check-untyped-defs +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) +ATTR_STATE_CLASS = "state_class" + DOMAIN = "sensor" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -56,8 +62,15 @@ DEVICE_CLASSES = [ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) +# The state represents a measurement in present time +STATE_CLASS_MEASUREMENT = "measurement" -async def async_setup(hass, config): +STATE_CLASSES = [STATE_CLASS_MEASUREMENT] + +STATE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(STATE_CLASSES)) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for sensors.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -67,15 +80,30 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component = cast(EntityComponent, hass.data[DOMAIN]) + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component = cast(EntityComponent, hass.data[DOMAIN]) + return await component.async_unload_entry(entry) class SensorEntity(Entity): """Base class for sensor entities.""" + + @property + def state_class(self) -> str | None: + """Return the state class of this entity, from STATE_CLASSES, if any.""" + return None + + @property + def capability_attributes(self) -> Mapping[str, Any] | None: + """Return the capability attributes.""" + if self.state_class: + return {ATTR_STATE_CLASS: self.state_class} + + return None From 57d334213775f7ecb69d9afaf02473521d5a9dbc Mon Sep 17 00:00:00 2001 From: Pawel Date: Fri, 7 May 2021 23:05:59 +0200 Subject: [PATCH 238/852] Fix Epson config flow unique_id (#45434) * switch Epson from HTTP to TCP communication * fix tests * add asyncio websession * fix manifest * fix config flow * fix manifest * fix logger warnings * switch Epson from HTTP to TCP communication * fix tests * add asyncio websession * fix config flow * fix manifest * fix logger warnings * add already configured to import yaml * remove neccessary projector on on config.yaml * remove check import None * reload integration if no unique_id * async_migrate_entry * add async_migrate_entry * add init tests * media player migration uid * unifi config flow * Update homeassistant/components/epson/media_player.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/epson/media_player.py Co-authored-by: Martin Hjelmare * Apply suggestions from code review Co-authored-by: Martin Hjelmare * review * remove unnecessary try except * add import test * Apply suggestions from code review Co-authored-by: Martin Hjelmare * revert PORT option from config yaml * fix tests * remove port from config flow * fix CONFIG_SCHEMA Co-authored-by: Martin Hjelmare --- homeassistant/components/epson/__init__.py | 43 ++++--- homeassistant/components/epson/config_flow.py | 45 +++++++- homeassistant/components/epson/const.py | 2 +- homeassistant/components/epson/exceptions.py | 4 + homeassistant/components/epson/manifest.json | 2 +- .../components/epson/media_player.py | 52 ++++++++- homeassistant/components/epson/strings.json | 6 +- .../components/epson/translations/en.json | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/epson/test_config_flow.py | 105 +++++++++++++----- 11 files changed, 205 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index b560151e058..1982731b9ef 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -2,44 +2,53 @@ import logging from epson_projector import Projector -from epson_projector.const import POWER, STATE_UNAVAILABLE as EPSON_STATE_UNAVAILABLE +from epson_projector.const import ( + PWR_OFF_STATE, + STATE_UNAVAILABLE as EPSON_STATE_UNAVAILABLE, +) from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_PLATFORM from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN -from .exceptions import CannotConnect +from .const import DOMAIN, HTTP +from .exceptions import CannotConnect, PoweredOff PLATFORMS = [MEDIA_PLAYER_PLATFORM] _LOGGER = logging.getLogger(__name__) -async def validate_projector(hass: HomeAssistant, host, port): - """Validate the given host and port allows us to connect.""" +async def validate_projector( + hass: HomeAssistant, host, check_power=True, check_powered_on=True +): + """Validate the given projector host allows us to connect.""" epson_proj = Projector( host=host, websession=async_get_clientsession(hass, verify_ssl=False), - port=port, + type=HTTP, ) - _power = await epson_proj.get_property(POWER) - if not _power or _power == EPSON_STATE_UNAVAILABLE: - raise CannotConnect + if check_power: + _power = await epson_proj.get_power() + if not _power or _power == EPSON_STATE_UNAVAILABLE: + _LOGGER.debug("Cannot connect to projector") + raise CannotConnect + if _power == PWR_OFF_STATE and check_powered_on: + _LOGGER.debug("Projector is off") + raise PoweredOff return epson_proj async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up epson from a config entry.""" - try: - projector = await validate_projector( - hass, entry.data[CONF_HOST], entry.data[CONF_PORT] - ) - except CannotConnect: - _LOGGER.warning("Cannot connect to projector %s", entry.data[CONF_HOST]) - return False + projector = await validate_projector( + hass=hass, + host=entry.data[CONF_HOST], + check_power=False, + check_powered_on=False, + ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = projector hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/epson/config_flow.py b/homeassistant/components/epson/config_flow.py index 4cade68f24c..5203cdbe9e0 100644 --- a/homeassistant/components/epson/config_flow.py +++ b/homeassistant/components/epson/config_flow.py @@ -1,4 +1,6 @@ """Config flow for epson integration.""" +import logging + import voluptuous as vol from homeassistant import config_entries @@ -6,16 +8,17 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from . import validate_projector from .const import DOMAIN -from .exceptions import CannotConnect +from .exceptions import CannotConnect, PoweredOff DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, vol.Required(CONF_NAME, default=DOMAIN): str, - vol.Required(CONF_PORT, default=80): int, } ) +_LOGGER = logging.getLogger(__name__) + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for epson.""" @@ -24,19 +27,51 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_config): """Import a config entry from configuration.yaml.""" - return await self.async_step_user(import_config) + for entry in self._async_current_entries(include_ignore=True): + if import_config[CONF_HOST] == entry.data[CONF_HOST]: + return self.async_abort(reason="already_configured") + try: + projector = await validate_projector( + hass=self.hass, + host=import_config[CONF_HOST], + check_power=True, + check_powered_on=False, + ) + except CannotConnect: + _LOGGER.warning("Cannot connect to projector") + return self.async_abort(reason="cannot_connect") + + serial_no = await projector.get_serial_number() + await self.async_set_unique_id(serial_no) + self._abort_if_unique_id_configured() + import_config.pop(CONF_PORT, None) + return self.async_create_entry( + title=import_config.pop(CONF_NAME), data=import_config + ) async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: try: - await validate_projector( - self.hass, user_input[CONF_HOST], user_input[CONF_PORT] + projector = await validate_projector( + hass=self.hass, + host=user_input[CONF_HOST], + check_power=True, + check_powered_on=True, ) except CannotConnect: errors["base"] = "cannot_connect" + except PoweredOff: + _LOGGER.warning( + "You need to turn ON projector for initial configuration" + ) + errors["base"] = "powered_off" else: + serial_no = await projector.get_serial_number() + await self.async_set_unique_id(serial_no) + self._abort_if_unique_id_configured() + user_input.pop(CONF_PORT, None) return self.async_create_entry( title=user_input.pop(CONF_NAME), data=user_input ) diff --git a/homeassistant/components/epson/const.py b/homeassistant/components/epson/const.py index cb227047f45..9b1ad0a8f5f 100644 --- a/homeassistant/components/epson/const.py +++ b/homeassistant/components/epson/const.py @@ -4,5 +4,5 @@ DOMAIN = "epson" SERVICE_SELECT_CMODE = "select_cmode" ATTR_CMODE = "cmode" - DEFAULT_NAME = "EPSON Projector" +HTTP = "http" diff --git a/homeassistant/components/epson/exceptions.py b/homeassistant/components/epson/exceptions.py index d781a74f7c1..5cc65b32891 100644 --- a/homeassistant/components/epson/exceptions.py +++ b/homeassistant/components/epson/exceptions.py @@ -4,3 +4,7 @@ from homeassistant import exceptions class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" + + +class PoweredOff(exceptions.HomeAssistantError): + """Error to indicate projector is off.""" diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index b02ef0dddd3..069956bdc9a 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -3,7 +3,7 @@ "name": "Epson", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/epson", - "requirements": ["epson-projector==0.2.3"], + "requirements": ["epson-projector==0.4.2"], "codeowners": ["@pszafer"], "iot_class": "local_polling" } diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 0b6828b7747..9910826cc3d 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -40,6 +40,7 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from .const import ATTR_CMODE, DEFAULT_NAME, DOMAIN, SERVICE_SELECT_CMODE @@ -66,10 +67,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Epson projector from a config entry.""" - unique_id = config_entry.entry_id - projector = hass.data[DOMAIN][unique_id] + entry_id = config_entry.entry_id + unique_id = config_entry.unique_id + projector = hass.data[DOMAIN][entry_id] projector_entity = EpsonProjectorMediaPlayer( - projector, config_entry.title, unique_id + projector=projector, + name=config_entry.title, + unique_id=unique_id, + entry=config_entry, ) async_add_entities([projector_entity], True) platform = entity_platform.async_get_current_platform() @@ -92,10 +97,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class EpsonProjectorMediaPlayer(MediaPlayerEntity): """Representation of Epson Projector Device.""" - def __init__(self, projector, name, unique_id): + def __init__(self, projector, name, unique_id, entry): """Initialize entity to control Epson projector.""" - self._name = name self._projector = projector + self._entry = entry + self._name = name self._available = False self._cmode = None self._source_list = list(DEFAULT_SOURCES.values()) @@ -104,9 +110,28 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): self._state = None self._unique_id = unique_id + async def set_unique_id(self): + """Set unique id for projector config entry.""" + _LOGGER.debug("Setting unique_id for projector") + if self._unique_id: + return False + uid = await self._projector.get_serial_number() + if uid: + self.hass.config_entries.async_update_entry(self._entry, unique_id=uid) + registry = async_get_entity_registry(self.hass) + old_entity_id = registry.async_get_entity_id( + "media_player", DOMAIN, self._entry.entry_id + ) + if old_entity_id is not None: + registry.async_update_entity(old_entity_id, new_unique_id=uid) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._entry.entry_id) + ) + return True + async def async_update(self): """Update state of device.""" - power_state = await self._projector.get_property(POWER) + power_state = await self._projector.get_power() _LOGGER.debug("Projector status: %s", power_state) if not power_state or power_state == EPSON_STATE_UNAVAILABLE: self._available = False @@ -114,6 +139,8 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): self._available = True if power_state == EPSON_CODES[POWER]: self._state = STATE_ON + if await self.set_unique_id(): + return self._source_list = list(DEFAULT_SOURCES.values()) cmode = await self._projector.get_property(CMODE) self._cmode = CMODE_LIST.get(cmode, self._cmode) @@ -127,6 +154,19 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): else: self._state = STATE_OFF + @property + def device_info(self): + """Get attributes about the device.""" + if not self._unique_id: + return None + return { + "identifiers": {(DOMAIN, self._unique_id)}, + "manufacturer": "Epson", + "name": "Epson projector", + "model": "Epson", + "via_hub": (DOMAIN, self._unique_id), + } + @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/epson/strings.json b/homeassistant/components/epson/strings.json index 263d7984aae..41a5f175ee7 100644 --- a/homeassistant/components/epson/strings.json +++ b/homeassistant/components/epson/strings.json @@ -4,13 +4,13 @@ "user": { "data": { "host": "[%key:common::config_flow::data::host%]", - "name": "[%key:common::config_flow::data::name%]", - "port": "[%key:common::config_flow::data::port%]" + "name": "[%key:common::config_flow::data::name%]" } } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "powered_off": "Is projector turned on? You need to turn on projector for initial configuration." } } } diff --git a/homeassistant/components/epson/translations/en.json b/homeassistant/components/epson/translations/en.json index bb914282c44..931bbcf557e 100644 --- a/homeassistant/components/epson/translations/en.json +++ b/homeassistant/components/epson/translations/en.json @@ -1,14 +1,14 @@ { "config": { "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "powered_off": "Is projector turned on? You need to turn on projector for initial configuration." }, "step": { "user": { "data": { "host": "Host", - "name": "Name", - "port": "Port" + "name": "Name" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 559658659a9..30233c350d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -563,7 +563,7 @@ envoy_reader==0.18.4 ephem==3.7.7.0 # homeassistant.components.epson -epson-projector==0.2.3 +epson-projector==0.4.2 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01173646f32..fd694adc0bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,7 +309,7 @@ envoy_reader==0.18.4 ephem==3.7.7.0 # homeassistant.components.epson -epson-projector==0.2.3 +epson-projector==0.4.2 # homeassistant.components.faa_delays faadelays==0.0.7 diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py index 849a88ba112..3ff7753d3eb 100644 --- a/tests/components/epson/test_config_flow.py +++ b/tests/components/epson/test_config_flow.py @@ -1,35 +1,40 @@ """Test the epson config flow.""" from unittest.mock import patch +from epson_projector.const import PWR_OFF_STATE + from homeassistant import config_entries, setup from homeassistant.components.epson.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_UNAVAILABLE +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE + +from tests.common import MockConfigEntry 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} - ) + with patch("homeassistant.components.epson.Projector.get_power", return_value="01"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result["type"] == "form" assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER - with patch( - "homeassistant.components.epson.Projector.get_property", - return_value="04", + "homeassistant.components.epson.Projector.get_power", + return_value="01", ), patch( "homeassistant.components.epson.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80}, + {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) + assert result2["type"] == "create_entry" assert result2["title"] == "test-epson" - assert result2["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 80} + assert result2["data"] == {CONF_HOST: "1.1.1.1"} await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -41,21 +46,43 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.epson.Projector.get_property", + "homeassistant.components.epson.Projector.get_power", return_value=STATE_UNAVAILABLE, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80}, + {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} +async def test_form_powered_off(hass): + """Test we handle powered off during initial configuration.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.epson.Projector.get_power", + return_value=PWR_OFF_STATE, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "powered_off"} + + async def test_import(hass): """Test config.yaml import.""" with patch( + "homeassistant.components.epson.Projector.get_power", + return_value="01", + ), patch( "homeassistant.components.epson.Projector.get_property", return_value="04", ), patch( @@ -65,27 +92,53 @@ async def test_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80}, + data={CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) - assert result["type"] == "create_entry" - assert result["title"] == "test-epson" - assert result["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 80} + assert result["type"] == "create_entry" + assert result["title"] == "test-epson" + assert result["data"] == {CONF_HOST: "1.1.1.1"} + + +async def test_already_imported(hass): + """Test config.yaml imported twice.""" + MockConfigEntry( + domain=DOMAIN, + source=config_entries.SOURCE_IMPORT, + unique_id="bla", + title="test-epson", + data={CONF_HOST: "1.1.1.1"}, + ).add_to_hass(hass) + + with patch( + "homeassistant.components.epson.Projector.get_power", + return_value="01", + ), patch( + "homeassistant.components.epson.Projector.get_property", + return_value="04", + ), patch( + "homeassistant.components.epson.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" async def test_import_cannot_connect(hass): - """Test we handle cannot connect error with import.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - + """Test we handle cannot connect error.""" with patch( - "homeassistant.components.epson.Projector.get_property", + "homeassistant.components.epson.Projector.get_power", return_value=STATE_UNAVAILABLE, ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80}, + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" From f32e15da36842d09b6eebdd911720a45b6cf9b89 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 8 May 2021 00:03:19 +0000 Subject: [PATCH 239/852] [ci skip] Translation update --- .../components/cast/translations/ca.json | 20 ++++++++++-- .../components/cast/translations/en.json | 15 +++++++-- .../components/epson/translations/en.json | 3 +- .../components/mqtt/translations/no.json | 6 ++-- .../components/mqtt/translations/zh-Hant.json | 6 ++-- .../components/nam/translations/ca.json | 24 ++++++++++++++ .../components/nam/translations/en.json | 24 ++++++++++++++ .../components/nam/translations/et.json | 24 ++++++++++++++ .../components/nam/translations/ru.json | 24 ++++++++++++++ .../rainmachine/translations/no.json | 1 + .../rainmachine/translations/zh-Hant.json | 1 + .../system_bridge/translations/no.json | 32 +++++++++++++++++++ .../components/zwave_js/translations/pl.json | 2 +- 13 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/nam/translations/ca.json create mode 100644 homeassistant/components/nam/translations/en.json create mode 100644 homeassistant/components/nam/translations/et.json create mode 100644 homeassistant/components/nam/translations/ru.json create mode 100644 homeassistant/components/system_bridge/translations/no.json diff --git a/homeassistant/components/cast/translations/ca.json b/homeassistant/components/cast/translations/ca.json index 6a5d16aa6bb..aaef5803b5c 100644 --- a/homeassistant/components/cast/translations/ca.json +++ b/homeassistant/components/cast/translations/ca.json @@ -10,10 +10,10 @@ "step": { "config": { "data": { - "known_hosts": "Llista opcional d'amfitrions coneguts per si el descobriment mDNS deixa de funcionar." + "known_hosts": "Amfitrions coneguts" }, - "description": "Introdueix la configuraci\u00f3 de Google Cast.", - "title": "Google Cast" + "description": "Amfitrions coneguts - Llista, separada per comes, dels noms d'amfitri\u00f3 o adreces IP dels dispositius Cast. Utilitza-ho si el descobriment mDNS no funciona.", + "title": "Configuraci\u00f3 de Google Cast" }, "confirm": { "description": "Vols comen\u00e7ar la configuraci\u00f3?" @@ -25,6 +25,20 @@ "invalid_known_hosts": "Els amfitrions coneguts han de ser una llista d'amfitrions separats per comes." }, "step": { + "advanced_options": { + "data": { + "ignore_cec": "Ignora CEC", + "uuid": "UUID permesos" + }, + "title": "Configuraci\u00f3 avan\u00e7ada de Google Cast" + }, + "basic_options": { + "data": { + "known_hosts": "Amfitrions coneguts" + }, + "description": "Amfitrions coneguts - Llista, separada per comes, dels noms d'amfitri\u00f3 o adreces IP dels dispositius Cast. Utilitza-ho si el descobriment mDNS no funciona.", + "title": "Configuraci\u00f3 de Google Cast" + }, "options": { "data": { "ignore_cec": "Llista opcional que es passar\u00e0 a pychromecast.IGNORE_CEC.", diff --git a/homeassistant/components/cast/translations/en.json b/homeassistant/components/cast/translations/en.json index a1aceb5f029..b61c5eee99e 100644 --- a/homeassistant/components/cast/translations/en.json +++ b/homeassistant/components/cast/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { @@ -11,7 +12,7 @@ "data": { "known_hosts": "Known hosts" }, - "description": "### Known Hosts \n A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.", + "description": "Known Hosts - A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.", "title": "Google Cast configuration" }, "confirm": { @@ -29,15 +30,23 @@ "ignore_cec": "Ignore CEC", "uuid": "Allowed UUIDs" }, - "description": "### Allowed UUIDs\n A comma-separated list of UUIDs of Cast devices to add to Home Assistant. **Use only if you don\u2019t want to add all available cast devices.** \n ### Ignore CEC \n A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be will be passed to pychromecast.IGNORE_CEC. [See the upstream documentation for more information](https://github.com/balloob/pychromecast#ignoring-cec-data).", + "description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don\u2019t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be will be passed to pychromecast.IGNORE_CEC.", "title": "Advanced Google Cast configuration" }, "basic_options": { "data": { "known_hosts": "Known hosts" }, - "description": "### Known Hosts \n A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.", + "description": "Known Hosts - A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.", "title": "Google Cast configuration" + }, + "options": { + "data": { + "ignore_cec": "Optional list which will be passed to pychromecast.IGNORE_CEC.", + "known_hosts": "Optional list of known hosts if mDNS discovery is not working.", + "uuid": "Optional list of UUIDs. Casts not listed will not be added." + }, + "description": "Please enter the Google Cast configuration." } } } diff --git a/homeassistant/components/epson/translations/en.json b/homeassistant/components/epson/translations/en.json index 931bbcf557e..2c477f65de4 100644 --- a/homeassistant/components/epson/translations/en.json +++ b/homeassistant/components/epson/translations/en.json @@ -8,7 +8,8 @@ "user": { "data": { "host": "Host", - "name": "Name" + "name": "Name", + "port": "Port" } } } diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 44792075813..d12237deba1 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -62,7 +62,8 @@ "port": "Port", "username": "Brukernavn" }, - "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler." + "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.", + "title": "Megleralternativer" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Testament melding behold", "will_topic": "Testament melding emne" }, - "description": "Vennligst velg MQTT-alternativer." + "description": "Vennligst velg MQTT-alternativer.", + "title": "MQTT-alternativer" } } } diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index e24474ed7b6..a8dc6d4ce9e 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -62,7 +62,8 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002", + "title": "Broker \u9078\u9805" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Will \u8a0a\u606f Retain", "will_topic": "Will \u8a0a\u606f\u4e3b\u984c" }, - "description": "\u8acb\u9078\u64c7 MQTT \u9078\u9805\u3002" + "description": "Discovery - \u5047\u5982\u63a2\u7d22\uff08Discovery\uff09\u529f\u80fd\u958b\u555f\uff08\u5efa\u8b70\uff09\uff0cHome Assistant \u5c07\u6703\u81ea\u52d5\u767c\u73fe\u88dd\u7f6e\u8207\u5be6\u9ad4\u3001\u4e26\u767c\u5e03\u5176\u8a2d\u5b9a\u81f3 MQTT Broker\u3002\u5047\u5982\u63a2\u7d22\u95dc\u9589\u7684\u8a71\uff0c\u6240\u6709\u8a2d\u5b9a\u5fc5\u9808\u624b\u52d5\u9032\u884c\u3002\nBirth \u8a0a\u606f - Birth \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u9023\u7dda\u81f3 MQTT Broker \u6642\u50b3\u9001\u3002\nWill \u8a0a\u606f - Will \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u81ea Broker \u65b7\u7dda\u6642\u50b3\u9001\u3001\u540c\u6642\u5305\u542b\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u95dc\u6a5f\uff09\u53ca\u975e\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u7576\u6a5f\u6216\u65b7\u7dda\uff09\u72c0\u6cc1\u3002", + "title": "MQTT \u9078\u9805" } } } diff --git a/homeassistant/components/nam/translations/ca.json b/homeassistant/components/nam/translations/ca.json new file mode 100644 index 00000000000..bc4ca456f4e --- /dev/null +++ b/homeassistant/components/nam/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "device_unsupported": "El dispositiu no \u00e9s compatible." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Vols configurar Nettigo Air Monitor a {host}?" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Configura la integraci\u00f3 Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/en.json b/homeassistant/components/nam/translations/en.json new file mode 100644 index 00000000000..0ea0c7ae6c1 --- /dev/null +++ b/homeassistant/components/nam/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "device_unsupported": "The device is unsupported." + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Do you want to set up Nettigo Air Monitor at {host}?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Set up Nettigo Air Monitor integration." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/et.json b/homeassistant/components/nam/translations/et.json new file mode 100644 index 00000000000..e94cd3a46b6 --- /dev/null +++ b/homeassistant/components/nam/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "device_unsupported": "Seadet ei toetata." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Kas seadistada Nettigo Air Monitori asukohas {host}?" + }, + "user": { + "data": { + "host": "host" + }, + "description": "Seadista Nettigo Air Monitori sidumine." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/ru.json b/homeassistant/components/nam/translations/ru.json new file mode 100644 index 00000000000..d475081285b --- /dev/null +++ b/homeassistant/components/nam/translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "device_unsupported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "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 Nettigo Air Monitor ({host})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/no.json b/homeassistant/components/rainmachine/translations/no.json index 214b50404a6..7c106a400e9 100644 --- a/homeassistant/components/rainmachine/translations/no.json +++ b/homeassistant/components/rainmachine/translations/no.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Ugyldig godkjenning" }, + "flow_title": "RainMachine {ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/zh-Hant.json b/homeassistant/components/rainmachine/translations/zh-Hant.json index 2cb80edb39b..942fd9ebea6 100644 --- a/homeassistant/components/rainmachine/translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/translations/zh-Hant.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, + "flow_title": "RainMachine {ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/system_bridge/translations/no.json b/homeassistant/components/system_bridge/translations/no.json new file mode 100644 index 00000000000..bd46c0e1824 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "flow_title": "System Bridge: {name}", + "step": { + "authenticate": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Angi API-n\u00f8kkelen du angav i konfigurasjonen for {name} ." + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "host": "Vert", + "port": "Port" + }, + "description": "Vennligst skriv inn tilkoblingsdetaljene dine." + } + } + }, + "title": "System Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 2bfd994132b..9fbd66de4f2 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -48,7 +48,7 @@ "title": "Wybierz metod\u0119 po\u0142\u0105czenia" }, "start_addon": { - "title": "Dodatek Z-Wave JS uruchamia si\u0119." + "title": "Dodatek Z-Wave JS uruchamia si\u0119..." }, "user": { "data": { From a32dc56153855072d4150d4b07706b26e5039527 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 8 May 2021 04:05:09 +0100 Subject: [PATCH 240/852] Update ovoenergy to 1.1.12 (#50268) --- homeassistant/components/ovo_energy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 37950df84cc..ba559ffb41d 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -3,7 +3,7 @@ "name": "OVO Energy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ovo_energy", - "requirements": ["ovoenergy==1.1.11"], + "requirements": ["ovoenergy==1.1.12"], "codeowners": ["@timmo001"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 30233c350d9..ee6dd46df86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1088,7 +1088,7 @@ oru==0.1.11 orvibo==1.1.1 # homeassistant.components.ovo_energy -ovoenergy==1.1.11 +ovoenergy==1.1.12 # homeassistant.components.mqtt # homeassistant.components.shiftr diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd694adc0bb..eae0e32ff6c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -579,7 +579,7 @@ onvif-zeep-async==1.0.0 openerz-api==0.1.0 # homeassistant.components.ovo_energy -ovoenergy==1.1.11 +ovoenergy==1.1.12 # homeassistant.components.mqtt # homeassistant.components.shiftr From c85f70b639acd7197841247c45919cb156048230 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 8 May 2021 05:14:41 +0200 Subject: [PATCH 241/852] Small code cleanup for Shelly (#50270) --- homeassistant/components/shelly/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 7fabdb9bf8b..2490eceaba5 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -192,7 +192,6 @@ def get_device_wrapper(hass: HomeAssistant, device_id: str): async def get_coap_context(hass): """Get CoAP context to be used in all Shelly devices.""" context = aioshelly.COAP() - port = DEFAULT_COAP_PORT if DOMAIN in hass.data: port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT) else: From 13fe837fd2d0c9a1b1cebaaeb67761273ae96395 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Fri, 7 May 2021 23:24:37 -0400 Subject: [PATCH 242/852] Add service target to Rachio (#49913) --- homeassistant/components/rachio/services.yaml | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/rachio/services.yaml b/homeassistant/components/rachio/services.yaml index 815a8601314..e40ccc6df29 100644 --- a/homeassistant/components/rachio/services.yaml +++ b/homeassistant/components/rachio/services.yaml @@ -1,33 +1,67 @@ set_zone_moisture_percent: - description: Set the moisture percentage of a zone or group of zones. + name: Set Zone Moisture Percent + description: Set the moisture percentage of a zone or list of zones. + target: + entity: + integration: rachio + domain: switch fields: - entity_id: - description: Name of the zone entity. Can also be a group of zones. [Required] - example: "switch.front_yard" percent: - description: Set the desired zone moisture percentage from 0 to 100. [Required] + name: Percent + description: Set the desired zone moisture percentage from 0 to 100. + required: true example: 50 + selector: + number: + min: 0 + max: 100 + mode: slider + unit_of_measurement: "%" + step: 1 start_multiple_zone_schedule: - description: Create a custom schedule of zones and runtimes. + name: Start Multiple Zones + description: Create a custom schedule of zones and runtimes. Note that all zones should be on the same controller to avoid issues. + target: + entity: + integration: rachio + domain: switch fields: - entity_id: - description: Name of the zone or zones to run. Zones should all be on the same controller, attempting to start zones on multiple controllers may have undesired results. [Required] - example: "switch.front_yard, switch.side_yard" duration: - description: Number of minutes to run the zone(s). If only 1 duration is given, that time will be used for all zones. If given a list of durations, the durations will apply to the respective zone listed above. [Required] + name: Duration + description: Number of minutes to run the zone(s). If only 1 duration is given, that time will be used for all zones. If given a list of durations, the durations will apply to the respective zones listed above. example: 15, 20 + required: true + selector: + object: pause_watering: + name: Pause Watering description: Pause any currently running zones or schedules. fields: devices: - description: Name of controllers to pause. Defaults to all controllers on the account if not provided. [Optional] - example: Main House + name: Devices + description: Name of controllers to pause. Defaults to all controllers on the account if not provided. + example: "Main House" + selector: + text: duration: - description: The number of minutes to pause running schedules. Accepts 1-60. Default is 60 minutes. [Optional] - example: 30 + name: Duration + description: The number of minutes to pause running schedules. Accepts 1-60. Default is 60 minutes if not provided. + example: 60 + default: 60 + selector: + number: + min: 1 + max: 60 + mode: slider + unit_of_measurement: "minutes" + step: 1 resume_watering: + name: Resume Watering description: Resume any paused zone runs or schedules. fields: devices: - description: Name of controllers to resume. Defaults to all controllers on the account if not provided. [Optional] - example: Main House + name: Devices + description: Name of controllers to resume. Defaults to all controllers on the account if not provided. + example: "Main House" + selector: + text: From bf2d40adfed9b9689bcbb61b18306fa1098f74eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 May 2021 00:46:26 -0500 Subject: [PATCH 243/852] Migrate from pytz to python-dateutil (#49643) Co-authored-by: Paulus Schoutsen --- .../components/homeassistant/triggers/time.py | 10 +- .../components/hvv_departures/sensor.py | 7 +- .../components/input_datetime/__init__.py | 8 +- homeassistant/components/radarr/sensor.py | 8 +- homeassistant/components/tod/binary_sensor.py | 108 ++++++---------- homeassistant/components/zamg/sensor.py | 8 +- homeassistant/core.py | 16 +-- homeassistant/package_constraints.txt | 2 +- homeassistant/util/dt.py | 92 +++++--------- requirements.txt | 2 +- setup.py | 2 +- tests/common.py | 2 +- tests/components/climacell/test_sensor.py | 4 +- tests/components/climacell/test_weather.py | 4 +- tests/components/config/test_core.py | 4 +- .../generic_thermostat/test_climate.py | 34 ++--- tests/components/history_stats/test_sensor.py | 3 +- tests/components/jewish_calendar/__init__.py | 8 +- .../jewish_calendar/test_binary_sensor.py | 10 +- .../components/jewish_calendar/test_sensor.py | 18 +-- .../pvpc_hourly_pricing/test_config_flow.py | 5 +- .../pvpc_hourly_pricing/test_sensor.py | 5 +- tests/components/recorder/models_original.py | 2 +- tests/components/recorder/test_init.py | 2 +- tests/components/recorder/test_models.py | 27 ++-- tests/components/sun/test_trigger.py | 2 +- tests/components/tod/test_binary_sensor.py | 76 ++++------- tests/components/withings/common.py | 4 +- tests/components/withings/test_sensor.py | 8 +- tests/components/xiaomi_miio/test_vacuum.py | 18 +-- tests/components/zwave/test_init.py | 6 +- tests/helpers/test_condition.py | 2 +- tests/helpers/test_event.py | 41 ++---- tests/helpers/test_template.py | 3 +- tests/test_config.py | 10 +- tests/test_core.py | 5 +- tests/util/test_dt.py | 120 ++++++++++++++---- 37 files changed, 325 insertions(+), 361 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 69f01672078..6668672732e 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -95,8 +95,14 @@ async def async_attach_trigger(hass, config, action, automation_info): if has_date: # If input_datetime has date, then track point in time. - trigger_dt = dt_util.DEFAULT_TIME_ZONE.localize( - datetime(year, month, day, hour, minute, second) + trigger_dt = datetime( + year, + month, + day, + hour, + minute, + second, + tzinfo=dt_util.DEFAULT_TIME_ZONE, ) # Only set up listener if time is now or in the future. if trigger_dt >= dt_util.now(): diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 5bc70c7a3b4..a3df466da74 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -4,13 +4,12 @@ import logging from aiohttp import ClientConnectorError from pygti.exceptions import InvalidAuth -from pytz import timezone from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ID, DEVICE_CLASS_TIMESTAMP from homeassistant.helpers import aiohttp_client from homeassistant.util import Throttle -from homeassistant.util.dt import utcnow +from homeassistant.util.dt import get_time_zone, utcnow from .const import ATTRIBUTION, CONF_STATION, DOMAIN, MANUFACTURER @@ -28,6 +27,7 @@ ATTR_DELAY = "delay" ATTR_NEXT = "next" PARALLEL_UPDATES = 0 +BERLIN_TIME_ZONE = get_time_zone("Europe/Berlin") _LOGGER = logging.getLogger(__name__) @@ -60,12 +60,11 @@ class HVVDepartureSensor(SensorEntity): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self, **kwargs): """Update the sensor.""" - departure_time = utcnow() + timedelta( minutes=self.config_entry.options.get("offset", 0) ) - departure_time_tz_berlin = departure_time.astimezone(timezone("Europe/Berlin")) + departure_time_tz_berlin = departure_time.astimezone(BERLIN_TIME_ZONE) payload = { "station": self.config_entry.data[CONF_STATION], diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index f423367019e..84a7fb89fe1 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -224,8 +224,8 @@ class InputDatetime(RestoreEntity): dt_util.DEFAULT_TIME_ZONE ) else: - self._current_datetime = dt_util.DEFAULT_TIME_ZONE.localize( - current_datetime + self._current_datetime = current_datetime.replace( + tzinfo=dt_util.DEFAULT_TIME_ZONE ) @classmethod @@ -388,8 +388,8 @@ class InputDatetime(RestoreEntity): if not time: time = self._current_datetime.time() - self._current_datetime = dt_util.DEFAULT_TIME_ZONE.localize( - py_datetime.datetime.combine(date, time) + self._current_datetime = py_datetime.datetime.combine( + date, time, dt_util.DEFAULT_TIME_ZONE ) self.async_write_ha_state() diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 542ff285261..fda7a37756b 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta import logging import time -from pytz import timezone import requests import voluptuous as vol @@ -26,6 +25,7 @@ from homeassistant.const import ( HTTP_OK, ) import homeassistant.helpers.config_validation as cv +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -112,7 +112,6 @@ class RadarrSensor(SensorEntity): self.ssl = "https" if conf.get(CONF_SSL) else "http" self._state = None self.data = [] - self._tz = timezone(str(hass.config.time_zone)) self.type = sensor_type self._name = SENSOR_TYPES[self.type][0] if self.type == "diskspace": @@ -177,8 +176,9 @@ class RadarrSensor(SensorEntity): def update(self): """Update the data for the sensor.""" - start = get_date(self._tz) - end = get_date(self._tz, self.days) + time_zone = dt_util.get_time_zone(self.hass.config.time_zone) + start = get_date(time_zone) + end = get_date(time_zone, self.days) try: res = requests.get( ENDPOINTS[self.type].format( diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index a0fed1f8032..4fd9a3b8bf9 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -1,8 +1,8 @@ """Support for representing current time of the day as binary sensors.""" from datetime import datetime, timedelta import logging +from typing import Callable -import pytz import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity @@ -70,6 +70,7 @@ class TodSensor(BinarySensorEntity): self._before_offset = before_offset self._before = before self._after = after + self._unsub_update: Callable[[], None] = None @property def should_poll(self): @@ -81,59 +82,37 @@ class TodSensor(BinarySensorEntity): """Return the name of the sensor.""" return self._name - @property - def after(self): - """Return the timestamp for the beginning of the period.""" - return self._time_after - - @property - def before(self): - """Return the timestamp for the end of the period.""" - return self._time_before - @property def is_on(self): """Return True is sensor is on.""" - if self.after < self.before: - return self.after <= self.current_datetime < self.before + if self._time_after < self._time_before: + return self._time_after <= dt_util.utcnow() < self._time_before return False - @property - def current_datetime(self): - """Return local current datetime according to hass configuration.""" - return dt_util.utcnow() - - @property - def next_update(self): - """Return the next update point in the UTC time.""" - return self._next_update - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" + time_zone = dt_util.get_time_zone(self.hass.config.time_zone) return { - ATTR_AFTER: self.after.astimezone(self.hass.config.time_zone).isoformat(), - ATTR_BEFORE: self.before.astimezone(self.hass.config.time_zone).isoformat(), - ATTR_NEXT_UPDATE: self.next_update.astimezone( - self.hass.config.time_zone - ).isoformat(), + ATTR_AFTER: self._time_after.astimezone(time_zone).isoformat(), + ATTR_BEFORE: self._time_before.astimezone(time_zone).isoformat(), + ATTR_NEXT_UPDATE: self._next_update.astimezone(time_zone).isoformat(), } def _naive_time_to_utc_datetime(self, naive_time): """Convert naive time from config to utc_datetime with current day.""" # get the current local date from utc time - current_local_date = self.current_datetime.astimezone( - self.hass.config.time_zone - ).date() - # calculate utc datetime corecponding to local time - utc_datetime = self.hass.config.time_zone.localize( - datetime.combine(current_local_date, naive_time) - ).astimezone(tz=pytz.UTC) - return utc_datetime + current_local_date = ( + dt_util.utcnow() + .astimezone(dt_util.get_time_zone(self.hass.config.time_zone)) + .date() + ) + # calculate utc datetime corresponding to local time + return dt_util.as_utc(datetime.combine(current_local_date, naive_time)) - def _calculate_initial_boudary_time(self): + def _calculate_boudary_time(self): """Calculate internal absolute time boundaries.""" - nowutc = self.current_datetime + nowutc = dt_util.utcnow() # If after value is a sun event instead of absolute time if is_sun_event(self._after): # Calculate the today's event utc time or @@ -177,43 +156,34 @@ class TodSensor(BinarySensorEntity): self._time_after += self._after_offset self._time_before += self._before_offset - def _turn_to_next_day(self): - """Turn to to the next day.""" - if is_sun_event(self._after): - self._time_after = get_astral_event_next( - self.hass, self._after, self._time_after - self._after_offset - ) - self._time_after += self._after_offset - else: - # Offset is already there - self._time_after += timedelta(days=1) - - if is_sun_event(self._before): - self._time_before = get_astral_event_next( - self.hass, self._before, self._time_before - self._before_offset - ) - self._time_before += self._before_offset - else: - # Offset is already there - self._time_before += timedelta(days=1) - async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" - self._calculate_initial_boudary_time() + self._calculate_boudary_time() self._calculate_next_update() - self._point_in_time_listener(dt_util.now()) + + @callback + def _clean_up_listener(): + if self._unsub_update is not None: + self._unsub_update() + self._unsub_update = None + + self.async_on_remove(_clean_up_listener) + + self._unsub_update = event.async_track_point_in_utc_time( + self.hass, self._point_in_time_listener, self._next_update + ) def _calculate_next_update(self): """Datetime when the next update to the state.""" - now = self.current_datetime - if now < self.after: - self._next_update = self.after + now = dt_util.utcnow() + if now < self._time_after: + self._next_update = self._time_after return - if now < self.before: - self._next_update = self.before + if now < self._time_before: + self._next_update = self._time_before return - self._turn_to_next_day() - self._next_update = self.after + self._calculate_boudary_time() + self._next_update = self._time_after @callback def _point_in_time_listener(self, now): @@ -221,6 +191,6 @@ class TodSensor(BinarySensorEntity): self._calculate_next_update() self.async_write_ha_state() - event.async_track_point_in_utc_time( - self.hass, self._point_in_time_listener, self.next_update + self._unsub_update = event.async_track_point_in_utc_time( + self.hass, self._point_in_time_listener, self._next_update ) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 2e2d07cea62..36c9a5fa380 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -7,7 +7,6 @@ import logging import os from aiohttp.hdrs import USER_AGENT -import pytz import requests import voluptuous as vol @@ -28,7 +27,7 @@ from homeassistant.const import ( __version__, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -41,6 +40,7 @@ CONF_STATION_ID = "station_id" DEFAULT_NAME = "zamg" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +VIENNA_TIME_ZONE = dt_util.get_time_zone("Europe/Vienna") SENSOR_TYPES = { "pressure": ("Pressure", PRESSURE_HPA, "LDstat hPa", float), @@ -187,7 +187,7 @@ class ZamgData: date, time = self.data.get("update_date"), self.data.get("update_time") if date is not None and time is not None: return datetime.strptime(date + time, "%d-%m-%Y%H:%M").replace( - tzinfo=pytz.timezone("Europe/Vienna") + tzinfo=VIENNA_TIME_ZONE ) @classmethod @@ -208,7 +208,7 @@ class ZamgData: """Get the latest data from ZAMG.""" if self.last_update and ( self.last_update + timedelta(hours=1) - > datetime.utcnow().replace(tzinfo=pytz.utc) + > datetime.utcnow().replace(tzinfo=dt_util.UTC) ): return # Not time to update yet; data is only hourly diff --git a/homeassistant/core.py b/homeassistant/core.py index c22526474a4..067afb23c8a 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1531,7 +1531,7 @@ class Config: self.longitude: float = 0 self.elevation: int = 0 self.location_name: str = "Home" - self.time_zone: datetime.tzinfo = dt_util.UTC + self.time_zone: str = "UTC" self.units: UnitSystem = METRIC_SYSTEM self.internal_url: str | None = None self.external_url: str | None = None @@ -1621,17 +1621,13 @@ class Config: Async friendly. """ - time_zone = dt_util.UTC.zone - if self.time_zone and getattr(self.time_zone, "zone"): - time_zone = getattr(self.time_zone, "zone") - return { "latitude": self.latitude, "longitude": self.longitude, "elevation": self.elevation, "unit_system": self.units.as_dict(), "location_name": self.location_name, - "time_zone": time_zone, + "time_zone": self.time_zone, "components": self.components, "config_dir": self.config_dir, # legacy, backwards compat @@ -1651,7 +1647,7 @@ class Config: time_zone = dt_util.get_time_zone(time_zone_str) if time_zone: - self.time_zone = time_zone + self.time_zone = time_zone_str dt_util.set_default_time_zone(time_zone) else: raise ValueError(f"Received invalid time zone {time_zone_str}") @@ -1721,17 +1717,13 @@ class Config: async def async_store(self) -> None: """Store [homeassistant] core config.""" - time_zone = dt_util.UTC.zone - if self.time_zone and getattr(self.time_zone, "zone"): - time_zone = getattr(self.time_zone, "zone") - data = { "latitude": self.latitude, "longitude": self.longitude, "elevation": self.elevation, "unit_system": self.units.name, "location_name": self.location_name, - "time_zone": time_zone, + "time_zone": self.time_zone, "external_url": self.external_url, "internal_url": self.internal_url, } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2b5546ac53c..71f3a75eac9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,8 +24,8 @@ paho-mqtt==1.5.1 pillow==8.1.2 pip>=8.0.3,<20.3 pyroute2==0.5.18 +python-dateutil==2.8.1 python-slugify==4.0.1 -pytz>=2021.1 pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index b0c6cb21fec..a144918713e 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -4,20 +4,16 @@ from __future__ import annotations from contextlib import suppress import datetime as dt import re -from typing import Any, cast +from typing import Any import ciso8601 -import pytz -import pytz.exceptions as pytzexceptions -import pytz.tzinfo as pytzinfo +from dateutil import tz from homeassistant.const import MATCH_ALL DATE_STR_FORMAT = "%Y-%m-%d" -NATIVE_UTC = dt.timezone.utc -UTC = pytz.utc -DEFAULT_TIME_ZONE: dt.tzinfo = pytz.utc - +UTC = dt.timezone.utc +DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc # Copyright (c) Django Software Foundation and individual contributors. # All rights reserved. @@ -37,7 +33,6 @@ def set_default_time_zone(time_zone: dt.tzinfo) -> None: """ global DEFAULT_TIME_ZONE # pylint: disable=global-statement - # NOTE: Remove in the future in favour of typing assert isinstance(time_zone, dt.tzinfo) DEFAULT_TIME_ZONE = time_zone @@ -48,15 +43,12 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: Async friendly. """ - try: - return pytz.timezone(time_zone_str) - except pytzexceptions.UnknownTimeZoneError: - return None + return tz.gettz(time_zone_str) def utcnow() -> dt.datetime: """Get now in UTC time.""" - return dt.datetime.now(NATIVE_UTC) + return dt.datetime.now(UTC) def now(time_zone: dt.tzinfo | None = None) -> dt.datetime: @@ -72,7 +64,7 @@ def as_utc(dattim: dt.datetime) -> dt.datetime: if dattim.tzinfo == UTC: return dattim if dattim.tzinfo is None: - dattim = DEFAULT_TIME_ZONE.localize(dattim) # type: ignore + dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE) return dattim.astimezone(UTC) @@ -93,14 +85,14 @@ def as_local(dattim: dt.datetime) -> dt.datetime: if dattim.tzinfo == DEFAULT_TIME_ZONE: return dattim if dattim.tzinfo is None: - dattim = UTC.localize(dattim) + dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE) return dattim.astimezone(DEFAULT_TIME_ZONE) def utc_from_timestamp(timestamp: float) -> dt.datetime: """Return a UTC time from a timestamp.""" - return UTC.localize(dt.datetime.utcfromtimestamp(timestamp)) + return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC) def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datetime: @@ -112,9 +104,7 @@ def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datet else: date = dt_or_d - return DEFAULT_TIME_ZONE.localize( # type: ignore - dt.datetime.combine(date, dt.time()) - ) + return dt.datetime.combine(date, dt.time(), tzinfo=DEFAULT_TIME_ZONE) # Copyright (c) Django Software Foundation and individual contributors. @@ -239,6 +229,12 @@ def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> lis return res +def _dst_offset_diff(dattim: dt.datetime) -> dt.timedelta: + """Return the offset when crossing the DST barrier.""" + delta = dt.timedelta(hours=24) + return (dattim + delta).utcoffset() - (dattim - delta).utcoffset() # type: ignore[operator] + + def find_next_time_expression_time( now: dt.datetime, # pylint: disable=redefined-outer-name seconds: list[int], @@ -312,38 +308,28 @@ def find_next_time_expression_time( result = result.replace(hour=next_hour) - if result.tzinfo is None: + if result.tzinfo in (None, UTC): return result - # Now we need to handle timezones. We will make this datetime object - # "naive" first and then re-convert it to the target timezone. - # This is so that we can call pytz's localize and handle DST changes. - tzinfo: pytzinfo.DstTzInfo = UTC if result.tzinfo == NATIVE_UTC else result.tzinfo - result = result.replace(tzinfo=None) - - try: - result = tzinfo.localize(result, is_dst=None) - except pytzexceptions.AmbiguousTimeError: + if tz.datetime_ambiguous(result): # This happens when we're leaving daylight saving time and local # clocks are rolled back. In this case, we want to trigger # on both the DST and non-DST time. So when "now" is in the DST # use the DST-on time, and if not, use the DST-off time. - use_dst = bool(now.dst()) - result = tzinfo.localize(result, is_dst=use_dst) - except pytzexceptions.NonExistentTimeError: + fold = 1 if now.dst() else 0 + if result.fold != fold: + result = result.replace(fold=fold) + + if not tz.datetime_exists(result): # This happens when we're entering daylight saving time and local # clocks are rolled forward, thus there are local times that do # not exist. In this case, we want to trigger on the next time # that *does* exist. # In the worst case, this will run through all the seconds in the # time shift, but that's max 3600 operations for once per year - result = result.replace(tzinfo=tzinfo) + dt.timedelta(seconds=1) - return find_next_time_expression_time(result, seconds, minutes, hours) - - result_dst = cast(dt.timedelta, result.dst()) - now_dst = cast(dt.timedelta, now.dst()) or dt.timedelta(0) - if result_dst >= now_dst: - return result + return find_next_time_expression_time( + result + dt.timedelta(seconds=1), seconds, minutes, hours + ) # Another edge-case when leaving DST: # When now is in DST and ambiguous *and* the next trigger time we *should* @@ -351,23 +337,11 @@ def find_next_time_expression_time( # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST) # we should trigger next on 28.10.2018 2:30 (out of DST), but our # algorithm above would produce 29.10.2018 2:30 (out of DST) + if tz.datetime_ambiguous(now): + check_result = find_next_time_expression_time( + now + _dst_offset_diff(now), seconds, minutes, hours + ) + if tz.datetime_ambiguous(check_result): + return check_result - # Step 1: Check if now is ambiguous - try: - tzinfo.localize(now.replace(tzinfo=None), is_dst=None) - return result - except pytzexceptions.AmbiguousTimeError: - pass - - # Step 2: Check if result of (now - DST) is ambiguous. - check = now - now_dst - check_result = find_next_time_expression_time(check, seconds, minutes, hours) - try: - tzinfo.localize(check_result.replace(tzinfo=None), is_dst=None) - return result - except pytzexceptions.AmbiguousTimeError: - pass - - # OK, edge case does apply. We must override the DST to DST-off - check_result = tzinfo.localize(check_result.replace(tzinfo=None), is_dst=False) - return check_result + return result diff --git a/requirements.txt b/requirements.txt index 475ece2b866..c134926c6fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ PyJWT==1.7.1 cryptography==3.3.2 pip>=8.0.3,<20.3 python-slugify==4.0.1 -pytz>=2021.1 +python-dateutil==2.8.1 pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 diff --git a/setup.py b/setup.py index 4791b0815f1..71b3d66046c 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ REQUIRES = [ "cryptography==3.3.2", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", - "pytz>=2021.1", + "python-dateutil==2.8.1", "pyyaml==5.4.1", "requests==2.25.1", "ruamel.yaml==0.15.100", diff --git a/tests/common.py b/tests/common.py index e3a4a714edf..952350fe68c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -270,7 +270,7 @@ async def async_test_home_assistant(loop, load_registries=True): hass.config.latitude = 32.87336 hass.config.longitude = -117.22743 hass.config.elevation = 0 - hass.config.time_zone = date_util.get_time_zone("US/Pacific") + hass.config.time_zone = "US/Pacific" hass.config.units = METRIC_SYSTEM hass.config.media_dirs = {"local": get_test_config_dir("media")} hass.config.skip_pip = True diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index 44fc163848b..d06742ba209 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -7,7 +7,6 @@ from typing import Any from unittest.mock import patch import pytest -import pytz from homeassistant.components.climacell.config_flow import ( _get_config_schema, @@ -18,6 +17,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt as dt_util from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA @@ -59,7 +59,7 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: """Set up entry and return entity state.""" with patch( "homeassistant.util.dt.utcnow", - return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=pytz.UTC), + return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), ): data = _get_config_schema(hass)(config) config_entry = MockConfigEntry( diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index 02aa65a350e..fa1ef9dc490 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -7,7 +7,6 @@ from typing import Any from unittest.mock import patch import pytest -import pytz from homeassistant.components.climacell.config_flow import ( _get_config_schema, @@ -46,6 +45,7 @@ from homeassistant.components.weather import ( from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt as dt_util from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA @@ -70,7 +70,7 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: """Set up entry and return entity state.""" with patch( "homeassistant.util.dt.utcnow", - return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=pytz.UTC), + return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), ): data = _get_config_schema(hass)(config) config_entry = MockConfigEntry( diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 361fceab565..b58b572e230 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -57,7 +57,7 @@ async def test_websocket_core_update(hass, client): assert hass.config.elevation != 25 assert hass.config.location_name != "Huis" assert hass.config.units.name != CONF_UNIT_SYSTEM_IMPERIAL - assert hass.config.time_zone.zone != "America/New_York" + assert hass.config.time_zone != "America/New_York" assert hass.config.external_url != "https://www.example.com" assert hass.config.internal_url != "http://example.com" @@ -91,7 +91,7 @@ async def test_websocket_core_update(hass, client): assert hass.config.internal_url == "http://example.local" assert len(mock_set_tz.mock_calls) == 1 - assert mock_set_tz.mock_calls[0][1][0].zone == "America/New_York" + assert mock_set_tz.mock_calls[0][1][0] == dt_util.get_time_zone("America/New_York") async def test_websocket_core_update_not_admin(hass, hass_ws_client, hass_admin_user): diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index f5a27ac8b97..a7f42fffbd8 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -4,7 +4,6 @@ from os import path from unittest.mock import patch import pytest -import pytz import voluptuous as vol from homeassistant import config as hass_config @@ -37,6 +36,7 @@ import homeassistant.core as ha from homeassistant.core import DOMAIN as HASS_DOMAIN, CoreState, State, callback from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM from tests.common import ( @@ -691,9 +691,7 @@ async def test_temp_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): async def test_temp_change_ac_trigger_on_long_enough(hass, setup_comp_4): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -719,9 +717,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough(hass, setup_comp_4): async def test_temp_change_ac_trigger_off_long_enough(hass, setup_comp_4): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -801,9 +797,7 @@ async def test_temp_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): async def test_temp_change_ac_trigger_on_long_enough_2(hass, setup_comp_5): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -829,9 +823,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough_2(hass, setup_comp_5): async def test_temp_change_ac_trigger_off_long_enough_2(hass, setup_comp_5): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -919,9 +911,7 @@ async def test_temp_change_heater_trigger_on_not_long_enough(hass, setup_comp_6) async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6): """Test if temperature change turn heater on after min cycle.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -938,9 +928,7 @@ async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6): async def test_temp_change_heater_trigger_off_long_enough(hass, setup_comp_6): """Test if temperature change turn heater off after min cycle.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -1019,7 +1007,7 @@ async def test_temp_change_ac_trigger_on_long_enough_3(hass, setup_comp_7): _setup_sensor(hass, 30) await hass.async_block_till_done() await common.async_set_temperature(hass, 25) - test_time = datetime.datetime.now(pytz.UTC) + test_time = datetime.datetime.now(dt_util.UTC) async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert len(calls) == 0 @@ -1042,7 +1030,7 @@ async def test_temp_change_ac_trigger_off_long_enough_3(hass, setup_comp_7): _setup_sensor(hass, 20) await hass.async_block_till_done() await common.async_set_temperature(hass, 25) - test_time = datetime.datetime.now(pytz.UTC) + test_time = datetime.datetime.now(dt_util.UTC) async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert len(calls) == 0 @@ -1090,7 +1078,7 @@ async def test_temp_change_heater_trigger_on_long_enough_2(hass, setup_comp_8): _setup_sensor(hass, 20) await hass.async_block_till_done() await common.async_set_temperature(hass, 25) - test_time = datetime.datetime.now(pytz.UTC) + test_time = datetime.datetime.now(dt_util.UTC) async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert len(calls) == 0 @@ -1113,7 +1101,7 @@ async def test_temp_change_heater_trigger_off_long_enough_2(hass, setup_comp_8): _setup_sensor(hass, 30) await hass.async_block_till_done() await common.async_set_temperature(hass, 25) - test_time = datetime.datetime.now(pytz.UTC) + test_time = datetime.datetime.now(dt_util.UTC) async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert len(calls) == 0 diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 06ba1f22f47..6e25e9e67cf 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -6,7 +6,6 @@ import unittest from unittest.mock import patch import pytest -import pytz from homeassistant import config as hass_config from homeassistant.components.history_stats import DOMAIN @@ -82,7 +81,7 @@ class TestHistoryStatsSensor(unittest.TestCase): ) def test_period_parsing(self, mock): """Test the conversion from templates to period.""" - now = datetime(2019, 1, 1, 23, 30, 0, tzinfo=pytz.utc) + now = datetime(2019, 1, 1, 23, 30, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): today = Template( "{{ now().replace(hour=0).replace(minute=0).replace(second=0) }}", diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index 2d42458cf1b..b0279fd2748 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -26,7 +26,9 @@ def make_nyc_test_params(dtime, results, havdalah_offset=0): if isinstance(results, dict): time_zone = dt_util.get_time_zone("America/New_York") results = { - key: time_zone.localize(value) if isinstance(value, datetime) else value + key: value.replace(tzinfo=time_zone) + if isinstance(value, datetime) + else value for key, value in results.items() } return ( @@ -46,7 +48,9 @@ def make_jerusalem_test_params(dtime, results, havdalah_offset=0): if isinstance(results, dict): time_zone = dt_util.get_time_zone("Asia/Jerusalem") results = { - key: time_zone.localize(value) if isinstance(value, datetime) else value + key: value.replace(tzinfo=time_zone) + if isinstance(value, datetime) + else value for key, value in results.items() } return ( diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index 1f34532eeb5..b34dfdb28e4 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -179,9 +179,9 @@ async def test_issur_melacha_sensor( ): """Test Issur Melacha sensor output.""" time_zone = dt_util.get_time_zone(tzname) - test_time = time_zone.localize(now) + test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = time_zone + hass.config.time_zone = tzname hass.config.latitude = latitude hass.config.longitude = longitude @@ -214,7 +214,7 @@ async def test_issur_melacha_sensor( [ latitude, longitude, - time_zone, + tzname, HDATE_DEFAULT_ALTITUDE, diaspora, "english", @@ -270,9 +270,9 @@ async def test_issur_melacha_sensor_update( ): """Test Issur Melacha sensor output.""" time_zone = dt_util.get_time_zone(tzname) - test_time = time_zone.localize(now) + test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = time_zone + hass.config.time_zone = tzname hass.config.latitude = latitude hass.config.longitude = longitude diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 8634f28d8fa..970e31c7985 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -163,9 +163,9 @@ async def test_jewish_calendar_sensor( ): """Test Jewish calendar sensor output.""" time_zone = dt_util.get_time_zone(tzname) - test_time = time_zone.localize(now) + test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = time_zone + hass.config.time_zone = tzname hass.config.latitude = latitude hass.config.longitude = longitude @@ -188,7 +188,9 @@ async def test_jewish_calendar_sensor( await hass.async_block_till_done() result = ( - dt_util.as_utc(time_zone.localize(result)) if isinstance(result, dt) else result + dt_util.as_utc(result.replace(tzinfo=time_zone)) + if isinstance(result, dt) + else result ) sensor_object = hass.states.get(f"sensor.test_{sensor}") @@ -506,9 +508,9 @@ async def test_shabbat_times_sensor( ): """Test sensor output for upcoming shabbat/yomtov times.""" time_zone = dt_util.get_time_zone(tzname) - test_time = time_zone.localize(now) + test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = time_zone + hass.config.time_zone = tzname hass.config.latitude = latitude hass.config.longitude = longitude @@ -559,7 +561,7 @@ async def test_shabbat_times_sensor( [ latitude, longitude, - time_zone, + tzname, HDATE_DEFAULT_ALTITUDE, diaspora, language, @@ -593,7 +595,7 @@ OMER_TEST_IDS = [ @pytest.mark.parametrize(["test_time", "result"], OMER_PARAMS, ids=OMER_TEST_IDS) async def test_omer_sensor(hass, legacy_patchable_time, test_time, result): """Test Omer Count sensor output.""" - test_time = hass.config.time_zone.localize(test_time) + test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) with alter_time(test_time): assert await async_setup_component( @@ -627,7 +629,7 @@ DAFYOMI_TEST_IDS = [ @pytest.mark.parametrize(["test_time", "result"], DAFYOMI_PARAMS, ids=DAFYOMI_TEST_IDS) async def test_dafyomi_sensor(hass, legacy_patchable_time, test_time, result): """Test Daf Yomi sensor output.""" - test_time = hass.config.time_zone.localize(test_time) + test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) with alter_time(test_time): assert await async_setup_component( diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 31a7005c4cc..2a64d81ef98 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -2,12 +2,11 @@ from datetime import datetime from unittest.mock import patch -from pytz import timezone - from homeassistant import config_entries, data_entry_flow from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN from homeassistant.const import CONF_NAME from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .conftest import check_valid_state @@ -26,7 +25,7 @@ async def test_config_flow( - Check abort when trying to config another with same tariff - Check removal and add again to check state restoration """ - hass.config.time_zone = timezone("Europe/Madrid") + hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") mock_data = {"return_time": datetime(2019, 10, 26, 14, 0, tzinfo=date_util.UTC)} def mock_now(): diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index 2045ba52671..19f3a7aa31c 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -3,12 +3,11 @@ from datetime import datetime, timedelta import logging from unittest.mock import patch -from pytz import timezone - from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN from homeassistant.const import CONF_NAME from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from .conftest import check_valid_state @@ -32,7 +31,7 @@ async def test_sensor_availability( hass, caplog, legacy_patchable_time, pvpc_aioclient_mock: AiohttpClientMocker ): """Test sensor availability and handling of cloud access.""" - hass.config.time_zone = timezone("Europe/Madrid") + hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") config = {DOMAIN: [{CONF_NAME: "test_dst", ATTR_TARIFF: "discrimination"}]} mock_data = {"return_time": datetime(2019, 10, 27, 20, 0, 0, tzinfo=date_util.UTC)} diff --git a/tests/components/recorder/models_original.py b/tests/components/recorder/models_original.py index 4c9880d9257..5f64fbda736 100644 --- a/tests/components/recorder/models_original.py +++ b/tests/components/recorder/models_original.py @@ -170,5 +170,5 @@ def _process_timestamp(ts): if ts is None: return None if ts.tzinfo is None: - return dt_util.UTC.localize(ts) + return ts.replace(tzinfo=dt_util.UTC) return dt_util.as_utc(ts) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index a34df0a4ac2..a045bb638ce 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -604,7 +604,7 @@ def test_auto_purge(hass_recorder): # # The clock is started at 4:15am then advanced forward below now = dt_util.utcnow() - test_time = tz.localize(datetime(now.year + 2, 1, 1, 4, 15, 0)) + test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) run_tasks_at_time(hass, test_time) with patch( diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 26a1e487ba8..9f32f1c5746 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -2,7 +2,6 @@ from datetime import datetime import pytest -import pytz from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker @@ -144,11 +143,11 @@ async def test_process_timestamp(): """Test processing time stamp to UTC.""" datetime_with_tzinfo = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC) datetime_without_tzinfo = datetime(2016, 7, 9, 11, 0, 0) - est = pytz.timezone("US/Eastern") + est = dt_util.get_time_zone("US/Eastern") datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est) - nst = pytz.timezone("Canada/Newfoundland") + nst = dt_util.get_time_zone("Canada/Newfoundland") datetime_nst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=nst) - hst = pytz.timezone("US/Hawaii") + hst = dt_util.get_time_zone("US/Hawaii") datetime_hst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=hst) assert process_timestamp(datetime_with_tzinfo) == datetime( @@ -158,13 +157,13 @@ async def test_process_timestamp(): 2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC ) assert process_timestamp(datetime_est_timezone) == datetime( - 2016, 7, 9, 15, 56, tzinfo=dt.UTC + 2016, 7, 9, 15, 0, tzinfo=dt.UTC ) assert process_timestamp(datetime_nst_timezone) == datetime( - 2016, 7, 9, 14, 31, tzinfo=dt.UTC + 2016, 7, 9, 13, 30, tzinfo=dt.UTC ) assert process_timestamp(datetime_hst_timezone) == datetime( - 2016, 7, 9, 21, 31, tzinfo=dt.UTC + 2016, 7, 9, 21, 0, tzinfo=dt.UTC ) assert process_timestamp(None) is None @@ -173,13 +172,13 @@ async def test_process_timestamp_to_utc_isoformat(): """Test processing time stamp to UTC isoformat.""" datetime_with_tzinfo = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC) datetime_without_tzinfo = datetime(2016, 7, 9, 11, 0, 0) - est = pytz.timezone("US/Eastern") + est = dt_util.get_time_zone("US/Eastern") datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est) - est = pytz.timezone("US/Eastern") + est = dt_util.get_time_zone("US/Eastern") datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est) - nst = pytz.timezone("Canada/Newfoundland") + nst = dt_util.get_time_zone("Canada/Newfoundland") datetime_nst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=nst) - hst = pytz.timezone("US/Hawaii") + hst = dt_util.get_time_zone("US/Hawaii") datetime_hst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=hst) assert ( @@ -192,15 +191,15 @@ async def test_process_timestamp_to_utc_isoformat(): ) assert ( process_timestamp_to_utc_isoformat(datetime_est_timezone) - == "2016-07-09T15:56:00+00:00" + == "2016-07-09T15:00:00+00:00" ) assert ( process_timestamp_to_utc_isoformat(datetime_nst_timezone) - == "2016-07-09T14:31:00+00:00" + == "2016-07-09T13:30:00+00:00" ) assert ( process_timestamp_to_utc_isoformat(datetime_hst_timezone) - == "2016-07-09T21:31:00+00:00" + == "2016-07-09T21:00:00+00:00" ) assert process_timestamp_to_utc_isoformat(None) is None diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 6e6a1f11ef9..f100fb53dc8 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -33,7 +33,7 @@ def calls(hass): def setup_comp(hass): """Initialize components.""" mock_component(hass, "group") - dt_util.set_default_time_zone(hass.config.time_zone) + hass.config.set_time_zone(hass.config.time_zone) hass.loop.run_until_complete( async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) ) diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 2eb506f80f3..8b63082c36c 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta from unittest.mock import patch import pytest -import pytz from homeassistant.const import STATE_OFF, STATE_ON import homeassistant.core as ha @@ -61,7 +60,7 @@ async def test_setup_no_sensors(hass): async def test_in_period_on_start(hass): """Test simple setting.""" - test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=hass.config.time_zone) + test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ { @@ -85,7 +84,7 @@ async def test_in_period_on_start(hass): async def test_midnight_turnover_before_midnight_inside_period(hass): """Test midnight turnover setting before midnight inside period .""" - test_time = datetime(2019, 1, 10, 22, 30, 0, tzinfo=hass.config.time_zone) + test_time = datetime(2019, 1, 10, 22, 30, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} @@ -104,9 +103,7 @@ async def test_midnight_turnover_before_midnight_inside_period(hass): async def test_midnight_turnover_after_midnight_inside_period(hass): """Test midnight turnover setting before midnight inside period .""" - test_time = hass.config.time_zone.localize( - datetime(2019, 1, 10, 21, 0, 0) - ).astimezone(pytz.UTC) + test_time = datetime(2019, 1, 10, 21, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} @@ -140,9 +137,7 @@ async def test_midnight_turnover_after_midnight_inside_period(hass): async def test_midnight_turnover_before_midnight_outside_period(hass): """Test midnight turnover setting before midnight outside period.""" - test_time = hass.config.time_zone.localize( - datetime(2019, 1, 10, 20, 30, 0) - ).astimezone(pytz.UTC) + test_time = datetime(2019, 1, 10, 20, 30, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} @@ -161,9 +156,7 @@ async def test_midnight_turnover_before_midnight_outside_period(hass): async def test_midnight_turnover_after_midnight_outside_period(hass): """Test midnight turnover setting before midnight inside period .""" - test_time = hass.config.time_zone.localize( - datetime(2019, 1, 10, 20, 0, 0) - ).astimezone(pytz.UTC) + test_time = datetime(2019, 1, 10, 20, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -180,9 +173,7 @@ async def test_midnight_turnover_after_midnight_outside_period(hass): state = hass.states.get("binary_sensor.night") assert state.state == STATE_OFF - switchover_time = hass.config.time_zone.localize( - datetime(2019, 1, 11, 4, 59, 0) - ).astimezone(pytz.UTC) + switchover_time = datetime(2019, 1, 11, 4, 59, 0, tzinfo=dt_util.UTC) with patch( "homeassistant.components.tod.binary_sensor.dt_util.utcnow", return_value=switchover_time, @@ -210,9 +201,7 @@ async def test_midnight_turnover_after_midnight_outside_period(hass): async def test_from_sunrise_to_sunset(hass): """Test period from sunrise to sunset.""" - test_time = hass.config.time_zone.localize(datetime(2019, 1, 12)).astimezone( - pytz.UTC - ) + test_time = datetime(2019, 1, 12, tzinfo=dt_util.UTC) sunrise = dt_util.as_local( get_astral_event_date(hass, "sunrise", dt_util.as_utc(test_time)) ) @@ -311,9 +300,7 @@ async def test_from_sunrise_to_sunset(hass): async def test_from_sunset_to_sunrise(hass): """Test period from sunset to sunrise.""" - test_time = hass.config.time_zone.localize(datetime(2019, 1, 12)).astimezone( - pytz.UTC - ) + test_time = datetime(2019, 1, 12, tzinfo=dt_util.UTC) sunset = dt_util.as_local(get_astral_event_date(hass, "sunset", test_time)) sunrise = dt_util.as_local(get_astral_event_next(hass, "sunrise", sunset)) # assert sunset == sunrise @@ -405,13 +392,13 @@ async def test_from_sunset_to_sunrise(hass): async def test_offset(hass): """Test offset.""" - after = hass.config.time_zone.localize(datetime(2019, 1, 10, 18, 0, 0)).astimezone( - pytz.UTC - ) + timedelta(hours=1, minutes=34) + after = datetime(2019, 1, 10, 18, 0, 0, tzinfo=dt_util.UTC) + timedelta( + hours=1, minutes=34 + ) - before = hass.config.time_zone.localize(datetime(2019, 1, 10, 22, 0, 0)).astimezone( - pytz.UTC - ) + timedelta(hours=1, minutes=45) + before = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) + timedelta( + hours=1, minutes=45 + ) entity_id = "binary_sensor.evening" config = { @@ -484,9 +471,9 @@ async def test_offset(hass): async def test_offset_overnight(hass): """Test offset overnight.""" - after = hass.config.time_zone.localize(datetime(2019, 1, 10, 18, 0, 0)).astimezone( - pytz.UTC - ) + timedelta(hours=1, minutes=34) + after = datetime(2019, 1, 10, 18, 0, 0, tzinfo=dt_util.UTC) + timedelta( + hours=1, minutes=34 + ) entity_id = "binary_sensor.evening" config = { "binary_sensor": [ @@ -528,9 +515,7 @@ async def test_norwegian_case_winter(hass): hass.config.latitude = 69.6 hass.config.longitude = 18.8 - test_time = hass.config.time_zone.localize(datetime(2010, 1, 1)).astimezone( - pytz.UTC - ) + test_time = datetime(2010, 1, 1, tzinfo=dt_util.UTC) sunrise = dt_util.as_local( get_astral_event_next(hass, "sunrise", dt_util.as_utc(test_time)) ) @@ -645,9 +630,7 @@ async def test_norwegian_case_summer(hass): hass.config.longitude = 18.8 hass.config.elevation = 10.0 - test_time = hass.config.time_zone.localize(datetime(2010, 6, 1)).astimezone( - pytz.UTC - ) + test_time = datetime(2010, 6, 1, tzinfo=dt_util.UTC) sunrise = dt_util.as_local( get_astral_event_next(hass, "sunrise", dt_util.as_utc(test_time)) @@ -759,9 +742,7 @@ async def test_norwegian_case_summer(hass): async def test_sun_offset(hass): """Test sun event with offset.""" - test_time = hass.config.time_zone.localize(datetime(2019, 1, 12)).astimezone( - pytz.UTC - ) + test_time = datetime(2019, 1, 12, tzinfo=dt_util.UTC) sunrise = dt_util.as_local( get_astral_event_date(hass, "sunrise", dt_util.as_utc(test_time)) + timedelta(hours=-1, minutes=-30) @@ -881,30 +862,27 @@ async def test_sun_offset(hass): async def test_dst(hass): """Test sun event with offset.""" - hass.config.time_zone = pytz.timezone("CET") - test_time = hass.config.time_zone.localize( - datetime(2019, 3, 30, 3, 0, 0) - ).astimezone(pytz.UTC) + hass.config.time_zone = "CET" + test_time = datetime(2019, 3, 30, 3, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ {"platform": "tod", "name": "Day", "after": "2:30", "before": "2:40"} ] } + # Test DST: # after 2019-03-30 03:00 CET the next update should ge scheduled # at 3:30 not 2:30 local time - # Internally the entity_id = "binary_sensor.day" - testtime = test_time with patch( "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, + return_value=test_time, ): await async_setup_component(hass, "binary_sensor", config) await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.attributes["after"] == "2019-03-31T03:30:00+02:00" - assert state.attributes["before"] == "2019-03-31T03:40:00+02:00" - assert state.attributes["next_update"] == "2019-03-31T03:30:00+02:00" + assert state.attributes["after"] == "2019-03-30T03:30:00+01:00" + assert state.attributes["before"] == "2019-03-30T03:40:00+01:00" + assert state.attributes["next_update"] == "2019-03-30T03:30:00+01:00" assert state.state == STATE_OFF diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 4d3552bf662..aea2d0152b2 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -7,7 +7,6 @@ from urllib.parse import urlparse from aiohttp.test_utils import TestClient import arrow -import pytz from withings_api.common import ( MeasureGetMeasResponse, NotifyAppli, @@ -40,6 +39,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.config_entry_oauth2_flow import AUTH_CALLBACK_PATH from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from tests.test_util.aiohttp import AiohttpClientMocker @@ -77,7 +77,7 @@ def new_profile_config( measuregrps=[], more=False, offset=0, - timezone=pytz.UTC, + timezone=dt_util.UTC, updatetime=arrow.get(12345), ), api_response_sleep_get_summary=api_response_sleep_get_summary diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 71e69967796..7e337da8afb 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -3,7 +3,6 @@ from typing import Any from unittest.mock import patch import arrow -import pytz from withings_api.common import ( GetSleepSummaryData, GetSleepSummarySerie, @@ -29,6 +28,7 @@ from homeassistant.components.withings.const import Measurement from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.util import dt as dt_util from .common import ComponentFactory, new_profile_config @@ -189,7 +189,7 @@ PERSON0 = new_profile_config( ), ), more=False, - timezone=pytz.UTC, + timezone=dt_util.UTC, updatetime=arrow.get("2019-08-01"), offset=0, ), @@ -198,7 +198,7 @@ PERSON0 = new_profile_config( offset=0, series=( GetSleepSummarySerie( - timezone=pytz.UTC, + timezone=dt_util.UTC, model=SleepModel.SLEEP_MONITOR, startdate=arrow.get("2019-02-01"), enddate=arrow.get("2019-02-01"), @@ -225,7 +225,7 @@ PERSON0 = new_profile_config( ), ), GetSleepSummarySerie( - timezone=pytz.UTC, + timezone=dt_util.UTC, model=SleepModel.SLEEP_MONITOR, startdate=arrow.get("2019-02-01"), enddate=arrow.get("2019-02-01"), diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 23e5d8884b3..fe0466472fa 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch from miio import DeviceException import pytest -from pytz import utc from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, @@ -55,6 +54,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.util import dt as dt_util from .test_config_flow import TEST_MAC @@ -106,12 +106,12 @@ def mirobo_is_got_error_fixture(): mock_timer_1 = MagicMock() mock_timer_1.enabled = True mock_timer_1.cron = "5 5 1 8 1" - mock_timer_1.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc) + mock_timer_1.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC) mock_timer_2 = MagicMock() mock_timer_2.enabled = False mock_timer_2.cron = "5 5 1 8 2" - mock_timer_2.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc) + mock_timer_2.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC) mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2] @@ -180,12 +180,12 @@ def mirobo_is_on_fixture(): mock_timer_1 = MagicMock() mock_timer_1.enabled = True mock_timer_1.cron = "5 5 1 8 1" - mock_timer_1.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc) + mock_timer_1.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC) mock_timer_2 = MagicMock() mock_timer_2.enabled = False mock_timer_2.cron = "5 5 1 8 2" - mock_timer_2.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc) + mock_timer_2.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC) mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2] @@ -255,12 +255,12 @@ async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): { "enabled": True, "cron": "5 5 1 8 1", - "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc), + "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC), }, { "enabled": False, "cron": "5 5 1 8 2", - "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc), + "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC), }, ] @@ -353,12 +353,12 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): { "enabled": True, "cron": "5 5 1 8 1", - "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc), + "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC), }, { "enabled": False, "cron": "5 5 1 8 2", - "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc), + "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC), }, ] diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 6b1a6fe4f98..ecf5759b835 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -5,7 +5,6 @@ from datetime import datetime from unittest.mock import MagicMock, patch import pytest -from pytz import utc import voluptuous as vol from homeassistant.bootstrap import async_setup_component @@ -19,6 +18,7 @@ from homeassistant.components.zwave import ( from homeassistant.components.zwave.binary_sensor import get_device from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, mock_registry from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue @@ -140,7 +140,7 @@ async def test_auto_heal_midnight(hass, mock_openzwave, legacy_patchable_time): network = hass.data[zwave.DATA_NETWORK] assert not network.heal.called - time = utc.localize(datetime(2017, 5, 6, 0, 0, 0)) + time = datetime(2017, 5, 6, 0, 0, 0, tzinfo=dt_util.UTC) async_fire_time_changed(hass, time) await hass.async_block_till_done() await hass.async_block_till_done() @@ -156,7 +156,7 @@ async def test_auto_heal_disabled(hass, mock_openzwave): network = hass.data[zwave.DATA_NETWORK] assert not network.heal.called - time = utc.localize(datetime(2017, 5, 6, 0, 0, 0)) + time = datetime(2017, 5, 6, 0, 0, 0, tzinfo=dt_util.UTC) async_fire_time_changed(hass, time) await hass.async_block_till_done() assert not network.heal.called diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 11705502e77..9347d0bc025 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -27,7 +27,7 @@ def calls(hass): @pytest.fixture(autouse=True) def setup_comp(hass): """Initialize components.""" - dt_util.set_default_time_zone(hass.config.time_zone) + hass.config.set_time_zone(hass.config.time_zone) hass.loop.run_until_complete( async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index b8291b97efa..e134c5e327d 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -2908,8 +2908,8 @@ async def test_periodic_task_entering_dst(hass): specific_runs = [] now = dt_util.utcnow() - time_that_will_not_match_right_away = timezone.localize( - datetime(now.year + 1, 3, 25, 2, 31, 0) + time_that_will_not_match_right_away = datetime( + now.year + 1, 3, 25, 2, 31, 0, tzinfo=timezone ) with patch( @@ -2924,25 +2924,25 @@ async def test_periodic_task_entering_dst(hass): ) async_fire_time_changed( - hass, timezone.localize(datetime(now.year + 1, 3, 25, 1, 50, 0, 999999)) + hass, datetime(now.year + 1, 3, 25, 1, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, timezone.localize(datetime(now.year + 1, 3, 25, 3, 50, 0, 999999)) + hass, datetime(now.year + 1, 3, 25, 3, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, timezone.localize(datetime(now.year + 1, 3, 26, 1, 50, 0, 999999)) + hass, datetime(now.year + 1, 3, 26, 1, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, timezone.localize(datetime(now.year + 1, 3, 26, 2, 50, 0, 999999)) + hass, datetime(now.year + 1, 3, 26, 2, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 1 @@ -2958,8 +2958,8 @@ async def test_periodic_task_leaving_dst(hass): now = dt_util.utcnow() - time_that_will_not_match_right_away = timezone.localize( - datetime(now.year + 1, 10, 28, 2, 28, 0), is_dst=True + time_that_will_not_match_right_away = datetime( + now.year + 1, 10, 28, 2, 28, 0, tzinfo=timezone, fold=1 ) with patch( @@ -2974,46 +2974,33 @@ async def test_periodic_task_leaving_dst(hass): ) async_fire_time_changed( - hass, - timezone.localize( - datetime(now.year + 1, 10, 28, 2, 5, 0, 999999), is_dst=False - ), + hass, datetime(now.year + 1, 10, 28, 2, 5, 0, 999999, tzinfo=timezone, fold=0) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, - timezone.localize( - datetime(now.year + 1, 10, 28, 2, 55, 0, 999999), is_dst=False - ), + hass, datetime(now.year + 1, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=0) ) await hass.async_block_till_done() assert len(specific_runs) == 1 async_fire_time_changed( hass, - timezone.localize( - datetime(now.year + 2, 10, 28, 2, 45, 0, 999999), is_dst=True - ), + datetime(now.year + 2, 10, 28, 2, 45, 0, 999999, tzinfo=timezone, fold=1), ) await hass.async_block_till_done() assert len(specific_runs) == 2 async_fire_time_changed( hass, - timezone.localize( - datetime(now.year + 2, 10, 28, 2, 55, 0, 999999), is_dst=True - ), + datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1), ) await hass.async_block_till_done() assert len(specific_runs) == 2 async_fire_time_changed( - hass, - timezone.localize( - datetime(now.year + 2, 10, 28, 2, 55, 0, 999999), is_dst=True - ), + hass, datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1) ) await hass.async_block_till_done() assert len(specific_runs) == 2 @@ -3224,7 +3211,7 @@ async def test_async_track_point_in_time_cancel(hass): await asyncio.sleep(0.2) assert len(times) == 1 - assert times[0].tzinfo.zone == "US/Hawaii" + assert "US/Hawaii" in str(times[0].tzinfo) async def test_async_track_entity_registry_updated_event(hass): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 704194cd49b..48ef6f25b67 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -5,7 +5,6 @@ import random from unittest.mock import patch import pytest -import pytz import voluptuous as vol from homeassistant.components import group @@ -763,7 +762,7 @@ def test_render_with_possible_json_value_non_string_value(hass): hass, ) value = datetime(2019, 1, 18, 12, 13, 14) - expected = str(pytz.utc.localize(value)) + expected = str(value.replace(tzinfo=dt_util.UTC)) assert tpl.async_render_with_possible_json_value(value) == expected diff --git a/tests/test_config.py b/tests/test_config.py index 76218ab5bf2..2ca46e2c1e9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -374,7 +374,7 @@ async def test_loading_configuration_from_storage(hass, hass_storage): assert hass.config.elevation == 10 assert hass.config.location_name == "Home" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert hass.config.time_zone.zone == "Europe/Copenhagen" + assert hass.config.time_zone == "Europe/Copenhagen" assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" assert len(hass.config.allowlist_external_dirs) == 3 @@ -405,7 +405,7 @@ async def test_loading_configuration_from_storage_with_yaml_only(hass, hass_stor assert hass.config.elevation == 10 assert hass.config.location_name == "Home" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert hass.config.time_zone.zone == "Europe/Copenhagen" + assert hass.config.time_zone == "Europe/Copenhagen" assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.media_dirs == {"mymedia": "/usr"} @@ -463,7 +463,7 @@ async def test_override_stored_configuration(hass, hass_storage): assert hass.config.elevation == 10 assert hass.config.location_name == "Home" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert hass.config.time_zone.zone == "Europe/Copenhagen" + assert hass.config.time_zone == "Europe/Copenhagen" assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.config_source == config_util.SOURCE_YAML @@ -493,7 +493,7 @@ async def test_loading_configuration(hass): assert hass.config.elevation == 25 assert hass.config.location_name == "Huis" assert hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL - assert hass.config.time_zone.zone == "America/New_York" + assert hass.config.time_zone == "America/New_York" assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" assert len(hass.config.allowlist_external_dirs) == 3 @@ -525,7 +525,7 @@ async def test_loading_configuration_temperature_unit(hass): assert hass.config.elevation == 25 assert hass.config.location_name == "Huis" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert hass.config.time_zone.zone == "America/New_York" + assert hass.config.time_zone == "America/New_York" assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" assert hass.config.config_source == config_util.SOURCE_YAML diff --git a/tests/test_core.py b/tests/test_core.py index d3283c14b84..0a205cedad1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,7 +9,6 @@ from tempfile import TemporaryDirectory from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest -import pytz import voluptuous as vol from homeassistant.const import ( @@ -44,7 +43,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM from tests.common import async_capture_events, async_mock_service -PST = pytz.timezone("America/Los_Angeles") +PST = dt_util.get_time_zone("America/Los_Angeles") def test_split_entity_id(): @@ -877,7 +876,7 @@ def test_config_defaults(): assert config.longitude == 0 assert config.elevation == 0 assert config.location_name == "Home" - assert config.time_zone == dt_util.UTC + assert config.time_zone == "UTC" assert config.internal_url is None assert config.external_url is None assert config.config_source == "default" diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 50013012201..628cb533681 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -16,17 +16,12 @@ def teardown(): def test_get_time_zone_retrieves_valid_time_zone(): """Test getting a time zone.""" - time_zone = dt_util.get_time_zone(TEST_TIME_ZONE) - - assert time_zone is not None - assert time_zone.zone == TEST_TIME_ZONE + assert dt_util.get_time_zone(TEST_TIME_ZONE) is not None def test_get_time_zone_returns_none_for_garbage_time_zone(): """Test getting a non existing time zone.""" - time_zone = dt_util.get_time_zone("Non existing time zone") - - assert time_zone is None + assert dt_util.get_time_zone("Non existing time zone") is None def test_set_default_time_zone(): @@ -35,8 +30,7 @@ def test_set_default_time_zone(): dt_util.set_default_time_zone(time_zone) - # We cannot compare the timezones directly because of DST - assert time_zone.zone == dt_util.now().tzinfo.zone + assert dt_util.now().tzinfo is time_zone def test_utcnow(): @@ -239,35 +233,111 @@ def test_find_next_time_expression_time_dst(): return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) # Entering DST, clocks are rolled forward - assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == find( - tz.localize(datetime(2018, 3, 25, 1, 50, 0)), 2, 30, 0 + assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + datetime(2018, 3, 25, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == find( - tz.localize(datetime(2018, 3, 25, 3, 50, 0)), 2, 30, 0 + assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + datetime(2018, 3, 25, 3, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == find( - tz.localize(datetime(2018, 3, 26, 1, 50, 0)), 2, 30, 0 + assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + datetime(2018, 3, 26, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) # Leaving DST, clocks are rolled back - assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False) == find( - tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False), 2, 30, 0 + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False) == find( - tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True), 2, 30, 0 + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert tz.localize(datetime(2018, 10, 28, 4, 30, 0), is_dst=False) == find( - tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True), 4, 30, 0 + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz), 2, 30, 0 ) - assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=True) == find( - tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True), 2, 30, 0 + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert tz.localize(datetime(2018, 10, 29, 2, 30, 0)) == find( - tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False), 2, 30, 0 + assert datetime(2018, 10, 28, 4, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0 + ) + + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0 + ) + + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 + ) + + +def test_find_next_time_expression_time_dst_chicago(): + """Test daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + def find(dt, hour, minute, second): + """Call test_find_next_time_expression_time.""" + seconds = dt_util.parse_time_expression(second, 0, 59) + minutes = dt_util.parse_time_expression(minute, 0, 59) + hours = dt_util.parse_time_expression(hour, 0, 23) + + return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + + # Entering DST, clocks are rolled forward + assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + datetime(2021, 3, 14, 3, 50, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 3, 14, 3, 30, 0, tzinfo=tz) == find( + datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 3, 30, 0 + ) + + # Leaving DST, clocks are rolled back + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0 + ) + + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz) == find( + datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2021, 11, 7, 2, 10, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0), 2, 30, 0 + ) + + assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 + ) + + assert datetime(2021, 11, 7, 4, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0 + ) + + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0 + ) + + assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz) == find( + datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) From e3bc9eaf5f72aa873905f477236424da14e6ce32 Mon Sep 17 00:00:00 2001 From: Leonardo Figueiro Date: Sat, 8 May 2021 06:26:13 -0300 Subject: [PATCH 244/852] pywilight update (#50207) --- homeassistant/components/wilight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json index 689a37f3c91..fec9fdb6c6a 100644 --- a/homeassistant/components/wilight/manifest.json +++ b/homeassistant/components/wilight/manifest.json @@ -3,7 +3,7 @@ "name": "WiLight", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wilight", - "requirements": ["pywilight==0.0.68"], + "requirements": ["pywilight==0.0.70"], "ssdp": [ { "manufacturer": "All Automacao Ltda" diff --git a/requirements_all.txt b/requirements_all.txt index ee6dd46df86..9ac6c8c1501 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1937,7 +1937,7 @@ pywebpush==1.9.2 pywemo==0.6.3 # homeassistant.components.wilight -pywilight==0.0.68 +pywilight==0.0.70 # homeassistant.components.xeoma pyxeoma==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eae0e32ff6c..cb9d2d7d033 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1043,7 +1043,7 @@ pywebpush==1.9.2 pywemo==0.6.3 # homeassistant.components.wilight -pywilight==0.0.68 +pywilight==0.0.70 # homeassistant.components.zerproc pyzerproc==0.4.8 From e0de6752af8aa489533b743cc57c5b4f4d1f3875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Sat, 8 May 2021 13:26:31 +0200 Subject: [PATCH 245/852] Fix incorrect attribute checks in Modbus hub (#50241) --- homeassistant/components/modbus/modbus.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index c1fbe7a9eb7..6568f552fa6 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -237,7 +237,7 @@ class ModbusHub: except ModbusException as exception_error: self._log_error(exception_error) result = exception_error - if not hasattr(result, "registers"): + if not hasattr(result, "bits"): self._log_error(result) return None self._in_error = False @@ -251,7 +251,7 @@ class ModbusHub: result = self._client.read_discrete_inputs(address, count, **kwargs) except ModbusException as exception_error: result = exception_error - if not hasattr(result, "registers"): + if not hasattr(result, "bits"): self._log_error(result) return None self._in_error = False @@ -293,7 +293,7 @@ class ModbusHub: result = self._client.write_coil(address, value, **kwargs) except ModbusException as exception_error: result = exception_error - if not hasattr(result, "registers"): + if not hasattr(result, "value"): self._log_error(result) return False self._in_error = False @@ -307,7 +307,7 @@ class ModbusHub: result = self._client.write_coils(address, values, **kwargs) except ModbusException as exception_error: result = exception_error - if not hasattr(result, "registers"): + if not hasattr(result, "count"): self._log_error(result) return False self._in_error = False @@ -321,7 +321,7 @@ class ModbusHub: result = self._client.write_register(address, value, **kwargs) except ModbusException as exception_error: result = exception_error - if not hasattr(result, "registers"): + if not hasattr(result, "value"): self._log_error(result) return False self._in_error = False @@ -335,7 +335,7 @@ class ModbusHub: result = self._client.write_registers(address, values, **kwargs) except ModbusException as exception_error: result = exception_error - if not hasattr(result, "registers"): + if not hasattr(result, "count"): self._log_error(result) return False self._in_error = False From 29eb31e9da1b50933da784f80b2d8c4bf0341716 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 8 May 2021 13:28:35 +0200 Subject: [PATCH 246/852] Add configurable delay between connect and first request in modbus (#50124) * Activate startup delay. * Add removal of call_later if HA is stopped. This is unlikely to happen, but just security measure. * Removing timing interval. async_fire_time_changed() needs to be called twice, first time the delay is ended and second time update() is executed. * Variable naming. --- homeassistant/components/modbus/modbus.py | 42 ++++++-- tests/components/modbus/test_init.py | 120 ++++++++++++++++++++++ 2 files changed, 155 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 6568f552fa6..bafb4d01c3f 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -22,6 +22,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.event import async_call_later from .const import ( ATTR_ADDRESS, @@ -59,7 +60,7 @@ def modbus_setup( # modbus needs to be activated before components are loaded # to avoid a racing problem - hub_collect[conf_hub[CONF_NAME]].setup() + hub_collect[conf_hub[CONF_NAME]].setup(hass) # load platforms for component, conf_key in ( @@ -131,13 +132,14 @@ class ModbusHub: # generic configuration self._client = None + self._cancel_listener = None self._in_error = False self._lock = threading.Lock() self._config_name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] self._config_port = client_config[CONF_PORT] self._config_timeout = client_config[CONF_TIMEOUT] - self._config_delay = 0 + self._config_delay = client_config[CONF_DELAY] Defaults.Timeout = 10 if self._config_type == "serial": @@ -150,10 +152,6 @@ class ModbusHub: else: # network configuration self._config_host = client_config[CONF_HOST] - self._config_delay = client_config[CONF_DELAY] - - if self._config_delay > 0: - _LOGGER.warning("Parameter delay is accepted but not used in this version") @property def name(self): @@ -168,7 +166,7 @@ class ModbusHub: _LOGGER.error(log_text) self._in_error = error_state - def setup(self): + def setup(self, hass): """Set up pymodbus client.""" try: if self._config_type == "serial": @@ -208,8 +206,22 @@ class ModbusHub: # Connect device self.connect() + # Start counting down to allow modbus requests. + if self._config_delay: + self._cancel_listener = async_call_later( + hass, self._config_delay, self.end_delay + ) + + def end_delay(self, args): + """End startup delay.""" + self._cancel_listener = None + self._config_delay = 0 + def close(self): """Disconnect client.""" + if self._cancel_listener: + self._cancel_listener() + self._cancel_listener = None with self._lock: try: if self._client: @@ -230,6 +242,8 @@ class ModbusHub: def read_coils(self, unit, address, count): """Read coils.""" + if self._config_delay: + return None with self._lock: kwargs = {"unit": unit} if unit else {} try: @@ -245,6 +259,8 @@ class ModbusHub: def read_discrete_inputs(self, unit, address, count): """Read discrete inputs.""" + if self._config_delay: + return None with self._lock: kwargs = {"unit": unit} if unit else {} try: @@ -259,6 +275,8 @@ class ModbusHub: def read_input_registers(self, unit, address, count): """Read input registers.""" + if self._config_delay: + return None with self._lock: kwargs = {"unit": unit} if unit else {} try: @@ -273,6 +291,8 @@ class ModbusHub: def read_holding_registers(self, unit, address, count): """Read holding registers.""" + if self._config_delay: + return None with self._lock: kwargs = {"unit": unit} if unit else {} try: @@ -287,6 +307,8 @@ class ModbusHub: def write_coil(self, unit, address, value) -> bool: """Write coil.""" + if self._config_delay: + return False with self._lock: kwargs = {"unit": unit} if unit else {} try: @@ -301,6 +323,8 @@ class ModbusHub: def write_coils(self, unit, address, values) -> bool: """Write coil.""" + if self._config_delay: + return False with self._lock: kwargs = {"unit": unit} if unit else {} try: @@ -315,6 +339,8 @@ class ModbusHub: def write_register(self, unit, address, value) -> bool: """Write register.""" + if self._config_delay: + return False with self._lock: kwargs = {"unit": unit} if unit else {} try: @@ -329,6 +355,8 @@ class ModbusHub: def write_registers(self, unit, address, values) -> bool: """Write registers.""" + if self._config_delay: + return False with self._lock: kwargs = {"unit": unit} if unit else {} try: diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 1cabaeb11ff..99a8ba467f6 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -39,6 +39,7 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_TIMEOUT, CONF_TYPE, @@ -160,6 +161,12 @@ async def _config_helper(hass, do_config, caplog): CONF_TIMEOUT: 30, CONF_DELAY: 10, }, + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_DELAY: 5, + }, ], ) async def test_config_modbus(hass, caplog, do_config, mock_pymodbus): @@ -467,3 +474,116 @@ async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus): await hass.async_block_till_done() assert len(caplog.records) == 1 assert caplog.records[0].levelname == "ERROR" + + +async def test_delay(hass, mock_pymodbus): + """Run test for different read.""" + + # the purpose of this test is to test startup delay + # We "hijiack" binary_sensor and sensor in order + # to make a proper blackbox test. + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_NAME: TEST_MODBUS_NAME, + CONF_DELAY: 15, + CONF_BINARY_SENSORS: [ + { + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_NAME: f"{TEST_SENSOR_NAME}_2", + CONF_ADDRESS: 52, + CONF_SCAN_INTERVAL: 5, + }, + { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_NAME: f"{TEST_SENSOR_NAME}_1", + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 5, + }, + ], + CONF_SENSORS: [ + { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_NAME: f"{TEST_SENSOR_NAME}_3", + CONF_ADDRESS: 53, + CONF_SCAN_INTERVAL: 5, + }, + { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_NAME: f"{TEST_SENSOR_NAME}_4", + CONF_ADDRESS: 54, + CONF_SCAN_INTERVAL: 5, + }, + ], + } + ] + } + mock_pymodbus.read_coils.return_value = ReadResult([0x01]) + mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) + mock_pymodbus.read_holding_registers.return_value = ReadResult([7]) + mock_pymodbus.read_input_registers.return_value = ReadResult([7]) + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + + now = now + timedelta(seconds=10) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Check states + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_1" + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_2" + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_3" + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_4" + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + mock_pymodbus.reset_mock() + data = { + ATTR_HUB: TEST_MODBUS_NAME, + ATTR_UNIT: 17, + ATTR_ADDRESS: 16, + ATTR_STATE: False, + } + await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) + assert not mock_pymodbus.write_coil.called + await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) + assert not mock_pymodbus.write_coil.called + data[ATTR_STATE] = [True, False, True] + await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) + assert not mock_pymodbus.write_coils.called + + del data[ATTR_STATE] + data[ATTR_VALUE] = 15 + await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) + assert not mock_pymodbus.write_register.called + data[ATTR_VALUE] = [1, 2, 3] + await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) + assert not mock_pymodbus.write_registers.called + + # 2 times fire_changed is needed to secure "normal" update is called. + now = now + timedelta(seconds=6) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + now = now + timedelta(seconds=10) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Check states + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_1" + assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_2" + assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE + entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_3" + assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE + entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_4" + assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE From 7374b844d7a6c7b42fbd5bbc9b9b7171ae6db4c1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 8 May 2021 17:53:08 +0200 Subject: [PATCH 247/852] Update denonavr to version 0.10.7 (#50288) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 123eac5d2bf..ed6b94e207a 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.10.6"], + "requirements": ["denonavr==0.10.7"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 9ac6c8c1501..39305ff7ccb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -479,7 +479,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.10.6 +denonavr==0.10.7 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb9d2d7d033..97ee8a487e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -264,7 +264,7 @@ debugpy==1.2.1 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.10.6 +denonavr==0.10.7 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.3 From 97eb4c6c62e085d463e34bfda3c442144d444676 Mon Sep 17 00:00:00 2001 From: Gleb Sinyavskiy Date: Sat, 8 May 2021 19:12:14 +0200 Subject: [PATCH 248/852] Add syncthing integration (#38331) * Scaffold the integration * Add config flow data schema * Handle configuration errors * Get folder states * Support https * Fix translations * Listen to syncthing events in a separate thread * Bump syncthing * Automatically reconnect to the syncthing server * Renames * Improve loading and unloading * Update folder states from events * Refactoring, handle FolderPaused event * Dynamic folder icons * Refactoring * Mark folders as unavailable when senrver is unavailable * Update folder satus when server is available * Raise PlatformNotReady * Implement additional polling * Stop polling when the server is not available * Minor fixes * Remove logging * Check name uniqueness * Refactoring * Minor refactorings * Bump python-syncthing * Migrate to aiosyncthing * Minor fixes * Update .coveragerc * Set quality scale * Bump aiosyncthing, properly handle invalid token * Fix logging * Fix logging * Use CONF_VERIFY_SSL from homeassistant.const * Bump aiosyncthing. Add Syncthing device * Fix device name * Bump aiosyncthing * Bump aiosyncthing * Extract SyncthingClient * Add folder to device_state_attributes * Do not pass the loop * Cover config_flow.py * Move self.async_create_entry outside of the try block * Raise ConfigEntryNotReady if syncthing server is not reachable * Fix already configured error message * Change default name to Syncthing * Bump aiosyncthing * Fix formatting * Fix formatting * Fix tests * Fix typo, use lis comprehension * Fix typo, remove unused CONFIG_SCHEMA * Bump aiosyncthing * Remove periods from log messages W0001 * Fix tests * Black, isort * Remove empty items from manifest.json * Fix variable naming * Remove async_setup * Use SensorEntity * Use asyncio.create_task instead of self._hass.loop.create_task * Do not pass hass to FolderSensor initializer * Rename device_state_attributes to extra_state_attributes * Use callbacks * Simplify tests * Refactor _listen() * Use url for the title * Use the url instead of the name to identify the config entry * Explicitly set sensor attributes, extract _filter_state * Use server url instead of name in device_info * Use server url instead of name in logs * User server id as a device identifier * Use URL instead of name to identify config entry * Use shortened server id instead of name to build entity name and unique id * Do not use CONF_NAME * Cleanup unused strings * Cleanup unused strings * Add IOT class * Scaffold the integration * Add config flow data schema * Handle configuration errors * Get folder states * Support https * Fix translations * Listen to syncthing events in a separate thread * Bump syncthing * Automatically reconnect to the syncthing server * Renames * Improve loading and unloading * Update folder states from events * Refactoring, handle FolderPaused event * Dynamic folder icons * Refactoring * Mark folders as unavailable when senrver is unavailable * Update folder satus when server is available * Raise PlatformNotReady * Implement additional polling * Stop polling when the server is not available * Minor fixes * Remove logging * Check name uniqueness * Refactoring * Minor refactorings * Bump python-syncthing * Migrate to aiosyncthing * Minor fixes * Update .coveragerc * Set quality scale * Bump aiosyncthing, properly handle invalid token * Fix logging * Fix logging * Use CONF_VERIFY_SSL from homeassistant.const * Bump aiosyncthing. Add Syncthing device * Fix device name * Bump aiosyncthing * Bump aiosyncthing * Extract SyncthingClient * Add folder to device_state_attributes * Do not pass the loop * Cover config_flow.py * Move self.async_create_entry outside of the try block * Raise ConfigEntryNotReady if syncthing server is not reachable * Fix already configured error message * Change default name to Syncthing * Bump aiosyncthing * Fix formatting * Fix formatting * Fix tests * Fix typo, use lis comprehension * Fix typo, remove unused CONFIG_SCHEMA * Bump aiosyncthing * Remove periods from log messages W0001 * Fix tests * Black, isort * Remove empty items from manifest.json * Fix variable naming * Remove async_setup * Use SensorEntity * Use asyncio.create_task instead of self._hass.loop.create_task * Do not pass hass to FolderSensor initializer * Rename device_state_attributes to extra_state_attributes * Use callbacks * Simplify tests * Refactor _listen() * Use url for the title * Use the url instead of the name to identify the config entry * Explicitly set sensor attributes, extract _filter_state * Use server url instead of name in device_info * Use server url instead of name in logs * User server id as a device identifier * Use URL instead of name to identify config entry * Use shortened server id instead of name to build entity name and unique id * Do not use CONF_NAME * Cleanup unused strings * Cleanup unused strings * Add IOT class * Apply suggestions from code review * Clean up * Fix dict comprehension * Clean sensor * Use the server ID as a config entry unique ID * Remove the AlreadyConfigured exception * Clean up old error string * Format json * Convert sensor attributes to snake case * Force CI Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + CODEOWNERS | 1 + .../components/syncthing/__init__.py | 172 +++++++++++ .../components/syncthing/config_flow.py | 72 +++++ homeassistant/components/syncthing/const.py | 33 +++ .../components/syncthing/manifest.json | 12 + homeassistant/components/syncthing/sensor.py | 268 ++++++++++++++++++ .../components/syncthing/strings.json | 22 ++ .../components/syncthing/translations/en.json | 22 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/syncthing/__init__.py | 1 + .../components/syncthing/test_config_flow.py | 106 +++++++ 14 files changed, 718 insertions(+) create mode 100644 homeassistant/components/syncthing/__init__.py create mode 100644 homeassistant/components/syncthing/config_flow.py create mode 100644 homeassistant/components/syncthing/const.py create mode 100644 homeassistant/components/syncthing/manifest.json create mode 100644 homeassistant/components/syncthing/sensor.py create mode 100644 homeassistant/components/syncthing/strings.json create mode 100644 homeassistant/components/syncthing/translations/en.json create mode 100644 tests/components/syncthing/__init__.py create mode 100644 tests/components/syncthing/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f86ea86d2d1..1f532727427 100644 --- a/.coveragerc +++ b/.coveragerc @@ -967,6 +967,8 @@ omit = homeassistant/components/switchbot/switch.py homeassistant/components/switcher_kis/switch.py homeassistant/components/switchmate/switch.py + homeassistant/components/syncthing/__init__.py + homeassistant/components/syncthing/sensor.py homeassistant/components/syncthru/__init__.py homeassistant/components/syncthru/binary_sensor.py homeassistant/components/syncthru/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index eb46da1353d..c2824fb33b6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -478,6 +478,7 @@ homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/switchbot/* @danielhiversen homeassistant/components/switcher_kis/* @tomerfi homeassistant/components/switchmate/* @danielhiversen +homeassistant/components/syncthing/* @zhulik homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185 homeassistant/components/synology_srm/* @aerialls diff --git a/homeassistant/components/syncthing/__init__.py b/homeassistant/components/syncthing/__init__.py new file mode 100644 index 00000000000..d7cc671465a --- /dev/null +++ b/homeassistant/components/syncthing/__init__.py @@ -0,0 +1,172 @@ +"""The syncthing integration.""" +import asyncio +import logging + +import aiosyncthing + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_TOKEN, + CONF_URL, + CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + DOMAIN, + EVENTS, + RECONNECT_INTERVAL, + SERVER_AVAILABLE, + SERVER_UNAVAILABLE, +) + +PLATFORMS = ["sensor"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up syncthing from a config entry.""" + data = entry.data + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + client = aiosyncthing.Syncthing( + data[CONF_TOKEN], + url=data[CONF_URL], + verify_ssl=data[CONF_VERIFY_SSL], + ) + + try: + status = await client.system.status() + except aiosyncthing.exceptions.SyncthingError as exception: + await client.close() + raise ConfigEntryNotReady from exception + + server_id = status["myID"] + + syncthing = SyncthingClient(hass, client, server_id) + syncthing.subscribe() + hass.data[DOMAIN][entry.entry_id] = syncthing + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + async def cancel_listen_task(_): + await syncthing.unsubscribe() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cancel_listen_task) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + syncthing = hass.data[DOMAIN].pop(entry.entry_id) + await syncthing.unsubscribe() + + return unload_ok + + +class SyncthingClient: + """A Syncthing client.""" + + def __init__(self, hass, client, server_id): + """Initialize the client.""" + self._hass = hass + self._client = client + self._server_id = server_id + self._listen_task = None + + @property + def server_id(self): + """Get server id.""" + return self._server_id + + @property + def url(self): + """Get server URL.""" + return self._client.url + + @property + def database(self): + """Get database namespace client.""" + return self._client.database + + @property + def system(self): + """Get system namespace client.""" + return self._client.system + + def subscribe(self): + """Start event listener coroutine.""" + self._listen_task = asyncio.create_task(self._listen()) + + async def unsubscribe(self): + """Stop event listener coroutine.""" + if self._listen_task: + self._listen_task.cancel() + await self._client.close() + + async def _listen(self): + """Listen to Syncthing events.""" + events = self._client.events + server_was_unavailable = False + while True: + if await self._server_available(): + if server_was_unavailable: + _LOGGER.info( + "The syncthing server '%s' is back online", self._client.url + ) + async_dispatcher_send( + self._hass, f"{SERVER_AVAILABLE}-{self._server_id}" + ) + server_was_unavailable = False + else: + await asyncio.sleep(RECONNECT_INTERVAL.total_seconds()) + continue + try: + async for event in events.listen(): + if events.last_seen_id == 0: + continue # skipping historical events from the first batch + if event["type"] not in EVENTS: + continue + + signal_name = EVENTS[event["type"]] + folder = None + if "folder" in event["data"]: + folder = event["data"]["folder"] + else: # A workaround, some events store folder id under `id` key + folder = event["data"]["id"] + async_dispatcher_send( + self._hass, + f"{signal_name}-{self._server_id}-{folder}", + event, + ) + except aiosyncthing.exceptions.SyncthingError: + _LOGGER.info( + "The syncthing server '%s' is not available. Sleeping %i seconds and retrying", + self._client.url, + RECONNECT_INTERVAL.total_seconds(), + ) + async_dispatcher_send( + self._hass, f"{SERVER_UNAVAILABLE}-{self._server_id}" + ) + await asyncio.sleep(RECONNECT_INTERVAL.total_seconds()) + server_was_unavailable = True + continue + + async def _server_available(self): + try: + await self._client.system.ping() + except aiosyncthing.exceptions.SyncthingError: + return False + else: + return True diff --git a/homeassistant/components/syncthing/config_flow.py b/homeassistant/components/syncthing/config_flow.py new file mode 100644 index 00000000000..e6a5c994834 --- /dev/null +++ b/homeassistant/components/syncthing/config_flow.py @@ -0,0 +1,72 @@ +"""Config flow for syncthing integration.""" +import logging + +import aiosyncthing +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL + +from .const import DEFAULT_URL, DEFAULT_VERIFY_SSL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_URL, default=DEFAULT_URL): str, + vol.Required(CONF_TOKEN): str, + vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + + try: + async with aiosyncthing.Syncthing( + data[CONF_TOKEN], + url=data[CONF_URL], + verify_ssl=data[CONF_VERIFY_SSL], + loop=hass.loop, + ) as client: + server_id = (await client.system.status())["myID"] + return {"title": f"{data[CONF_URL]}", "server_id": server_id} + except aiosyncthing.exceptions.UnauthorizedError as error: + raise InvalidAuth from error + except Exception as error: + raise CannotConnect from error + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for syncthing.""" + + VERSION = 1 + + 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) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors[CONF_TOKEN] = "invalid_auth" + else: + await self.async_set_unique_id(info["server_id"]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["title"], data=user_input) + + 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/homeassistant/components/syncthing/const.py b/homeassistant/components/syncthing/const.py new file mode 100644 index 00000000000..a9ec0ad0375 --- /dev/null +++ b/homeassistant/components/syncthing/const.py @@ -0,0 +1,33 @@ +"""Constants for the syncthing integration.""" +from datetime import timedelta + +DOMAIN = "syncthing" + +DEFAULT_VERIFY_SSL = True +DEFAULT_URL = "http://127.0.0.1:8384" + +RECONNECT_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=120) + +FOLDER_SUMMARY_RECEIVED = "syncthing_folder_summary_received" +FOLDER_PAUSED_RECEIVED = "syncthing_folder_paused_received" +SERVER_UNAVAILABLE = "syncthing_server_unavailable" +SERVER_AVAILABLE = "syncthing_server_available" +STATE_CHANGED_RECEIVED = "syncthing_state_changed_received" + +EVENTS = { + "FolderSummary": FOLDER_SUMMARY_RECEIVED, + "StateChanged": STATE_CHANGED_RECEIVED, + "FolderPaused": FOLDER_PAUSED_RECEIVED, +} + + +FOLDER_SENSOR_ICONS = { + "paused": "mdi:folder-clock", + "scanning": "mdi:folder-search", + "syncing": "mdi:folder-sync", + "idle": "mdi:folder", +} + +FOLDER_SENSOR_ALERT_ICON = "mdi:folder-alert" +FOLDER_SENSOR_DEFAULT_ICON = "mdi:folder" diff --git a/homeassistant/components/syncthing/manifest.json b/homeassistant/components/syncthing/manifest.json new file mode 100644 index 00000000000..cd779e1657b --- /dev/null +++ b/homeassistant/components/syncthing/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "syncthing", + "name": "Syncthing", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/syncthing", + "requirements": ["aiosyncthing==0.5.1"], + "codeowners": [ + "@zhulik" + ], + "quality_scale": "silver", + "iot_class": "local_polling" +} diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py new file mode 100644 index 00000000000..5e8ea2f88c2 --- /dev/null +++ b/homeassistant/components/syncthing/sensor.py @@ -0,0 +1,268 @@ +"""Support for monitoring the Syncthing instance.""" + +import logging + +import aiosyncthing + +from homeassistant.components.sensor import SensorEntity +from homeassistant.core import callback +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + DOMAIN, + FOLDER_PAUSED_RECEIVED, + FOLDER_SENSOR_ALERT_ICON, + FOLDER_SENSOR_DEFAULT_ICON, + FOLDER_SENSOR_ICONS, + FOLDER_SUMMARY_RECEIVED, + SCAN_INTERVAL, + SERVER_AVAILABLE, + SERVER_UNAVAILABLE, + STATE_CHANGED_RECEIVED, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Syncthing sensors.""" + syncthing = hass.data[DOMAIN][config_entry.entry_id] + + try: + config = await syncthing.system.config() + version = await syncthing.system.version() + except aiosyncthing.exceptions.SyncthingError as exception: + raise PlatformNotReady from exception + + server_id = syncthing.server_id + entities = [ + FolderSensor( + syncthing, + server_id, + folder["id"], + folder["label"], + version["version"], + ) + for folder in config["folders"] + ] + + async_add_entities(entities) + + +class FolderSensor(SensorEntity): + """A Syncthing folder sensor.""" + + STATE_ATTRIBUTES = { + "errors": "errors", + "globalBytes": "global_bytes", + "globalDeleted": "global_deleted", + "globalDirectories": "global_directories", + "globalFiles": "global_files", + "globalSymlinks": "global_symlinks", + "globalTotalItems": "global_total_items", + "ignorePatterns": "ignore_patterns", + "inSyncBytes": "in_sync_bytes", + "inSyncFiles": "in_sync_files", + "invalid": "invalid", + "localBytes": "local_bytes", + "localDeleted": "local_deleted", + "localDirectories": "local_directories", + "localFiles": "local_files", + "localSymlinks": "local_symlinks", + "localTotalItems": "local_total_items", + "needBytes": "need_bytes", + "needDeletes": "need_deletes", + "needDirectories": "need_directories", + "needFiles": "need_files", + "needSymlinks": "need_symlinks", + "needTotalItems": "need_total_items", + "pullErrors": "pull_errors", + "state": "state", + } + + def __init__(self, syncthing, server_id, folder_id, folder_label, version): + """Initialize the sensor.""" + self._syncthing = syncthing + self._server_id = server_id + self._folder_id = folder_id + self._folder_label = folder_label + self._state = None + self._unsub_timer = None + self._version = version + + self._short_server_id = server_id.split("-")[0] + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._short_server_id} {self._folder_id} {self._folder_label}" + + @property + def unique_id(self): + """Return the unique id of the entity.""" + return f"{self._short_server_id}-{self._folder_id}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state["state"] + + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._state is not None + + @property + def icon(self): + """Return the icon for this sensor.""" + if self._state is None: + return FOLDER_SENSOR_DEFAULT_ICON + if self.state in FOLDER_SENSOR_ICONS: + return FOLDER_SENSOR_ICONS[self.state] + return FOLDER_SENSOR_ALERT_ICON + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return self._state + + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + @property + def device_info(self): + """Return device information.""" + return { + "identifiers": {(DOMAIN, self._server_id)}, + "name": f"Syncthing ({self._syncthing.url})", + "manufacturer": "Syncthing Team", + "sw_version": self._version, + "entry_type": "service", + } + + async def async_update_status(self): + """Request folder status and update state.""" + try: + state = await self._syncthing.database.status(self._folder_id) + except aiosyncthing.exceptions.SyncthingError: + self._state = None + else: + self._state = self._filter_state(state) + self.async_write_ha_state() + + def subscribe(self): + """Start polling syncthing folder status.""" + if self._unsub_timer is None: + + async def refresh(event_time): + """Get the latest data from Syncthing.""" + await self.async_update_status() + + self._unsub_timer = async_track_time_interval( + self.hass, refresh, SCAN_INTERVAL + ) + + @callback + def unsubscribe(self): + """Stop polling syncthing folder status.""" + if self._unsub_timer is not None: + self._unsub_timer() + self._unsub_timer = None + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + + @callback + def handle_folder_summary(event): + if self._state is not None: + self._state = self._filter_state(event["data"]["summary"]) + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{FOLDER_SUMMARY_RECEIVED}-{self._server_id}-{self._folder_id}", + handle_folder_summary, + ) + ) + + @callback + def handle_state_changed(event): + if self._state is not None: + self._state["state"] = event["data"]["to"] + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{STATE_CHANGED_RECEIVED}-{self._server_id}-{self._folder_id}", + handle_state_changed, + ) + ) + + @callback + def handle_folder_paused(event): + if self._state is not None: + self._state["state"] = "paused" + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{FOLDER_PAUSED_RECEIVED}-{self._server_id}-{self._folder_id}", + handle_folder_paused, + ) + ) + + @callback + def handle_server_unavailable(): + self._state = None + self.unsubscribe() + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SERVER_UNAVAILABLE}-{self._server_id}", + handle_server_unavailable, + ) + ) + + async def handle_server_available(): + self.subscribe() + await self.async_update_status() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SERVER_AVAILABLE}-{self._server_id}", + handle_server_available, + ) + ) + + self.subscribe() + self.async_on_remove(self.unsubscribe) + + await self.async_update_status() + + def _filter_state(self, state): + # Select only needed state attributes and map their names + state = { + self.STATE_ATTRIBUTES[key]: value + for key, value in state.items() + if key in self.STATE_ATTRIBUTES + } + + # A workaround, for some reason, state of paused folders is an empty string + if state["state"] == "": + state["state"] = "paused" + + # Add some useful attributes + state["id"] = self._folder_id + state["label"] = self._folder_label + + return state diff --git a/homeassistant/components/syncthing/strings.json b/homeassistant/components/syncthing/strings.json new file mode 100644 index 00000000000..1781df56f1e --- /dev/null +++ b/homeassistant/components/syncthing/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Syncthing", + "config": { + "step": { + "user": { + "data": { + "title": "Setup Syncthing integration", + "url": "[%key:common::config_flow::data::url%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", + "token": "Token" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant/components/syncthing/translations/en.json b/homeassistant/components/syncthing/translations/en.json new file mode 100644 index 00000000000..00c73bedb9e --- /dev/null +++ b/homeassistant/components/syncthing/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "Unable to connect to the Syncthing server.", + "invalid_auth": "Invalid authentication" + }, + "abort": { + "already_configured": "Service is already configured" + }, + "step": { + "user": { + "title": "Setup Syncthing integration", + "data": { + "url": "URL", + "token": "Token", + "verify_ssl": "Verify SSL" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6adcb16cc15..bb346ff5b0f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -239,6 +239,7 @@ FLOWS = [ "srp_energy", "starline", "subaru", + "syncthing", "syncthru", "synology_dsm", "system_bridge", diff --git a/requirements_all.txt b/requirements_all.txt index 39305ff7ccb..481763adcc7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -232,6 +232,9 @@ aioshelly==0.6.2 # homeassistant.components.switcher_kis aioswitcher==1.2.1 +# homeassistant.components.syncthing +aiosyncthing==0.5.1 + # homeassistant.components.unifi aiounifi==26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97ee8a487e1..883ece77c4e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -154,6 +154,9 @@ aioshelly==0.6.2 # homeassistant.components.switcher_kis aioswitcher==1.2.1 +# homeassistant.components.syncthing +aiosyncthing==0.5.1 + # homeassistant.components.unifi aiounifi==26 diff --git a/tests/components/syncthing/__init__.py b/tests/components/syncthing/__init__.py new file mode 100644 index 00000000000..8a4f28832ea --- /dev/null +++ b/tests/components/syncthing/__init__.py @@ -0,0 +1 @@ +"""Tests for the syncthing integration.""" diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py new file mode 100644 index 00000000000..30f8bc0386b --- /dev/null +++ b/tests/components/syncthing/test_config_flow.py @@ -0,0 +1,106 @@ +"""Tests for syncthing config flow.""" + +from unittest.mock import patch + +from aiosyncthing.exceptions import UnauthorizedError + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.syncthing.const import DOMAIN +from homeassistant.const import CONF_NAME, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL + +from tests.common import MockConfigEntry + +NAME = "Syncthing" +URL = "http://127.0.0.1:8384" +TOKEN = "token" +VERIFY_SSL = True + +MOCK_ENTRY = { + CONF_NAME: NAME, + CONF_URL: URL, + CONF_TOKEN: TOKEN, + CONF_VERIFY_SSL: VERIFY_SSL, +} + + +async def test_show_setup_form(hass): + """Test that the setup form is served.""" + 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"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + assert result["step_id"] == "user" + + +async def test_flow_successfull(hass): + """Test with required fields only.""" + with patch( + "aiosyncthing.system.System.status", return_value={"myID": "server-id"} + ), patch( + "homeassistant.components.syncthing.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + data={ + CONF_NAME: NAME, + CONF_URL: URL, + CONF_TOKEN: TOKEN, + CONF_VERIFY_SSL: VERIFY_SSL, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "http://127.0.0.1:8384" + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_URL] == URL + assert result["data"][CONF_TOKEN] == TOKEN + assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_already_configured(hass): + """Test name is already configured.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY, unique_id="server-id") + entry.add_to_hass(hass) + + with patch("aiosyncthing.system.System.status", return_value={"myID": "server-id"}): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + data=MOCK_ENTRY, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_invalid_auth(hass): + """Test invalid auth.""" + + with patch("aiosyncthing.system.System.status", side_effect=UnauthorizedError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + data=MOCK_ENTRY, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"]["token"] == "invalid_auth" + + +async def test_flow_cannot_connect(hass): + """Test cannot connect.""" + + with patch("aiosyncthing.system.System.status", side_effect=Exception): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + data=MOCK_ENTRY, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"]["base"] == "cannot_connect" From 4853fb7966554dece917d6cc59d78c0c495d42b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 May 2021 12:20:22 -0500 Subject: [PATCH 249/852] Fix tplink unloading when no switches are present (#50301) --- homeassistant/components/tplink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index e68c30f48b5..f424f90d6d3 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -111,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigType): async def async_unload_entry(hass, entry): """Unload a config entry.""" - platforms = [platform for platform in PLATFORMS if platform in hass.data[DOMAIN]] + platforms = [platform for platform in PLATFORMS if hass.data[DOMAIN].get(platform)] unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) if unload_ok: hass.data[DOMAIN].clear() From 61b0e66405969811754cbc4ab6c090cd38fdf933 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 8 May 2021 19:37:09 +0200 Subject: [PATCH 250/852] Fix ESPHome timestamp sensor (#50305) --- homeassistant/components/esphome/sensor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index ceb391f6bda..12319be8c40 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -6,10 +6,15 @@ import math from aioesphomeapi import SensorInfo, SensorState, TextSensorInfo, TextSensorState import voluptuous as vol -from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity +from homeassistant.components.sensor import ( + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASSES, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.util import dt from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry @@ -74,6 +79,8 @@ class EsphomeSensor(EsphomeEntity, SensorEntity): return None if self._state.missing_state: return None + if self.device_class == DEVICE_CLASS_TIMESTAMP: + return dt.utc_from_timestamp(self._state.state).isoformat() return f"{self._state.state:.{self._static_info.accuracy_decimals}f}" @property From 10afbe4279e17ab63dd9f6d7ba2c2c9c71932dc9 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 8 May 2021 20:33:55 +0200 Subject: [PATCH 251/852] Bump ha-philipsjs to 2.7.3 (#50293) --- homeassistant/components/philips_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index d41ac0881ba..9d1c4dbd04d 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -2,7 +2,7 @@ "domain": "philips_js", "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": ["ha-philipsjs==2.7.0"], + "requirements": ["ha-philipsjs==2.7.3"], "codeowners": ["@elupus"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 481763adcc7..aeafcf74f35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -720,7 +720,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.7.0 +ha-philipsjs==2.7.3 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 883ece77c4e..7044c838c83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -393,7 +393,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.7.0 +ha-philipsjs==2.7.3 # homeassistant.components.habitica habitipy==0.2.0 From 52b1a416d97aea84505ecd8e04311e6a17f297db Mon Sep 17 00:00:00 2001 From: Gleb Sinyavskiy Date: Sun, 9 May 2021 00:58:23 +0200 Subject: [PATCH 252/852] Remove the N26 integration (#50292) --- .coveragerc | 1 - homeassistant/components/n26/__init__.py | 164 -------------- homeassistant/components/n26/const.py | 7 - homeassistant/components/n26/manifest.json | 8 - homeassistant/components/n26/sensor.py | 244 --------------------- homeassistant/components/n26/switch.py | 61 ------ mypy.ini | 3 - requirements_all.txt | 3 - script/hassfest/mypy_config.py | 1 - 9 files changed, 492 deletions(-) delete mode 100644 homeassistant/components/n26/__init__.py delete mode 100644 homeassistant/components/n26/const.py delete mode 100644 homeassistant/components/n26/manifest.json delete mode 100644 homeassistant/components/n26/sensor.py delete mode 100644 homeassistant/components/n26/switch.py diff --git a/.coveragerc b/.coveragerc index 1f532727427..95d699be69f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -658,7 +658,6 @@ omit = homeassistant/components/mystrom/light.py homeassistant/components/mystrom/switch.py homeassistant/components/myq/__init__.py - homeassistant/components/n26/* homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py diff --git a/homeassistant/components/n26/__init__.py b/homeassistant/components/n26/__init__.py deleted file mode 100644 index b1e83cd5311..00000000000 --- a/homeassistant/components/n26/__init__.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Support for N26 bank accounts.""" -from datetime import datetime, timedelta, timezone -import logging - -from n26 import api as n26_api, config as n26_config -from requests import HTTPError -import voluptuous as vol - -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.util import Throttle - -from .const import DATA, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) - -# define configuration parameters -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ], - ) - }, - extra=vol.ALLOW_EXTRA, -) - -PLATFORMS = ["sensor", "switch"] - - -def setup(hass, config): - """Set up N26 Component.""" - acc_list = config[DOMAIN] - - api_data_list = [] - - for acc in acc_list: - user = acc[CONF_USERNAME] - password = acc[CONF_PASSWORD] - - api = n26_api.Api(n26_config.Config(user, password)) - - try: - api.get_token() - except HTTPError as err: - _LOGGER.error(str(err)) - return False - - api_data = N26Data(api) - api_data.update() - - api_data_list.append(api_data) - - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA] = api_data_list - - # Load platforms for supported devices - for platform in PLATFORMS: - load_platform(hass, platform, DOMAIN, {}, config) - - return True - - -def timestamp_ms_to_date(epoch_ms) -> datetime or None: - """Convert millisecond timestamp to datetime.""" - if epoch_ms: - return datetime.fromtimestamp(epoch_ms / 1000, timezone.utc) - - -class N26Data: - """Handle N26 API object and limit updates.""" - - def __init__(self, api): - """Initialize the data object.""" - self._api = api - - self._account_info = {} - self._balance = {} - self._limits = {} - self._account_statuses = {} - - self._cards = {} - self._spaces = {} - - @property - def api(self): - """Return N26 api client.""" - return self._api - - @property - def account_info(self): - """Return N26 account info.""" - return self._account_info - - @property - def balance(self): - """Return N26 account balance.""" - return self._balance - - @property - def limits(self): - """Return N26 account limits.""" - return self._limits - - @property - def account_statuses(self): - """Return N26 account statuses.""" - return self._account_statuses - - @property - def cards(self): - """Return N26 cards.""" - return self._cards - - def card(self, card_id: str, default: dict = None): - """Return a card by its id or the given default.""" - return next((card for card in self.cards if card["id"] == card_id), default) - - @property - def spaces(self): - """Return N26 spaces.""" - return self._spaces - - def space(self, space_id: str, default: dict = None): - """Return a space by its id or the given default.""" - return next( - (space for space in self.spaces["spaces"] if space["id"] == space_id), - default, - ) - - @Throttle(min_time=DEFAULT_SCAN_INTERVAL * 0.8) - def update_account(self): - """Get the latest account data from N26.""" - self._account_info = self._api.get_account_info() - self._balance = self._api.get_balance() - self._limits = self._api.get_account_limits() - self._account_statuses = self._api.get_account_statuses() - - @Throttle(min_time=DEFAULT_SCAN_INTERVAL * 0.8) - def update_cards(self): - """Get the latest cards data from N26.""" - self._cards = self._api.get_cards() - - @Throttle(min_time=DEFAULT_SCAN_INTERVAL * 0.8) - def update_spaces(self): - """Get the latest spaces data from N26.""" - self._spaces = self._api.get_spaces() - - def update(self): - """Get the latest data from N26.""" - self.update_account() - self.update_cards() - self.update_spaces() diff --git a/homeassistant/components/n26/const.py b/homeassistant/components/n26/const.py deleted file mode 100644 index 0a640d0f34e..00000000000 --- a/homeassistant/components/n26/const.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Provides the constants needed for component.""" -DOMAIN = "n26" - -DATA = "data" - -CARD_STATE_ACTIVE = "M_ACTIVE" -CARD_STATE_BLOCKED = "M_DISABLED" diff --git a/homeassistant/components/n26/manifest.json b/homeassistant/components/n26/manifest.json deleted file mode 100644 index a73f4742fae..00000000000 --- a/homeassistant/components/n26/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "n26", - "name": "N26", - "documentation": "https://www.home-assistant.io/integrations/n26", - "requirements": ["n26==0.2.7"], - "codeowners": [], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/n26/sensor.py b/homeassistant/components/n26/sensor.py deleted file mode 100644 index 98d86194b86..00000000000 --- a/homeassistant/components/n26/sensor.py +++ /dev/null @@ -1,244 +0,0 @@ -"""Support for N26 bank account sensors.""" -from homeassistant.components.sensor import SensorEntity - -from . import DEFAULT_SCAN_INTERVAL, DOMAIN, timestamp_ms_to_date -from .const import DATA - -SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL - -ATTR_IBAN = "account" -ATTR_USABLE_BALANCE = "usable_balance" -ATTR_BANK_BALANCE = "bank_balance" - -ATTR_ACC_OWNER_TITLE = "owner_title" -ATTR_ACC_OWNER_FIRST_NAME = "owner_first_name" -ATTR_ACC_OWNER_LAST_NAME = "owner_last_name" -ATTR_ACC_OWNER_GENDER = "owner_gender" -ATTR_ACC_OWNER_BIRTH_DATE = "owner_birth_date" -ATTR_ACC_OWNER_EMAIL = "owner_email" -ATTR_ACC_OWNER_PHONE_NUMBER = "owner_phone_number" - -ICON_ACCOUNT = "mdi:currency-eur" -ICON_CARD = "mdi:credit-card" -ICON_SPACE = "mdi:crop-square" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the N26 sensor platform.""" - if discovery_info is None: - return - - api_list = hass.data[DOMAIN][DATA] - - sensor_entities = [] - for api_data in api_list: - sensor_entities.append(N26Account(api_data)) - - for card in api_data.cards: - sensor_entities.append(N26Card(api_data, card)) - - for space in api_data.spaces["spaces"]: - sensor_entities.append(N26Space(api_data, space)) - - add_entities(sensor_entities) - - -class N26Account(SensorEntity): - """Sensor for a N26 balance account. - - A balance account contains an amount of money (=balance). The amount may - also be negative. - """ - - def __init__(self, api_data) -> None: - """Initialize a N26 balance account.""" - self._data = api_data - self._iban = self._data.balance["iban"] - - def update(self) -> None: - """Get the current balance and currency for the account.""" - self._data.update_account() - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._iban[-4:] - - @property - def name(self) -> str: - """Friendly name of the sensor.""" - return f"n26_{self._iban[-4:]}" - - @property - def state(self) -> float: - """Return the balance of the account as state.""" - if self._data.balance is None: - return None - - return self._data.balance.get("availableBalance") - - @property - def unit_of_measurement(self) -> str: - """Use the currency as unit of measurement.""" - if self._data.balance is None: - return None - - return self._data.balance.get("currency") - - @property - def extra_state_attributes(self) -> dict: - """Additional attributes of the sensor.""" - attributes = { - ATTR_IBAN: self._data.balance.get("iban"), - ATTR_BANK_BALANCE: self._data.balance.get("bankBalance"), - ATTR_USABLE_BALANCE: self._data.balance.get("usableBalance"), - ATTR_ACC_OWNER_TITLE: self._data.account_info.get("title"), - ATTR_ACC_OWNER_FIRST_NAME: self._data.account_info.get("kycFirstName"), - ATTR_ACC_OWNER_LAST_NAME: self._data.account_info.get("kycLastName"), - ATTR_ACC_OWNER_GENDER: self._data.account_info.get("gender"), - ATTR_ACC_OWNER_BIRTH_DATE: timestamp_ms_to_date( - self._data.account_info.get("birthDate") - ), - ATTR_ACC_OWNER_EMAIL: self._data.account_info.get("email"), - ATTR_ACC_OWNER_PHONE_NUMBER: self._data.account_info.get( - "mobilePhoneNumber" - ), - } - - for limit in self._data.limits: - limit_attr_name = f"limit_{limit['limit'].lower()}" - attributes[limit_attr_name] = limit["amount"] - - return attributes - - @property - def icon(self) -> str: - """Set the icon for the sensor.""" - return ICON_ACCOUNT - - -class N26Card(SensorEntity): - """Sensor for a N26 card.""" - - def __init__(self, api_data, card) -> None: - """Initialize a N26 card.""" - self._data = api_data - self._account_name = api_data.balance["iban"][-4:] - self._card = card - - def update(self) -> None: - """Get the current balance and currency for the account.""" - self._data.update_cards() - self._card = self._data.card(self._card["id"], self._card) - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._card["id"] - - @property - def name(self) -> str: - """Friendly name of the sensor.""" - return f"{self._account_name.lower()}_card_{self._card['id']}" - - @property - def state(self) -> float: - """Return the balance of the account as state.""" - return self._card["status"] - - @property - def extra_state_attributes(self) -> dict: - """Additional attributes of the sensor.""" - attributes = { - "apple_pay_eligible": self._card.get("applePayEligible"), - "card_activated": timestamp_ms_to_date(self._card.get("cardActivated")), - "card_product": self._card.get("cardProduct"), - "card_product_type": self._card.get("cardProductType"), - "card_settings_id": self._card.get("cardSettingsId"), - "card_Type": self._card.get("cardType"), - "design": self._card.get("design"), - "exceet_actual_delivery_date": self._card.get("exceetActualDeliveryDate"), - "exceet_card_status": self._card.get("exceetCardStatus"), - "exceet_expected_delivery_date": self._card.get( - "exceetExpectedDeliveryDate" - ), - "exceet_express_card_delivery": self._card.get("exceetExpressCardDelivery"), - "exceet_express_card_delivery_email_sent": self._card.get( - "exceetExpressCardDeliveryEmailSent" - ), - "exceet_express_card_delivery_tracking_id": self._card.get( - "exceetExpressCardDeliveryTrackingId" - ), - "expiration_date": timestamp_ms_to_date(self._card.get("expirationDate")), - "google_pay_eligible": self._card.get("googlePayEligible"), - "masked_pan": self._card.get("maskedPan"), - "membership": self._card.get("membership"), - "mpts_card": self._card.get("mptsCard"), - "pan": self._card.get("pan"), - "pin_defined": timestamp_ms_to_date(self._card.get("pinDefined")), - "username_on_card": self._card.get("usernameOnCard"), - } - return attributes - - @property - def icon(self) -> str: - """Set the icon for the sensor.""" - return ICON_CARD - - -class N26Space(SensorEntity): - """Sensor for a N26 space.""" - - def __init__(self, api_data, space) -> None: - """Initialize a N26 space.""" - self._data = api_data - self._space = space - - def update(self) -> None: - """Get the current balance and currency for the account.""" - self._data.update_spaces() - self._space = self._data.space(self._space["id"], self._space) - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return f"space_{self._data.balance['iban'][-4:]}_{self._space['name'].lower()}" - - @property - def name(self) -> str: - """Friendly name of the sensor.""" - return self._space["name"] - - @property - def state(self) -> float: - """Return the balance of the account as state.""" - return self._space["balance"]["availableBalance"] - - @property - def unit_of_measurement(self) -> str: - """Use the currency as unit of measurement.""" - return self._space["balance"]["currency"] - - @property - def extra_state_attributes(self) -> dict: - """Additional attributes of the sensor.""" - goal_value = "" - if "goal" in self._space: - goal_value = self._space.get("goal").get("amount") - - attributes = { - "name": self._space.get("name"), - "goal": goal_value, - "background_image_url": self._space.get("backgroundImageUrl"), - "image_url": self._space.get("imageUrl"), - "is_card_attached": self._space.get("isCardAttached"), - "is_hidden_from_balance": self._space.get("isHiddenFromBalance"), - "is_locked": self._space.get("isLocked"), - "is_primary": self._space.get("isPrimary"), - } - return attributes - - @property - def icon(self) -> str: - """Set the icon for the sensor.""" - return ICON_SPACE diff --git a/homeassistant/components/n26/switch.py b/homeassistant/components/n26/switch.py deleted file mode 100644 index 910aa96ca49..00000000000 --- a/homeassistant/components/n26/switch.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Support for N26 switches.""" -from homeassistant.components.switch import SwitchEntity - -from . import DEFAULT_SCAN_INTERVAL, DOMAIN -from .const import CARD_STATE_ACTIVE, CARD_STATE_BLOCKED, DATA - -SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the N26 switch platform.""" - if discovery_info is None: - return - - api_list = hass.data[DOMAIN][DATA] - - switch_entities = [] - for api_data in api_list: - for card in api_data.cards: - switch_entities.append(N26CardSwitch(api_data, card)) - - add_entities(switch_entities) - - -class N26CardSwitch(SwitchEntity): - """Representation of a N26 card block/unblock switch.""" - - def __init__(self, api_data, card: dict): - """Initialize the N26 card block/unblock switch.""" - self._data = api_data - self._card = card - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._card["id"] - - @property - def name(self) -> str: - """Friendly name of the sensor.""" - return f"card_{self._card['id']}" - - @property - def is_on(self): - """Return true if switch is on.""" - return self._card["status"] == CARD_STATE_ACTIVE - - def turn_on(self, **kwargs): - """Block the card.""" - self._data.api.unblock_card(self._card["id"]) - self._card["status"] = CARD_STATE_ACTIVE - - def turn_off(self, **kwargs): - """Unblock the card.""" - self._data.api.block_card(self._card["id"]) - self._card["status"] = CARD_STATE_BLOCKED - - def update(self): - """Update the switch state.""" - self._data.update_cards() - self._card = self._data.card(self._card["id"], self._card) diff --git a/mypy.ini b/mypy.ini index 7637ffc4d6a..4624aab9dd8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1028,9 +1028,6 @@ ignore_errors = true [mypy-homeassistant.components.mysensors.*] ignore_errors = true -[mypy-homeassistant.components.n26.*] -ignore_errors = true - [mypy-homeassistant.components.neato.*] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index aeafcf74f35..63f7d857f89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -971,9 +971,6 @@ mychevy==2.1.1 # homeassistant.components.mycroft mycroftapi==2.0 -# homeassistant.components.n26 -n26==0.2.7 - # homeassistant.components.nad nad_receiver==0.0.12 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f7472d791f3..f041ba03c5b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -137,7 +137,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.mqtt.*", "homeassistant.components.mullvad.*", "homeassistant.components.mysensors.*", - "homeassistant.components.n26.*", "homeassistant.components.neato.*", "homeassistant.components.ness_alarm.*", "homeassistant.components.nest.*", From 6665a62557494a061e2fe7d60fa0e22d2e1504bd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 9 May 2021 00:04:11 +0000 Subject: [PATCH 253/852] [ci skip] Translation update --- .../components/adguard/translations/fr.json | 1 + .../buienradar/translations/fr.json | 23 +++++++++++++++++ .../components/cast/translations/ca.json | 1 + .../components/cast/translations/et.json | 21 +++++++++++++--- .../components/cast/translations/fr.json | 15 +++++++++++ .../components/cast/translations/it.json | 21 +++++++++++++--- .../components/cast/translations/ru.json | 21 +++++++++++++--- .../components/denonavr/translations/fr.json | 1 + .../devolo_home_control/translations/fr.json | 7 ++++++ .../components/epson/translations/ca.json | 3 ++- .../components/epson/translations/et.json | 3 ++- .../components/epson/translations/fr.json | 3 ++- .../components/epson/translations/it.json | 3 ++- .../components/epson/translations/ru.json | 3 ++- .../components/flume/translations/fr.json | 7 ++++++ .../components/fritz/translations/fr.json | 4 +++ .../huawei_lte/translations/fr.json | 3 ++- .../components/lyric/translations/fr.json | 3 +++ .../components/motioneye/translations/fr.json | 20 +++++++++++++++ .../components/mqtt/translations/fr.json | 6 +++-- .../components/myq/translations/fr.json | 7 ++++++ .../components/mysensors/translations/fr.json | 1 + .../components/nam/translations/fr.json | 20 +++++++++++++++ .../components/nam/translations/it.json | 24 ++++++++++++++++++ .../components/picnic/translations/fr.json | 17 +++++++++++++ .../rainmachine/translations/fr.json | 1 + .../components/sma/translations/fr.json | 21 ++++++++++++++++ .../components/smarttub/translations/fr.json | 5 +++- .../components/syncthing/translations/ca.json | 22 ++++++++++++++++ .../components/syncthing/translations/en.json | 14 +++++------ .../components/syncthing/translations/et.json | 22 ++++++++++++++++ .../components/syncthing/translations/it.json | 22 ++++++++++++++++ .../system_bridge/translations/fr.json | 25 +++++++++++++++++++ 33 files changed, 345 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/buienradar/translations/fr.json create mode 100644 homeassistant/components/motioneye/translations/fr.json create mode 100644 homeassistant/components/nam/translations/fr.json create mode 100644 homeassistant/components/nam/translations/it.json create mode 100644 homeassistant/components/picnic/translations/fr.json create mode 100644 homeassistant/components/sma/translations/fr.json create mode 100644 homeassistant/components/syncthing/translations/ca.json create mode 100644 homeassistant/components/syncthing/translations/et.json create mode 100644 homeassistant/components/syncthing/translations/it.json create mode 100644 homeassistant/components/system_bridge/translations/fr.json diff --git a/homeassistant/components/adguard/translations/fr.json b/homeassistant/components/adguard/translations/fr.json index 613145ec0f2..f97eb7a0df1 100644 --- a/homeassistant/components/adguard/translations/fr.json +++ b/homeassistant/components/adguard/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.", "single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e." }, diff --git a/homeassistant/components/buienradar/translations/fr.json b/homeassistant/components/buienradar/translations/fr.json new file mode 100644 index 00000000000..d9c2fadcbf7 --- /dev/null +++ b/homeassistant/components/buienradar/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Code de pays du pays pour afficher les images de la cam\u00e9ra.", + "delta": "Intervalle de temps en secondes entre les mises \u00e0 jour de l'image de la cam\u00e9ra", + "timeframe": "Minutes \u00e0 pr\u00e9voir pour les pr\u00e9visions de pr\u00e9cipitations" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/ca.json b/homeassistant/components/cast/translations/ca.json index aaef5803b5c..944c3c043d5 100644 --- a/homeassistant/components/cast/translations/ca.json +++ b/homeassistant/components/cast/translations/ca.json @@ -30,6 +30,7 @@ "ignore_cec": "Ignora CEC", "uuid": "UUID permesos" }, + "description": "UUIDs permesos - Llista, separada per comes, dels UUIDs dels dispositius Cast a afegir a Home Assistant. Utilitza-ho si no vols afegir tots els dispositius Cast disponibles.\nIgnora CEC - Llista, separada per comes, dels Chromecasts que han d'ignorar les dades CEC al determinar l'entrada activa. S'enviar\u00e0 a pychromecast.IGNORE_CEC.", "title": "Configuraci\u00f3 avan\u00e7ada de Google Cast" }, "basic_options": { diff --git a/homeassistant/components/cast/translations/et.json b/homeassistant/components/cast/translations/et.json index 6397951272a..48397e044f4 100644 --- a/homeassistant/components/cast/translations/et.json +++ b/homeassistant/components/cast/translations/et.json @@ -10,10 +10,10 @@ "step": { "config": { "data": { - "known_hosts": "Valikuline loend teadaolevatest hostidest kui mDNS-i tuvastamine ei t\u00f6\u00f6ta." + "known_hosts": "Tuntud hostid" }, - "description": "Sisesta Google Casti andmed.", - "title": "Google Cast" + "description": "Tuntud hostid - komadega eraldatud loend seadmete hostinimedest v\u00f5i IP-aadressidest. Kasuta seda juhul kui mDNS-i tuvastus ei t\u00f6\u00f6ta.", + "title": "Google Casti s\u00e4tted" }, "confirm": { "description": "Kas soovid seadistada Google Casti?" @@ -25,6 +25,21 @@ "invalid_known_hosts": "Teadaolevad hostid peab olema komaeraldusega hostide loend." }, "step": { + "advanced_options": { + "data": { + "ignore_cec": "Eira CEC-i", + "uuid": "Lubatud UUID-d" + }, + "description": "Lubatud UUID-d - komadega eraldatud loetelu UUID-dest, mida soovitakse lisada Home Assistant'ile. Kasuta ainult siis, kui ei soovi lisada k\u00f5iki olemasolevaid Cast seadmeid.\nIgnore CEC - komadega eraldatud loetelu Chromecastidest, mis peaksid aktiivse sisendi m\u00e4\u00e4ramisel CEC-andmeid ignoreerima. See edastatakse pychromecast.IGNORE_CEC.", + "title": "Google Casti seadistamise t\u00e4psemad valikud" + }, + "basic_options": { + "data": { + "known_hosts": "Tuntud hostid" + }, + "description": "Tuntud hostid - komadega eraldatud loend hostitud seadmete hostinimedest v\u00f5i IP-aadressidest. Kasuta seda juhul kui mDNS-i tuvastus ei t\u00f6\u00f6ta.", + "title": "Google Casti s\u00e4tted" + }, "options": { "data": { "ignore_cec": "Valikuline nimekiri mis edastatakse pychromecast.IGNORE_CEC-ile.", diff --git a/homeassistant/components/cast/translations/fr.json b/homeassistant/components/cast/translations/fr.json index f5ee03a6c00..a907fbe4a76 100644 --- a/homeassistant/components/cast/translations/fr.json +++ b/homeassistant/components/cast/translations/fr.json @@ -25,6 +25,21 @@ "invalid_known_hosts": "Les h\u00f4tes connus doivent \u00eatre une liste d'h\u00f4tes s\u00e9par\u00e9s par des virgules." }, "step": { + "advanced_options": { + "data": { + "ignore_cec": "Ignorer CEC", + "uuid": "UUID autoris\u00e9s" + }, + "description": "UUID autoris\u00e9s: liste s\u00e9par\u00e9e par des virgules des UUID des appareils Cast \u00e0 ajouter \u00e0 Home Assistant. \u00c0 utiliser uniquement si vous ne souhaitez pas ajouter tous les appareils de diffusion disponibles.\n Ignorer CEC - Une liste de Chromecast s\u00e9par\u00e9s par des virgules qui doivent ignorer les donn\u00e9es CEC pour d\u00e9terminer l'entr\u00e9e active. Ce sera transmis \u00e0 pychromecast.IGNORE_CEC.", + "title": "Configuration avanc\u00e9e de Google Cast" + }, + "basic_options": { + "data": { + "known_hosts": "H\u00f4tes connus" + }, + "description": "H\u00f4tes connus - Une liste de noms d'h\u00f4te ou d'adresses IP s\u00e9par\u00e9s par des virgules des p\u00e9riph\u00e9riques de diffusion, \u00e0 utiliser si la d\u00e9couverte mDNS ne fonctionne pas.", + "title": "Configuration de Google Cast" + }, "options": { "data": { "ignore_cec": "Liste facultative qui sera transmise \u00e0 pychromecast.IGNORE_CEC.", diff --git a/homeassistant/components/cast/translations/it.json b/homeassistant/components/cast/translations/it.json index 83586bf9f2c..c0ff9144a2f 100644 --- a/homeassistant/components/cast/translations/it.json +++ b/homeassistant/components/cast/translations/it.json @@ -10,10 +10,10 @@ "step": { "config": { "data": { - "known_hosts": "Elenco facoltativo di host noti se l'individuazione di mDNS non funziona." + "known_hosts": "Host conosciuti" }, - "description": "Inserisci la configurazione di Google Cast.", - "title": "Google Cast" + "description": "Host conosciuti: un elenco separato da virgole di nomi host o indirizzi IP di dispositivi di trasmissione, da utilizzare se l'individuazione di mDNS non funziona.", + "title": "Configurazione di Google Cast" }, "confirm": { "description": "Vuoi iniziare la configurazione?" @@ -25,6 +25,21 @@ "invalid_known_hosts": "Gli host noti devono essere indicati sotto forma di un elenco di host separati da virgole." }, "step": { + "advanced_options": { + "data": { + "ignore_cec": "Ignora CEC", + "uuid": "UUID consentiti" + }, + "description": "UUID consentiti: un elenco separato da virgole di UUID dei dispositivi di trasmissione da aggiungere a Home Assistant. Utilizza solo se non desideri aggiungere tutti i dispositivi di trasmissione disponibili.\nIgnora CEC: un elenco separato da virgole di Chromecast che dovrebbero ignorare i dati CEC per determinare l'input attivo. Questo verr\u00e0 passato a pychromecast.IGNORE_CEC.", + "title": "Configurazione avanzata di Google Cast" + }, + "basic_options": { + "data": { + "known_hosts": "Host conosciuti" + }, + "description": "Host conosciuti: un elenco separato da virgole di nomi host o indirizzi IP di dispositivi di trasmissione, da utilizzare se l'individuazione di mDNS non funziona.", + "title": "Configurazione di Google Cast" + }, "options": { "data": { "ignore_cec": "Elenco opzionale che sar\u00e0 passato a pychromecast.IGNORE_CEC.", diff --git a/homeassistant/components/cast/translations/ru.json b/homeassistant/components/cast/translations/ru.json index 7c412476151..cb432acbf64 100644 --- a/homeassistant/components/cast/translations/ru.json +++ b/homeassistant/components/cast/translations/ru.json @@ -10,10 +10,10 @@ "step": { "config": { "data": { - "known_hosts": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0445\u043e\u0441\u0442\u043e\u0432, \u0435\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 mDNS \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442." + "known_hosts": "\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Google Cast.", - "title": "Google Cast" + "description": "\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b \u2014 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u043c\u0435\u043d \u0445\u043e\u0441\u0442\u043e\u0432 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 mDNS \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Google Cast" }, "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" @@ -25,6 +25,21 @@ "invalid_known_hosts": "\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u0441\u043f\u0438\u0441\u043a\u043e\u043c, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u043c \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438." }, "step": { + "advanced_options": { + "data": { + "ignore_cec": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c CEC", + "uuid": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u043d\u044b\u0435 UUID" + }, + "description": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u043d\u044b\u0435 UUID \u2014 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438 \u0441\u043f\u0438\u0441\u043e\u043a UUID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Google Cast \u0434\u043b\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0432 Home Assistant. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u0435\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u0432\u0441\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Google Cast.\n\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c CEC \u2014 \u0441\u043f\u0438\u0441\u043e\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Chromecast, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 CEC \u0434\u043b\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u0432\u0432\u043e\u0434\u0430. \u042d\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0430 \u0432 pychromecast.IGNORE_CEC.", + "title": "\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Google Cast" + }, + "basic_options": { + "data": { + "known_hosts": "\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b" + }, + "description": "\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b \u2014 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u043c\u0435\u043d \u0445\u043e\u0441\u0442\u043e\u0432 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 mDNS \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Google Cast" + }, "options": { "data": { "ignore_cec": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d \u0432 pychromecast.IGNORE_CEC.", diff --git a/homeassistant/components/denonavr/translations/fr.json b/homeassistant/components/denonavr/translations/fr.json index 16183c90c17..797f10fe06f 100644 --- a/homeassistant/components/denonavr/translations/fr.json +++ b/homeassistant/components/denonavr/translations/fr.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Afficher tous les sources", + "update_audyssey": "Mettre \u00e0 jour les param\u00e8tres Audyssey", "zone2": "Configurer Zone 2", "zone3": "Configurer Zone 3" }, diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index fa8871bd17e..13354e9da76 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -14,6 +14,13 @@ "password": "Mot de passe", "username": "Adresse e-mail / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Mot de passe", + "username": "[%key:common::config_flow::d ata::email%] / devolo ID" + } } } } diff --git a/homeassistant/components/epson/translations/ca.json b/homeassistant/components/epson/translations/ca.json index 46e6311ef05..51fbbe1e273 100644 --- a/homeassistant/components/epson/translations/ca.json +++ b/homeassistant/components/epson/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "powered_off": "El projector est\u00e0 enc\u00e8s? Per fer la configuraci\u00f3 inicial has d'activar el projector." }, "step": { "user": { diff --git a/homeassistant/components/epson/translations/et.json b/homeassistant/components/epson/translations/et.json index 1fb510c37a4..a0e3ec395f5 100644 --- a/homeassistant/components/epson/translations/et.json +++ b/homeassistant/components/epson/translations/et.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "powered_off": "Kas projektor on sisse l\u00fclitatud? Esmaseks seadistamiseks pead projektori sisse l\u00fclitama." }, "step": { "user": { diff --git a/homeassistant/components/epson/translations/fr.json b/homeassistant/components/epson/translations/fr.json index cfc37079379..3bbdd3063f5 100644 --- a/homeassistant/components/epson/translations/fr.json +++ b/homeassistant/components/epson/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Echec de la connection" + "cannot_connect": "Echec de la connection", + "powered_off": "Le projecteur est-il allum\u00e9? Vous devez allumer le projecteur pour la configuration initiale." }, "step": { "user": { diff --git a/homeassistant/components/epson/translations/it.json b/homeassistant/components/epson/translations/it.json index 233a004fe0c..fe72abc8739 100644 --- a/homeassistant/components/epson/translations/it.json +++ b/homeassistant/components/epson/translations/it.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "powered_off": "Il proiettore \u00e8 acceso? \u00c8 necessario accendere il proiettore per la configurazione iniziale." }, "step": { "user": { diff --git a/homeassistant/components/epson/translations/ru.json b/homeassistant/components/epson/translations/ru.json index d60b7c60c84..47209d311a5 100644 --- a/homeassistant/components/epson/translations/ru.json +++ b/homeassistant/components/epson/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "powered_off": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0440? \u0414\u043b\u044f \u043f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/flume/translations/fr.json b/homeassistant/components/flume/translations/fr.json index fdb7ab8ed9a..a111d66b937 100644 --- a/homeassistant/components/flume/translations/fr.json +++ b/homeassistant/components/flume/translations/fr.json @@ -9,6 +9,13 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Le mot de passe de {username} n'est plus valide.", + "title": "R\u00e9authentifiez votre compte Flume" + }, "user": { "data": { "client_id": "ID du client", diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json index 32e7e3694f9..e0fa5dd3e8c 100644 --- a/homeassistant/components/fritz/translations/fr.json +++ b/homeassistant/components/fritz/translations/fr.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9 ", "connection_error": "Erreur de connexion", "invalid_auth": "Authentification invalide" }, diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index e598f4a3b86..df7e6c2e380 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -34,7 +34,8 @@ "data": { "name": "Nom du service de notification (red\u00e9marrage requis)", "recipient": "Destinataires des notifications SMS", - "track_new_devices": "Suivre les nouveaux appareils" + "track_new_devices": "Suivre les nouveaux appareils", + "track_wired_clients": "Suivre les clients du r\u00e9seau filaire" } } } diff --git a/homeassistant/components/lyric/translations/fr.json b/homeassistant/components/lyric/translations/fr.json index 540d3e1e6c2..db23120b40d 100644 --- a/homeassistant/components/lyric/translations/fr.json +++ b/homeassistant/components/lyric/translations/fr.json @@ -10,6 +10,9 @@ "step": { "pick_implementation": { "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration Lyric doit authentifier \u00e0 nouveau votre compte." } } } diff --git a/homeassistant/components/motioneye/translations/fr.json b/homeassistant/components/motioneye/translations/fr.json new file mode 100644 index 00000000000..a520c05dba2 --- /dev/null +++ b/homeassistant/components/motioneye/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_url": "URL invalide" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Mot de passe", + "admin_username": "Admin Nom d'utilisateur", + "surveillance_password": "Surveillance Mot de passe", + "surveillance_username": "Surveillance Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index 6ee3788725d..af13e69ab4a 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -62,7 +62,8 @@ "port": "Port", "username": "Username" }, - "description": "Veuillez entrer les informations de connexion de votre broker MQTT." + "description": "Veuillez entrer les informations de connexion de votre broker MQTT.", + "title": "Options de courtier" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Retenir le message de testament", "will_topic": "Topic du message de testament" }, - "description": "Veuillez s\u00e9lectionner les options MQTT." + "description": "D\u00e9couverte - Si la d\u00e9couverte est activ\u00e9e (recommand\u00e9e), Home Assistant d\u00e9couvrira automatiquement les appareils et les entit\u00e9s qui publient leur configuration sur le courtier MQTT. Si la d\u00e9couverte est d\u00e9sactiv\u00e9e, toute la configuration doit \u00eatre effectu\u00e9e manuellement.\n Message de naissance - Le message de naissance sera envoy\u00e9 chaque fois que Home Assistant (re) se connecte au courtier MQTT.\n Will message - Le message will sera envoy\u00e9 chaque fois que Home Assistant perd sa connexion avec le courtier, \u00e0 la fois en cas de nettoyage (par exemple, arr\u00eat de Home Assistant) et en cas de salet\u00e9 (par exemple, Home Assistant se bloque ou perd sa connexion r\u00e9seau) d\u00e9connecter.", + "title": "Options MQTT" } } } diff --git a/homeassistant/components/myq/translations/fr.json b/homeassistant/components/myq/translations/fr.json index 4ae00e7495f..e9a6bc60b82 100644 --- a/homeassistant/components/myq/translations/fr.json +++ b/homeassistant/components/myq/translations/fr.json @@ -9,6 +9,13 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "mot de passe" + }, + "description": "Le mot de passe de {username} n'est plus valide.", + "title": "R\u00e9authentifiez votre compte MyQ" + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/mysensors/translations/fr.json b/homeassistant/components/mysensors/translations/fr.json index 00f9831c035..e104c69e815 100644 --- a/homeassistant/components/mysensors/translations/fr.json +++ b/homeassistant/components/mysensors/translations/fr.json @@ -33,6 +33,7 @@ "invalid_serial": "Port s\u00e9rie non valide", "invalid_subscribe_topic": "Sujet d'abonnement non valide", "invalid_version": "Version de MySensors non valide", + "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", "not_a_number": "Veuillez saisir un nombre", "port_out_of_range": "Le num\u00e9ro de port doit \u00eatre au moins 1 et au plus 65535", "same_topic": "Les sujets de souscription et de publication sont identiques", diff --git a/homeassistant/components/nam/translations/fr.json b/homeassistant/components/nam/translations/fr.json new file mode 100644 index 00000000000..0c58af2a800 --- /dev/null +++ b/homeassistant/components/nam/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "device_unsupported": "L'appareil n'est pas pris en charge." + }, + "flow_title": "{nom}", + "step": { + "confirm_discovery": { + "description": "Voulez-vous configurer Nettigo Air Monitor chez {host} ?" + }, + "user": { + "data": { + "host": "Hotes" + }, + "description": "Configurez l'int\u00e9gration Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/it.json b/homeassistant/components/nam/translations/it.json new file mode 100644 index 00000000000..9a208cbfd3c --- /dev/null +++ b/homeassistant/components/nam/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "device_unsupported": "Il dispositivo non \u00e8 supportato." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Vuoi configurare Nettigo Air Monitor su {host} ?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Configura l'integrazione di Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/fr.json b/homeassistant/components/picnic/translations/fr.json new file mode 100644 index 00000000000..044b0a72771 --- /dev/null +++ b/homeassistant/components/picnic/translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "country_code": "Code postal", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + }, + "title": "Pique-nique" +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/fr.json b/homeassistant/components/rainmachine/translations/fr.json index 02b7dbc2699..df0f9efa588 100644 --- a/homeassistant/components/rainmachine/translations/fr.json +++ b/homeassistant/components/rainmachine/translations/fr.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Authentification invalide" }, + "flow_title": "RainMachine {ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/sma/translations/fr.json b/homeassistant/components/sma/translations/fr.json new file mode 100644 index 00000000000..ab154fea3f8 --- /dev/null +++ b/homeassistant/components/sma/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_retrieve_device_info": "Connexion r\u00e9ussie, mais impossible de r\u00e9cup\u00e9rer les informations sur l'appareil" + }, + "step": { + "user": { + "data": { + "group": "Groupe", + "host": "H\u00f4te ", + "password": "Mot de passe" + }, + "description": "Saisissez les informations relatives \u00e0 votre appareil SMA.", + "title": "Configurer SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/fr.json b/homeassistant/components/smarttub/translations/fr.json index 15dfa04fc78..f51d34a0958 100644 --- a/homeassistant/components/smarttub/translations/fr.json +++ b/homeassistant/components/smarttub/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "reauth_successful": "La r\u00e9-authentification a \u00e9t\u00e9 un succ\u00e8s" }, "error": { @@ -9,6 +9,9 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "description": "L'int\u00e9gration SmartTub doit r\u00e9-authentifier votre compte" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/syncthing/translations/ca.json b/homeassistant/components/syncthing/translations/ca.json new file mode 100644 index 00000000000..a10b5d8c134 --- /dev/null +++ b/homeassistant/components/syncthing/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "title": "Configura la integraci\u00f3 Syncthing", + "token": "Token", + "url": "URL", + "verify_ssl": "Verifica el certificat SSL" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/en.json b/homeassistant/components/syncthing/translations/en.json index 00c73bedb9e..68efde737f2 100644 --- a/homeassistant/components/syncthing/translations/en.json +++ b/homeassistant/components/syncthing/translations/en.json @@ -1,19 +1,19 @@ { "config": { - "error": { - "cannot_connect": "Unable to connect to the Syncthing server.", - "invalid_auth": "Invalid authentication" - }, "abort": { "already_configured": "Service is already configured" }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, "step": { "user": { - "title": "Setup Syncthing integration", "data": { - "url": "URL", + "title": "Setup Syncthing integration", "token": "Token", - "verify_ssl": "Verify SSL" + "url": "URL", + "verify_ssl": "Verify SSL certificate" } } } diff --git a/homeassistant/components/syncthing/translations/et.json b/homeassistant/components/syncthing/translations/et.json new file mode 100644 index 00000000000..12922ad3f6d --- /dev/null +++ b/homeassistant/components/syncthing/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine" + }, + "step": { + "user": { + "data": { + "title": "Seadista Syncthingi sidumine", + "token": "Token", + "url": "URL", + "verify_ssl": "Kontrolli SSL serti" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/it.json b/homeassistant/components/syncthing/translations/it.json new file mode 100644 index 00000000000..2333b09093a --- /dev/null +++ b/homeassistant/components/syncthing/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "title": "Configurazione integrazione Syncthing", + "token": "Token", + "url": "URL", + "verify_ssl": "Verificare il certificato SSL" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/fr.json b/homeassistant/components/system_bridge/translations/fr.json new file mode 100644 index 00000000000..187360bac5e --- /dev/null +++ b/homeassistant/components/system_bridge/translations/fr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "flow_title": "Pont syst\u00e8me: {name}", + "step": { + "authenticate": { + "data": { + "api_key": "Clef d'API" + }, + "description": "Veuillez saisir la cl\u00e9 API que vous avez d\u00e9finie dans votre configuration pour {name} ." + }, + "user": { + "data": { + "api_key": "Clef d'API", + "host": "H\u00f4te", + "port": "Port" + }, + "description": "Veuillez saisir vos informations de connexion." + } + } + }, + "title": "Pont syst\u00e8me" +} \ No newline at end of file From e92516c07256be53a3ade83a68599fc8f05401de Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 8 May 2021 20:21:00 -0400 Subject: [PATCH 254/852] Add targets and selectors to services (A) (#49818) --- homeassistant/components/abode/services.yaml | 25 ++++++ .../components/adguard/services.yaml | 29 +++++++ homeassistant/components/ads/services.yaml | 23 ++++- .../components/advantage_air/services.yaml | 8 ++ .../components/aftership/services.yaml | 19 ++++ .../components/agent_dvr/services.yaml | 45 +++++----- .../components/alarmdecoder/services.yaml | 26 ++++-- .../components/ambiclimate/services.yaml | 24 ++++- .../components/amcrest/services.yaml | 87 +++++++++++++++++-- .../components/androidtv/services.yaml | 47 +++++++++- homeassistant/components/arlo/services.yaml | 1 + 11 files changed, 294 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/abode/services.yaml b/homeassistant/components/abode/services.yaml index f694afc0298..9b5362c0929 100644 --- a/homeassistant/components/abode/services.yaml +++ b/homeassistant/components/abode/services.yaml @@ -1,21 +1,46 @@ capture_image: + name: Capture image description: Request a new image capture from a camera device. fields: entity_id: + name: Entity description: Entity id of the camera to request an image. + required: true example: camera.downstairs_motion_camera + selector: + entity: + integration: abode + domain: camera + change_setting: + name: Change setting description: Change an Abode system setting. fields: setting: + name: Setting description: Setting to change. + required: true example: beeper_mute + selector: + text: value: + name: Value description: Value of the setting. + required: true example: "1" + selector: + text: + trigger_automation: + name: Trigger automation description: Trigger an Abode automation. fields: entity_id: + name: Entity description: Entity id of the automation to trigger. + required: true example: switch.my_automation + selector: + entity: + integration: abode + domain: switch diff --git a/homeassistant/components/adguard/services.yaml b/homeassistant/components/adguard/services.yaml index 736acdd923c..2e97d164e3a 100644 --- a/homeassistant/components/adguard/services.yaml +++ b/homeassistant/components/adguard/services.yaml @@ -1,37 +1,66 @@ add_url: + name: Add url description: Add a new filter subscription to AdGuard Home. fields: name: + name: Name description: The name of the filter subscription. + required: true example: Example + selector: + text: url: + name: Url description: The filter URL to subscribe to, containing the filter rules. + required: true example: https://www.example.com/filter/1.txt + selector: + text: remove_url: + name: Remove url description: Removes a filter subscription from AdGuard Home. fields: url: + name: Url description: The filter subscription URL to remove. + required: true example: https://www.example.com/filter/1.txt + selector: + text: enable_url: + name: Enable url description: Enables a filter subscription in AdGuard Home. fields: url: + name: Url description: The filter subscription URL to enable. + required: true example: https://www.example.com/filter/1.txt + selector: + text: disable_url: + name: Disable url description: Disables a filter subscription in AdGuard Home. fields: url: + name: Url description: The filter subscription URL to disable. + required: true example: https://www.example.com/filter/1.txt + selector: + text: refresh: + name: Refresh description: Refresh all filter subscriptions in AdGuard Home. fields: force: + name: Force description: Force update (by passes AdGuard Home throttling). example: '"true" to force, "false" or omit for a regular refresh.' + default: false + selector: + boolean: diff --git a/homeassistant/components/ads/services.yaml b/homeassistant/components/ads/services.yaml index 1e7b664b674..5139662a522 100644 --- a/homeassistant/components/ads/services.yaml +++ b/homeassistant/components/ads/services.yaml @@ -1,15 +1,36 @@ # Describes the format for available ADS services write_data_by_name: + name: Write data by name description: Write a value to the connected ADS device. - fields: adsvar: + name: ADS variable description: The name of the variable to write to. + required: true example: ".global_var" + selector: + text: adstype: + name: ADS type description: The data type of the variable to write to. + required: true example: "int" + selector: + select: + options: + - 'bool' + - 'byte' + - 'dint' + - 'int' + - 'udint' + - 'uint' value: + name: Value description: The value to write to the variable. + required: true example: 1 + selector: + number: + min: 0 + max: 10000 diff --git a/homeassistant/components/advantage_air/services.yaml b/homeassistant/components/advantage_air/services.yaml index e70208c4ac1..24088421c99 100644 --- a/homeassistant/components/advantage_air/services.yaml +++ b/homeassistant/components/advantage_air/services.yaml @@ -7,8 +7,16 @@ set_time_to: domain: sensor fields: minutes: + name: Minutes description: Minutes until action + required: true example: "60" + selector: + number: + min: 0 + max: 1440 + unit_of_measurement: minutes + set_myzone: name: Set MyZone description: Change which zone is set as the reference for temperature control diff --git a/homeassistant/components/aftership/services.yaml b/homeassistant/components/aftership/services.yaml index 5ad30d25d8b..e4d90646aa6 100644 --- a/homeassistant/components/aftership/services.yaml +++ b/homeassistant/components/aftership/services.yaml @@ -1,24 +1,43 @@ # Describes the format for available aftership services add_tracking: + name: Add tracking description: Add new tracking to Aftership. fields: tracking_number: + name: Tracking number description: Tracking number for the new tracking + required: true example: "123456789" + selector: + text: title: + name: Title description: A custom title for the new tracking example: "Laptop" + selector: + text: slug: + name: Slug description: Slug (carrier) of the new tracking example: "USPS" + selector: + text: remove_tracking: + name: Remove tracking description: Remove a tracking from Aftership. fields: tracking_number: + name: Tracking number description: Tracking number of the tracking to remove + required: true example: "123456789" + selector: + text: slug: + name: Slug description: Slug (carrier) of the tracking to remove example: "USPS" + selector: + text: diff --git a/homeassistant/components/agent_dvr/services.yaml b/homeassistant/components/agent_dvr/services.yaml index 8bf1e01269a..206b32cb526 100644 --- a/homeassistant/components/agent_dvr/services.yaml +++ b/homeassistant/components/agent_dvr/services.yaml @@ -1,34 +1,39 @@ start_recording: + name: Start recording description: Enable continuous recording. - fields: - entity_id: - description: "Name(s) of the entity to start recording." - example: "camera.camera_1" + target: + entity: + integration: agent_dvr + domain: camera stop_recording: + name: Stop recording description: Disable continuous recording. - fields: - entity_id: - description: "Name(s) of the entity to stop recording." - example: "camera.camera_1" + target: + entity: + integration: agent_dvr + domain: camera enable_alerts: + name: Enable alerts description: Enable alerts - fields: - entity_id: - description: "Name(s) of the entity to enable alerts." - example: "camera.camera_1" + target: + entity: + integration: agent_dvr + domain: camera disable_alerts: + name: Disable alerts description: Disable alerts - fields: - entity_id: - description: "Name(s) of the entity to disable alerts." - example: "camera.camera_1" + target: + entity: + integration: agent_dvr + domain: camera snapshot: + name: Snapshot description: Take a photo - fields: - entity_id: - description: "Name(s) of the entity to take a snapshot." - example: "camera.camera_1" + target: + entity: + integration: agent_dvr + domain: camera diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml index 37c7ddf210c..9d50eae07e6 100644 --- a/homeassistant/components/alarmdecoder/services.yaml +++ b/homeassistant/components/alarmdecoder/services.yaml @@ -1,19 +1,31 @@ alarm_keypress: + name: Key press description: Send custom keypresses to the alarm. + target: + entity: + integration: alarmdecoder + domain: alarm_control_panel fields: - entity_id: - description: Name of alarm control panel to deliver keypress. - example: "alarm_control_panel.main" keypress: + name: Key press description: "String to send to the alarm panel." + required: true example: "*71" + selector: + text: alarm_toggle_chime: + name: Toggle Chime description: Send the alarm the toggle chime command. + target: + entity: + integration: alarmdecoder + domain: alarm_control_panel fields: - entity_id: - description: Name of alarm control panel to toggle chime. - example: "alarm_control_panel.main" code: - description: A required code to toggle the alarm control panel chime with. + name: Code + description: A code to toggle the alarm control panel chime with. + required: true example: 1234 + selector: + text: diff --git a/homeassistant/components/ambiclimate/services.yaml b/homeassistant/components/ambiclimate/services.yaml index 19f47c6c35f..f75857e4d2e 100644 --- a/homeassistant/components/ambiclimate/services.yaml +++ b/homeassistant/components/ambiclimate/services.yaml @@ -1,36 +1,54 @@ # Describes the format for available services for ambiclimate set_comfort_mode: + name: Set comfort mode description: > - Enable comfort mode on your AC + Enable comfort mode on your AC. fields: Name: description: > String with device name. + required: true example: Bedroom + selector: + text: send_comfort_feedback: + name: Send comfort feedback description: > - Send feedback for comfort mode + Send feedback for comfort mode. fields: Name: description: > String with device name. + required: true example: Bedroom + selector: + text: Value: description: > Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing + required: true example: bit_warm + selector: + text: set_temperature_mode: + name: Set temperature mode description: > - Enable temperature mode on your AC + Enable temperature mode on your AC. fields: Name: description: > String with device name. + required: true example: Bedroom + selector: + text: Value: description: > Target value in celsius + required: true example: 22 + selector: + text: diff --git a/homeassistant/components/amcrest/services.yaml b/homeassistant/components/amcrest/services.yaml index 10865586b6d..c4a12c59828 100644 --- a/homeassistant/components/amcrest/services.yaml +++ b/homeassistant/components/amcrest/services.yaml @@ -1,88 +1,165 @@ enable_recording: + name: Enable recording description: Enable continuous recording to camera storage. fields: entity_id: + name: Entity description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" + selector: + text: disable_recording: + name: Disable recording description: Disable continuous recording to camera storage. fields: entity_id: + name: Entity description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" + selector: + text: enable_audio: + name: Enable audio description: Enable audio stream. fields: entity_id: + name: Entity description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" + selector: + text: disable_audio: + name: Disable audio description: Disable audio stream. fields: entity_id: + name: Entity description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" + selector: + text: enable_motion_recording: + name: Enable motion recording description: Enable recording a clip to camera storage when motion is detected. fields: entity_id: + name: Entity description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" + selector: + text: disable_motion_recording: + name: Disable motion recording description: Disable recording a clip to camera storage when motion is detected. fields: entity_id: + name: Entity description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" + selector: + text: goto_preset: + name: Go to preset description: Move camera to PTZ preset. fields: entity_id: description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" preset: - description: Preset number, starting from 1. + name: Preset + description: Preset number. + required: true example: 1 + selector: + number: + min: 1 + max: 1000 set_color_bw: + name: Set color description: Set camera color mode. fields: entity_id: + name: Entity description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" + selector: + text: color_bw: - description: Color mode, one of 'auto', 'color' or 'bw'. + name: Color + description: Color mode. example: auto + selector: + select: + options: + - 'auto' + - 'bw' + - 'color' start_tour: + name: Start tour description: Start camera's PTZ tour function. fields: entity_id: + name: Entity description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" + selector: + text: stop_tour: + name: Stop tour description: Stop camera's PTZ tour function. fields: entity_id: + name: Entity description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" + selector: + text: ptz_control: - description: Move (Pan/Tilt) and/or Zoom a PTZ camera + name: PTZ control + description: Move (Pan/Tilt) and/or Zoom a PTZ camera. fields: entity_id: + name: Entity description: "Name of the camera, or 'all' for all cameras." example: "camera.house_front" + selector: + text: movement: - description: "up, down, right, left, right_up, right_down, left_up, left_down, zoom_in, zoom_out" + name: Movement + description: "Direction to move the camera." + required: true example: "right" + selector: + select: + options: + - 'down' + - 'left' + - 'left_down' + - 'left_up' + - 'right' + - 'right_down' + - 'right_up' + - 'up' + - 'zoom_in' + - 'zoom_out' travel_time: - description: "(optional) Travel time in fractional seconds: from 0 to 1. Default: .2" + name: Travel time + description: "Travel time in fractional seconds: from 0 to 1." example: ".5" + default: .2 + selector: + number: + min: 0 + max: 1 + step: 0.01 + unit_of_measurement: seconds diff --git a/homeassistant/components/androidtv/services.yaml b/homeassistant/components/androidtv/services.yaml index 65e83dfbe4f..55b871ff58f 100644 --- a/homeassistant/components/androidtv/services.yaml +++ b/homeassistant/components/androidtv/services.yaml @@ -1,41 +1,80 @@ # Describes the format for available Android TV and Fire TV services adb_command: + name: ADB command description: Send an ADB command to an Android TV / Fire TV device. fields: entity_id: description: Name(s) of Android TV / Fire TV entities. + required: true example: "media_player.android_tv_living_room" + selector: + entity: + integration: androidtv + domain: media_player command: + name: Command description: Either a key command or an ADB shell command. + required: true example: "HOME" + selector: + text: download: + name: Download description: Download a file from your Android TV / Fire TV device to your Home Assistant instance. fields: entity_id: description: Name of Android TV / Fire TV entity. + required: true example: "media_player.android_tv_living_room" + selector: + entity: + integration: androidtv + domain: media_player device_path: + name: Device path description: The filepath on the Android TV / Fire TV device. + required: true example: "/storage/emulated/0/Download/example.txt" + selector: + text: local_path: + name: Local path description: The filepath on your Home Assistant instance. + required: true example: "/config/www/example.txt" + selector: + text: upload: + name: Upload description: Upload a file from your Home Assistant instance to an Android TV / Fire TV device. fields: entity_id: description: Name(s) of Android TV / Fire TV entities. + required: true example: "media_player.android_tv_living_room" + selector: + entity: + integration: androidtv + domain: media_player device_path: + name: Device path description: The filepath on the Android TV / Fire TV device. + required: true example: "/storage/emulated/0/Download/example.txt" + selector: + text: local_path: + name: Local path description: The filepath on your Home Assistant instance. + required: true example: "/config/www/example.txt" + selector: + text: learn_sendevent: + name: Learn sendevent description: Translate a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service. - fields: - entity_id: - description: Name(s) of Android TV / Fire TV entities. - example: "media_player.android_tv_living_room" + target: + entity: + integration: androidtv + domain: media_player diff --git a/homeassistant/components/arlo/services.yaml b/homeassistant/components/arlo/services.yaml index a35fec8fb73..8481ffc4d53 100644 --- a/homeassistant/components/arlo/services.yaml +++ b/homeassistant/components/arlo/services.yaml @@ -1,4 +1,5 @@ # Describes the format for available arlo services update: + name: Update description: Update the state for all cameras and the base station. From 9059ce1c0f274b565937e4454df9b13692aea841 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 9 May 2021 04:07:13 +0100 Subject: [PATCH 255/852] Additional System Bridge Sensors (#50274) * Update systembridge to 1.1.4 * Update systembridge to 1.1.5 * Names * Add memory sensors * Set icons * Add bios version sensor * Memory used percentage sensor * Add types Co-authored-by: Martin Hjelmare * Disable by default * Typing Co-authored-by: Martin Hjelmare --- .../components/system_bridge/__init__.py | 1 + .../components/system_bridge/binary_sensor.py | 14 +- .../components/system_bridge/manifest.json | 2 +- .../components/system_bridge/sensor.py | 135 ++++++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 134 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 279c63680f0..cb78603f6dc 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -77,6 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): client.async_get_battery(), client.async_get_cpu(), client.async_get_filesystem(), + client.async_get_memory(), client.async_get_network(), client.async_get_os(), client.async_get_processes(), diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index b1010a19ae4..e18bcf516eb 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -1,4 +1,4 @@ -"""Support for System Bridge sensors.""" +"""Support for System Bridge binary sensors.""" from __future__ import annotations from systembridge import Bridge @@ -18,7 +18,7 @@ from .const import DOMAIN async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: - """Set up System Bridge sensor based on a config entry.""" + """Set up System Bridge binary sensor based on a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] bridge: Bridge = coordinator.data @@ -27,7 +27,7 @@ async def async_setup_entry( class BridgeBinarySensor(BridgeDeviceEntity, BinarySensorEntity): - """Defines a System Bridge sensor.""" + """Defines a System Bridge binary sensor.""" def __init__( self, @@ -39,22 +39,22 @@ class BridgeBinarySensor(BridgeDeviceEntity, BinarySensorEntity): device_class: str | None, enabled_by_default: bool, ) -> None: - """Initialize System Bridge sensor.""" + """Initialize System Bridge binary sensor.""" self._device_class = device_class super().__init__(coordinator, bridge, key, name, icon, enabled_by_default) @property def device_class(self) -> str | None: - """Return the class of this sensor.""" + """Return the class of this binary sensor.""" return self._device_class class BridgeBatteryIsChargingBinarySensor(BridgeBinarySensor): - """Defines a Battery is charging sensor.""" + """Defines a Battery is charging binary sensor.""" def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): - """Initialize System Bridge sensor.""" + """Initialize System Bridge binary sensor.""" super().__init__( coordinator, bridge, diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index c960c1c6557..0a800657009 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,7 +3,7 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridge==1.1.3"], + "requirements": ["systembridge==1.1.5"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._udp.local."], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index 7fa9efd791e..68a3fbdbd39 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -9,6 +9,7 @@ from systembridge import Bridge from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DATA_GIGABYTES, DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, @@ -51,9 +52,13 @@ async def async_setup_entry( BridgeFilesystemSensor(coordinator, bridge, key) for key, _ in bridge.filesystem.fsSize.items() ], + BridgeMemoryFreeSensor(coordinator, bridge), + BridgeMemoryUsedSensor(coordinator, bridge), + BridgeMemoryUsedPercentageSensor(coordinator, bridge), BridgeKernelSensor(coordinator, bridge), BridgeOsSensor(coordinator, bridge), BridgeProcessesLoadSensor(coordinator, bridge), + BridgeBiosVersionSensor(coordinator, bridge), ] if bridge.battery.hasBattery: @@ -97,7 +102,7 @@ class BridgeSensor(BridgeDeviceEntity, SensorEntity): class BridgeBatterySensor(BridgeSensor): """Defines a Battery sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, @@ -120,7 +125,7 @@ class BridgeBatterySensor(BridgeSensor): class BridgeBatteryTimeRemainingSensor(BridgeSensor): """Defines the Battery Time Remaining sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, @@ -145,14 +150,14 @@ class BridgeBatteryTimeRemainingSensor(BridgeSensor): class BridgeCpuSpeedSensor(BridgeSensor): """Defines a CPU speed sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, bridge, "cpu_speed", "CPU Speed", - None, + "mdi:speedometer", None, FREQUENCY_GIGAHERTZ, True, @@ -168,7 +173,7 @@ class BridgeCpuSpeedSensor(BridgeSensor): class BridgeCpuTemperatureSensor(BridgeSensor): """Defines a CPU temperature sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, @@ -191,7 +196,7 @@ class BridgeCpuTemperatureSensor(BridgeSensor): class BridgeCpuVoltageSensor(BridgeSensor): """Defines a CPU voltage sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, @@ -214,14 +219,16 @@ class BridgeCpuVoltageSensor(BridgeSensor): class BridgeFilesystemSensor(BridgeSensor): """Defines a filesystem sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge, key: str): + def __init__( + self, coordinator: DataUpdateCoordinator, bridge: Bridge, key: str + ) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, bridge, f"filesystem_{key}", f"{key} Space Used", - None, + "mdi:harddisk", None, PERCENTAGE, True, @@ -252,10 +259,91 @@ class BridgeFilesystemSensor(BridgeSensor): } +class BridgeMemoryFreeSensor(BridgeSensor): + """Defines a memory free sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "memory_free", + "Memory Free", + "mdi:memory", + None, + DATA_GIGABYTES, + True, + ) + + @property + def state(self) -> float | None: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return ( + round(bridge.memory.free / 1000 ** 3, 2) + if bridge.memory.free is not None + else None + ) + + +class BridgeMemoryUsedSensor(BridgeSensor): + """Defines a memory used sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "memory_used", + "Memory Used", + "mdi:memory", + None, + DATA_GIGABYTES, + False, + ) + + @property + def state(self) -> str | None: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return ( + round(bridge.memory.used / 1000 ** 3, 2) + if bridge.memory.used is not None + else None + ) + + +class BridgeMemoryUsedPercentageSensor(BridgeSensor): + """Defines a memory used percentage sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "memory_used_percentage", + "Memory Used %", + "mdi:memory", + None, + PERCENTAGE, + True, + ) + + @property + def state(self) -> str | None: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return ( + round((bridge.memory.used / bridge.memory.total) * 100, 2) + if bridge.memory.used is not None and bridge.memory.total is not None + else None + ) + + class BridgeKernelSensor(BridgeSensor): """Defines a kernel sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, @@ -278,7 +366,7 @@ class BridgeKernelSensor(BridgeSensor): class BridgeOsSensor(BridgeSensor): """Defines an OS sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, @@ -301,7 +389,7 @@ class BridgeOsSensor(BridgeSensor): class BridgeProcessesLoadSensor(BridgeSensor): """Defines a Processes Load sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: """Initialize System Bridge sensor.""" super().__init__( coordinator, @@ -315,7 +403,7 @@ class BridgeProcessesLoadSensor(BridgeSensor): ) @property - def state(self) -> float: + def state(self) -> float | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -338,3 +426,26 @@ class BridgeProcessesLoadSensor(BridgeSensor): if bridge.processes.load.currentLoadIdle is not None: attrs[ATTR_LOAD_IDLE] = round(bridge.processes.load.currentLoadIdle, 2) return attrs + + +class BridgeBiosVersionSensor(BridgeSensor): + """Defines a bios version sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: + """Initialize System Bridge sensor.""" + super().__init__( + coordinator, + bridge, + "bios_version", + "BIOS Version", + "mdi:chip", + None, + None, + False, + ) + + @property + def state(self) -> str: + """Return the state of the sensor.""" + bridge: Bridge = self.coordinator.data + return bridge.system.bios.version diff --git a/requirements_all.txt b/requirements_all.txt index 63f7d857f89..29b964099c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2193,7 +2193,7 @@ synology-srm==0.2.0 synologydsm-api==1.0.2 # homeassistant.components.system_bridge -systembridge==1.1.3 +systembridge==1.1.5 # homeassistant.components.tahoma tahoma-api==0.0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7044c838c83..235397a9560 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1180,7 +1180,7 @@ surepy==0.6.0 synologydsm-api==1.0.2 # homeassistant.components.system_bridge -systembridge==1.1.3 +systembridge==1.1.5 # homeassistant.components.tellduslive tellduslive==0.10.11 From 07a93be1764c25a400eebabc57808cd6bd3c104e Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sat, 8 May 2021 23:09:31 -0400 Subject: [PATCH 256/852] Revert Rachio to seconds instead of total_seconds (#50307) * revert seconds * Add comment * Update comment to include max runtime --- homeassistant/components/rachio/switch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 65249d0b8ea..de897cb7f07 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -418,9 +418,8 @@ class RachioZone(RachioSwitch): CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS ) ) - self._controller.rachio.zone.start( - self.zone_id, manual_run_time.total_seconds() - ) + # The API limit is 3 hours, and requires an int be passed + self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds) _LOGGER.debug( "Watering %s on %s for %s", self.name, From 4d9d565ecc7613f67c35a1e130007a45b7484987 Mon Sep 17 00:00:00 2001 From: hubbergit Date: Sun, 9 May 2021 10:07:51 +0200 Subject: [PATCH 257/852] Fix unit of measurement from Pa to hPa (#49664) The unit of measurement should be hPa instead of Pa for the air pressure. --- homeassistant/components/luftdaten/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 6db0ad96f64..6775b9ec023 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_SENSORS, CONF_SHOW_ON_MAP, PERCENTAGE, - PRESSURE_PA, + PRESSURE_HPA, TEMP_CELSIUS, ) from homeassistant.core import callback @@ -47,8 +47,8 @@ TOPIC_UPDATE = f"{DOMAIN}_data_update" SENSORS = { SENSOR_TEMPERATURE: ["Temperature", "mdi:thermometer", TEMP_CELSIUS], SENSOR_HUMIDITY: ["Humidity", "mdi:water-percent", PERCENTAGE], - SENSOR_PRESSURE: ["Pressure", "mdi:arrow-down-bold", PRESSURE_PA], - SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:download", PRESSURE_PA], + SENSOR_PRESSURE: ["Pressure", "mdi:arrow-down-bold", PRESSURE_HPA], + SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:download", PRESSURE_HPA], SENSOR_PM10: [ "PM10", "mdi:thought-bubble", From 85d782808c532bb5d0c5da885ce176a7f85a896f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 9 May 2021 04:09:56 -0500 Subject: [PATCH 258/852] Fix Sonos polling bug (#50265) --- homeassistant/components/sonos/speaker.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 73704c61364..03cce67e4d8 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -4,6 +4,7 @@ from __future__ import annotations from asyncio import gather import contextlib import datetime +from functools import partial import logging from typing import Any, Callable @@ -223,7 +224,11 @@ class SonosSpeaker: return self._poll_timer = self.hass.helpers.event.async_track_time_interval( - async_dispatcher_send(self.hass, f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}"), + partial( + async_dispatcher_send, + self.hass, + f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}", + ), SCAN_INTERVAL, ) From 9cab8a19cd497d5cc0865f469b64f8f25a2bdcb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 May 2021 04:19:26 -0500 Subject: [PATCH 259/852] Add iCloud discovery (#50304) Since homekit requires iCloud keychains to be enabled, if they have a homekit hub, they must have iCloud. --- homeassistant/components/icloud/manifest.json | 1 + homeassistant/generated/zeroconf.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 6c40ef6bf03..4e07ebd2573 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/icloud", "requirements": ["pyicloud==0.10.2"], "codeowners": ["@Quentame", "@nzapponi"], + "zeroconf": ["_homekit._tcp.local."], "iot_class": "cloud_polling" } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index d4e490170d0..0826dc4a593 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -91,6 +91,9 @@ ZEROCONF = { "_homekit._tcp.local.": [ { "domain": "homekit" + }, + { + "domain": "icloud" } ], "_http._tcp.local.": [ From 29cd5f20b9e8ada29888629302d3b55bb84b59dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 May 2021 04:41:27 -0500 Subject: [PATCH 260/852] Add zeroconf discovery to powerview (#50308) --- .../components/hunterdouglas_powerview/config_flow.py | 10 ++++++++++ .../components/hunterdouglas_powerview/manifest.json | 1 + homeassistant/generated/zeroconf.py | 5 +++++ .../hunterdouglas_powerview/test_config_flow.py | 6 ++++++ 4 files changed, 22 insertions(+) diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 869d703b641..6ff7a0027ba 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -19,6 +19,7 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) HAP_SUFFIX = "._hap._tcp.local." +POWERVIEW_SUFFIX = "._powerview._tcp.local." async def validate_input(hass: core.HomeAssistant, hub_address: str) -> dict[str, str]: @@ -91,6 +92,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.discovered_name = discovery_info[HOSTNAME] return await self.async_step_discovery_confirm() + async def async_step_zeroconf(self, discovery_info): + """Handle zeroconf discovery.""" + self.discovered_ip = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + if name.endswith(POWERVIEW_SUFFIX): + name = name[: -len(POWERVIEW_SUFFIX)] + self.discovered_name = name + return await self.async_step_discovery_confirm() + async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" self.discovered_ip = discovery_info[CONF_HOST] diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 15a993d6d48..ad763a33bc8 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -14,5 +14,6 @@ "macaddress": "002674*" } ], + "zeroconf": ["_powerview._tcp.local."], "iot_class": "local_polling" } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 0826dc4a593..8b6075d9e3b 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -158,6 +158,11 @@ ZEROCONF = { "domain": "plugwise" } ], + "_powerview._tcp.local.": [ + { + "domain": "hunterdouglas_powerview" + } + ], "_printer._tcp.local.": [ { "domain": "brother", diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 60ee532c73f..712ebea64a9 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -16,6 +16,11 @@ HOMEKIT_DISCOVERY_INFO = { "properties": {"id": "AA::BB::CC::DD::EE::FF"}, } +ZEROCONF_DISCOVERY_INFO = { + "name": "Hunter Douglas Powerview Hub._powerview._tcp.local.", + "host": "1.2.3.4", +} + DHCP_DISCOVERY_INFO = {"hostname": "Hunter Douglas Powerview Hub", "ip": "1.2.3.4"} DISCOVERY_DATA = [ @@ -27,6 +32,7 @@ DISCOVERY_DATA = [ config_entries.SOURCE_DHCP, DHCP_DISCOVERY_INFO, ), + (config_entries.SOURCE_ZEROCONF, ZEROCONF_DISCOVERY_INFO), ] From 2bff7f8020728dd8769c1e841aa0ae57c32c8072 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 May 2021 04:46:07 -0500 Subject: [PATCH 261/852] Remove YAML support from gogogate2/ismartgate (#50312) --- .../components/gogogate2/config_flow.py | 11 +-- homeassistant/components/gogogate2/cover.py | 21 +---- tests/components/gogogate2/test_cover.py | 87 ------------------- 3 files changed, 2 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index 94ac97a3ef2..bfe740ecaa5 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -6,7 +6,7 @@ from ismartgate.common import AbstractInfoResponse, ApiError from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigFlow +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_DEVICE, CONF_IP_ADDRESS, @@ -28,12 +28,6 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): self._ip_address = None self._device_type = None - async def async_step_import(self, config_data: dict = None): - """Handle importing of configuration.""" - result = await self.async_step_user(config_data) - self._abort_if_unique_id_configured() - return result - async def async_step_homekit(self, discovery_info): """Handle homekit discovery.""" await self.async_set_unique_id(discovery_info["properties"]["id"]) @@ -91,9 +85,6 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except errors["base"] = "cannot_connect" - if errors and self.source == SOURCE_IMPORT: - return self.async_abort(reason="cannot_connect") - return self.async_show_form( step_id="user", data_schema=vol.Schema( diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 1410bc9e97d..073c48e55b8 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -17,7 +17,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, CoverEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -27,29 +27,10 @@ from .common import ( cover_unique_id, get_data_update_coordinator, ) -from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass: HomeAssistant, - config: dict, - add_entities: AddEntitiesCallback, - discovery_info=None, -) -> None: - """Convert old style file configs to new style configs.""" - _LOGGER.warning( - "Loading gogogate2 via platform config is deprecated; The configuration" - " has been migrated to a config entry and can be safely removed" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 9c4f3ee8e69..5391bf1e4aa 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch from ismartgate import GogoGate2Api, ISmartGateApi from ismartgate.common import ( - ApiError, DoorMode, DoorStatus, GogoGate2ActivateResponse, @@ -31,18 +30,12 @@ from homeassistant.components.gogogate2.const import ( DOMAIN, MANUFACTURER, ) -from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN -from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_DEVICE, CONF_IP_ADDRESS, - CONF_NAME, CONF_PASSWORD, - CONF_PLATFORM, - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_METRIC, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING, @@ -52,7 +45,6 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed, mock_device_registry @@ -184,85 +176,6 @@ def _mocked_ismartgate_closed_door_response(): ) -@patch("homeassistant.components.gogogate2.common.GogoGate2Api") -async def test_import_fail(gogogate2api_mock, hass: HomeAssistant) -> None: - """Test the failure to import.""" - api = MagicMock(spec=GogoGate2Api) - api.async_info.side_effect = ApiError(22, "Error") - gogogate2api_mock.return_value = api - - hass_config = { - HA_DOMAIN: {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, - COVER_DOMAIN: [ - { - CONF_PLATFORM: "gogogate2", - CONF_NAME: "cover0", - CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, - CONF_IP_ADDRESS: "127.0.1.0", - CONF_USERNAME: "user0", - CONF_PASSWORD: "password0", - } - ], - } - - await async_process_ha_core_config(hass, hass_config[HA_DOMAIN]) - assert await async_setup_component(hass, HA_DOMAIN, {}) - assert await async_setup_component(hass, COVER_DOMAIN, hass_config) - await hass.async_block_till_done() - - entity_ids = hass.states.async_entity_ids(COVER_DOMAIN) - assert not entity_ids - - -@patch("homeassistant.components.gogogate2.common.GogoGate2Api") -@patch("homeassistant.components.gogogate2.common.ISmartGateApi") -async def test_import( - ismartgateapi_mock, gogogate2api_mock, hass: HomeAssistant -) -> None: - """Test importing of file based config.""" - api0 = MagicMock(spec=GogoGate2Api) - api0.async_info.return_value = _mocked_gogogate_open_door_response() - gogogate2api_mock.return_value = api0 - - api1 = MagicMock(spec=ISmartGateApi) - api1.async_info.return_value = _mocked_ismartgate_closed_door_response() - ismartgateapi_mock.return_value = api1 - - hass_config = { - HA_DOMAIN: {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, - COVER_DOMAIN: [ - { - CONF_PLATFORM: "gogogate2", - CONF_NAME: "cover0", - CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, - CONF_IP_ADDRESS: "127.0.1.0", - CONF_USERNAME: "user0", - CONF_PASSWORD: "password0", - }, - { - CONF_PLATFORM: "gogogate2", - CONF_NAME: "cover1", - CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, - CONF_IP_ADDRESS: "127.0.1.1", - CONF_USERNAME: "user1", - CONF_PASSWORD: "password1", - }, - ], - } - - await async_process_ha_core_config(hass, hass_config[HA_DOMAIN]) - assert await async_setup_component(hass, HA_DOMAIN, {}) - assert await async_setup_component(hass, COVER_DOMAIN, hass_config) - await hass.async_block_till_done() - - entity_ids = hass.states.async_entity_ids(COVER_DOMAIN) - assert entity_ids is not None - assert len(entity_ids) == 3 - assert "cover.door1" in entity_ids - assert "cover.door1_2" in entity_ids - assert "cover.door2" in entity_ids - - @patch("homeassistant.components.gogogate2.common.GogoGate2Api") async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None: """Test open and close and data update.""" From cbf46328955ef62560295471159414242f8c203c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 12:03:41 +0200 Subject: [PATCH 262/852] Remove YAML configuration from SolarEdge (#50105) * Remove YAML configuration from SolarEdge * Restore already setup tests --- .../components/solaredge/__init__.py | 38 ++----------------- .../components/solaredge/config_flow.py | 8 ---- .../components/solaredge/test_config_flow.py | 31 +-------------- 3 files changed, 4 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index f01226bcb45..424371e9002 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -1,45 +1,13 @@ """The solaredge integration.""" from __future__ import annotations -from typing import Any - -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN +from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_SITE_ID): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: - """Platform setup, do nothing.""" - if DOMAIN not in config: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config[DOMAIN]) - ) - ) - return True +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 222bc27cdb2..523c75116e9 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -89,11 +89,3 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), errors=self._errors, ) - - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Import a config entry.""" - if self._site_in_configuration_exists(user_input[CONF_SITE_ID]): - return self.async_abort(reason="already_configured") - return await self.async_step_user(user_input) diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index bb21607fead..c01a5b6827c 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -51,43 +51,14 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: assert result["data"][CONF_API_KEY] == API_KEY -async def test_import(hass: HomeAssistant, test_api: Mock) -> None: - """Test import step.""" - flow = init_config_flow(hass) - - # import with site_id and api_key - result = await flow.async_step_import( - {CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "solaredge" - assert result["data"][CONF_SITE_ID] == SITE_ID - assert result["data"][CONF_API_KEY] == API_KEY - - # import with all - result = await flow.async_step_import( - {CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID, CONF_NAME: NAME} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "solaredge_site_1_2_3" - assert result["data"][CONF_SITE_ID] == SITE_ID - assert result["data"][CONF_API_KEY] == API_KEY - - async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> None: """Test we abort if the site_id is already setup.""" - flow = init_config_flow(hass) MockConfigEntry( domain="solaredge", data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}, ).add_to_hass(hass) - # import: Should fail, same SITE_ID - result = await flow.async_step_import( - {CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + flow = init_config_flow(hass) # user: Should fail, same SITE_ID result = await flow.async_step_user( From f814a7a8ae186e5057816ebf060b6c6fa2d0a07f Mon Sep 17 00:00:00 2001 From: gabrialdestruir <33361700+gabrialdestruir@users.noreply.github.com> Date: Sun, 9 May 2021 07:27:49 -0700 Subject: [PATCH 263/852] Add tplink light setting ignore default (#50334) This fixes issue #50115 by allowing color, brightness, and temperature to be set from an off state. This adds code to allow "ignore_default=1" to be sent to bulb letting it know to power on with the parameters set. --- homeassistant/components/tplink/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 61123bd6353..5984698a796 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -43,6 +43,7 @@ ATTR_DAILY_ENERGY_KWH = "daily_energy_kwh" ATTR_MONTHLY_ENERGY_KWH = "monthly_energy_kwh" LIGHT_STATE_DFT_ON = "dft_on_state" +LIGHT_STATE_DFT_IGNORE = "ignore_default" LIGHT_STATE_ON_OFF = "on_off" LIGHT_STATE_RELAY_STATE = "relay_state" LIGHT_STATE_BRIGHTNESS = "brightness" @@ -117,6 +118,7 @@ class LightState(NamedTuple): return { LIGHT_STATE_ON_OFF: 1 if self.state else 0, + LIGHT_STATE_DFT_IGNORE: 1 if self.state else 0, LIGHT_STATE_BRIGHTNESS: brightness_to_percentage(self.brightness), LIGHT_STATE_COLOR_TEMP: color_temp, LIGHT_STATE_HUE: self.hs[0] if self.hs else 0, From a74aa9272c7e98f16eaeb868900df1ff42a2be5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 May 2021 10:03:41 -0500 Subject: [PATCH 264/852] Add dhcp discovery to tplink (#50303) --- homeassistant/components/tplink/manifest.json | 64 +++++++++++++++- homeassistant/generated/dhcp.py | 75 +++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 2cb4b5f369f..fa8c32c35d7 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -5,5 +5,67 @@ "documentation": "https://www.home-assistant.io/integrations/tplink", "requirements": ["pyHS100==0.3.5.2"], "codeowners": ["@rytilahti", "@thegardenmonkey"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "dhcp": [ + { + "hostname": "hs*", + "macaddress": "1C3BF3*" + }, + { + "hostname": "hs*", + "macaddress": "50C7BF*" + }, + { + "hostname": "hs*", + "macaddress": "68FF7B*" + }, + { + "hostname": "hs*", + "macaddress": "98DAC4*" + }, + { + "hostname": "hs*", + "macaddress": "B09575*" + }, + { + "hostname": "k[lp]*", + "macaddress": "1C3BF3*" + }, + { + "hostname": "k[lp]*", + "macaddress": "50C7BF*" + }, + { + "hostname": "k[lp]*", + "macaddress": "68FF7B*" + }, + { + "hostname": "k[lp]*", + "macaddress": "98DAC4*" + }, + { + "hostname": "k[lp]*", + "macaddress": "B09575*" + }, + { + "hostname": "lb*", + "macaddress": "1C3BF3*" + }, + { + "hostname": "lb*", + "macaddress": "50C7BF*" + }, + { + "hostname": "lb*", + "macaddress": "68FF7B*" + }, + { + "hostname": "lb*", + "macaddress": "98DAC4*" + }, + { + "hostname": "lb*", + "macaddress": "B09575*" + } + ] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index d53cf133ac1..06783b5d666 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -208,6 +208,81 @@ DHCP = [ "hostname": "eneco-*", "macaddress": "74C63B*" }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "1C3BF3*" + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "50C7BF*" + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "68FF7B*" + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "98DAC4*" + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "B09575*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "1C3BF3*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "50C7BF*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "68FF7B*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "98DAC4*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "B09575*" + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "1C3BF3*" + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "50C7BF*" + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "68FF7B*" + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "98DAC4*" + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "B09575*" + }, { "domain": "tuya", "macaddress": "508A06*" From ce02614780285ba532eb12f7d0540ae88b9e13fb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 17:43:56 +0200 Subject: [PATCH 265/852] Deprecate Hive YAML configuration (#50357) --- homeassistant/components/hive/__init__.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 19ed6beedf9..aa05daf8e58 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -22,15 +22,18 @@ from .const import DOMAIN, PLATFORM_LOOKUP, PLATFORMS _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=2): cv.positive_int, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=2): cv.positive_int, + }, + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From e9709d4449203a36d813ef23983bf0fac78bb054 Mon Sep 17 00:00:00 2001 From: EddyK69 Date: Sun, 9 May 2021 17:51:58 +0200 Subject: [PATCH 266/852] Add LastTrip sensors for BMW Connected Drive (#45906) --- .../components/bmw_connected_drive/sensor.py | 176 ++++++++++++++---- 1 file changed, 144 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 38415d0006f..48d28e26f8a 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,19 +1,24 @@ """Support for reading vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.const import SERVICE_LAST_TRIP, SERVICE_STATUS from bimmer_connected.state import ChargingState from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, + DEVICE_CLASS_TIMESTAMP, + ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, TIME_HOURS, + TIME_MINUTES, VOLUME_GALLONS, VOLUME_LITERS, ) from homeassistant.helpers.icon import icon_for_battery_level +import homeassistant.util.dt as dt_util from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity from .const import CONF_ACCOUNT, DATA_ENTRIES @@ -21,31 +26,89 @@ from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) ATTR_TO_HA_METRIC = { - "mileage": ["mdi:speedometer", 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", TIME_HOURS], - "charging_status": ["mdi:battery-charging", None], - # No icon as this is dealt with directly as a special case in icon() - "charging_level_hv": [None, PERCENTAGE], + # "": [, , , ], + "mileage": ["mdi:speedometer", None, LENGTH_KILOMETERS, True], + "remaining_range_total": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], + "remaining_range_electric": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + True, + ], + "remaining_range_fuel": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], + "max_range_electric": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], + "remaining_fuel": ["mdi:gas-station", None, VOLUME_LITERS, True], + # LastTrip attributes + "average_combined_consumption": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + True, + ], + "average_electric_consumption": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + True, + ], + "average_recuperation": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + True, + ], + "electric_distance": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], + "saved_fuel": ["mdi:fuel", None, VOLUME_LITERS, False], + "total_distance": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], } ATTR_TO_HA_IMPERIAL = { - "mileage": ["mdi:speedometer", 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", TIME_HOURS], - "charging_status": ["mdi:battery-charging", None], - # No icon as this is dealt with directly as a special case in icon() - "charging_level_hv": [None, PERCENTAGE], + # "": [, , , ], + "mileage": ["mdi:speedometer", None, LENGTH_MILES, True], + "remaining_range_total": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + "remaining_range_electric": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + "remaining_range_fuel": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + "max_range_electric": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + "remaining_fuel": ["mdi:gas-station", None, VOLUME_GALLONS, True], + # LastTrip attributes + "average_combined_consumption": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ], + "average_electric_consumption": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ], + "average_recuperation": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ], + "electric_distance": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + "saved_fuel": ["mdi:fuel", None, VOLUME_GALLONS, False], + "total_distance": ["mdi:map-marker-distance", None, LENGTH_MILES, True], } +ATTR_TO_HA_GENERIC = { + # "": [, , , ], + "charging_time_remaining": ["mdi:update", None, TIME_HOURS, True], + "charging_status": ["mdi:battery-charging", None, None, True], + # No icon as this is dealt with directly as a special case in icon() + "charging_level_hv": [None, None, PERCENTAGE, True], + # LastTrip attributes + "date_utc": [None, DEVICE_CLASS_TIMESTAMP, None, True], + "duration": ["mdi:timer-outline", None, TIME_MINUTES, True], + "electric_distance_ratio": ["mdi:percent-outline", None, PERCENTAGE, False], +} + +ATTR_TO_HA_METRIC.update(ATTR_TO_HA_GENERIC) +ATTR_TO_HA_IMPERIAL.update(ATTR_TO_HA_GENERIC) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the BMW ConnectedDrive sensors from config entry.""" @@ -58,26 +121,54 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for vehicle in account.account.vehicles: - for attribute_name in vehicle.drive_train_attributes: - if attribute_name in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info - ) - entities.append(device) + for service in vehicle.available_state_services: + if service == SERVICE_STATUS: + for attribute_name in vehicle.drive_train_attributes: + if attribute_name in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info + ) + entities.append(device) + if service == SERVICE_LAST_TRIP: + for attribute_name in vehicle.state.last_trip.available_attributes: + if attribute_name == "date": + device = BMWConnectedDriveSensor( + account, + vehicle, + "date_utc", + attribute_info, + service, + ) + entities.append(device) + else: + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info, service + ) + entities.append(device) + async_add_entities(entities, True) class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str, attribute_info): + def __init__(self, account, vehicle, attribute: str, attribute_info, service=None): """Initialize BMW vehicle sensor.""" super().__init__(account, vehicle) self._attribute = attribute + self._service = service self._state = None - self._name = f"{self._vehicle.name} {self._attribute}" - self._unique_id = f"{self._vehicle.vin}-{self._attribute}" + if self._service: + self._name = ( + f"{self._vehicle.name} {self._service.lower()}_{self._attribute}" + ) + self._unique_id = ( + f"{self._vehicle.vin}-{self._service.lower()}-{self._attribute}" + ) + else: + self._name = f"{self._vehicle.name} {self._attribute}" + self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._attribute_info = attribute_info @property @@ -100,9 +191,17 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): return icon_for_battery_level( battery_level=vehicle_state.charging_level_hv, charging=charging_state ) - icon, _ = self._attribute_info.get(self._attribute, [None, None]) + icon = self._attribute_info.get(self._attribute, [None, None, None, None])[0] return icon + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + enabled_default = self._attribute_info.get( + self._attribute, [None, None, None, True] + )[3] + return enabled_default + @property def state(self): """Return the state of the sensor. @@ -112,16 +211,23 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): """ return self._state + @property + def device_class(self) -> str: + """Get the device class.""" + clss = self._attribute_info.get(self._attribute, [None, None, None, None])[1] + return clss + @property def unit_of_measurement(self) -> str: """Get the unit of measurement.""" - unit = self._attribute_info.get(self._attribute, [None, None])[1] + unit = self._attribute_info.get(self._attribute, [None, None, None, None])[2] return unit def update(self) -> None: """Read new state data from the library.""" _LOGGER.debug("Updating %s", self._vehicle.name) vehicle_state = self._vehicle.state + vehicle_last_trip = self._vehicle.state.last_trip if self._attribute == "charging_status": self._state = getattr(vehicle_state, self._attribute).value elif self.unit_of_measurement == VOLUME_GALLONS: @@ -132,5 +238,11 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): value = getattr(vehicle_state, self._attribute) value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) self._state = round(value_converted) - else: + elif self._service is None: self._state = getattr(vehicle_state, self._attribute) + elif self._service == SERVICE_LAST_TRIP: + if self._attribute == "date_utc": + date_str = getattr(vehicle_last_trip, "date") + self._state = dt_util.parse_datetime(date_str).isoformat() + else: + self._state = getattr(vehicle_last_trip, self._attribute) From 9b058551f7731b71547fe5df9a46cb5a55037ed1 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Sun, 9 May 2021 17:04:57 +0100 Subject: [PATCH 267/852] Enable type checks for camera platform (#50179) --- homeassistant/components/camera/__init__.py | 20 ++++++++++++-------- homeassistant/components/camera/const.py | 18 ++++++++++-------- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 3a2fe8ba417..054ded44d14 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -1,4 +1,6 @@ """Component to interface with cameras.""" +from __future__ import annotations + import asyncio import base64 import collections @@ -8,7 +10,7 @@ import hashlib import logging import os from random import SystemRandom -from typing import final +from typing import cast, final from aiohttp import web import async_timeout @@ -347,7 +349,7 @@ class Camera(Entity): """Return the interval between frames of the mjpeg stream.""" return 0.5 - async def create_stream(self) -> Stream: + async def create_stream(self) -> Stream | None: """Create a Stream for stream_source.""" # There is at most one stream (a decode worker) per camera if not self.stream: @@ -471,6 +473,8 @@ class CameraView(HomeAssistantView): if camera is None: raise web.HTTPNotFound() + camera = cast(Camera, camera) + authenticated = ( request[KEY_AUTHENTICATED] or request.query.get("token") in camera.access_tokens @@ -516,13 +520,13 @@ class CameraMjpegStream(CameraView): async def handle(self, request: web.Request, camera: Camera) -> web.Response: """Serve camera stream, possibly with interval.""" - interval = request.query.get("interval") - if interval is None: + interval_str = request.query.get("interval") + if interval_str is None: return await camera.handle_async_mjpeg_stream(request) try: # Compose camera stream from stills - interval = float(request.query.get("interval")) + interval = float(interval_str) if interval < MIN_STREAM_INTERVAL: raise ValueError(f"Stream interval must be be > {MIN_STREAM_INTERVAL}") return await camera.handle_async_still_stream(request, interval) @@ -554,7 +558,6 @@ async def websocket_camera_thumbnail(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "camera/stream", @@ -562,6 +565,7 @@ async def websocket_camera_thumbnail(hass, connection, msg): vol.Optional("format", default="hls"): vol.In(OUTPUT_FORMATS), } ) +@websocket_api.async_response async def ws_camera_stream(hass, connection, msg): """Handle get camera stream websocket command. @@ -582,17 +586,16 @@ async def ws_camera_stream(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command( {vol.Required("type"): "camera/get_prefs", vol.Required("entity_id"): cv.entity_id} ) +@websocket_api.async_response async def websocket_get_prefs(hass, connection, msg): """Handle request for account info.""" prefs = hass.data[DATA_CAMERA_PREFS].get(msg["entity_id"]) connection.send_result(msg["id"], prefs.as_dict()) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "camera/update_prefs", @@ -600,6 +603,7 @@ async def websocket_get_prefs(hass, connection, msg): vol.Optional("preload_stream"): bool, } ) +@websocket_api.async_response async def websocket_update_prefs(hass, connection, msg): """Handle request for account info.""" prefs = hass.data[DATA_CAMERA_PREFS] diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index 7218b19f8fe..2cb01f44aa9 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -1,14 +1,16 @@ """Constants for Camera component.""" -DOMAIN = "camera" +from typing import Final -DATA_CAMERA_PREFS = "camera_prefs" +DOMAIN: Final = "camera" -PREF_PRELOAD_STREAM = "preload_stream" +DATA_CAMERA_PREFS: Final = "camera_prefs" -SERVICE_RECORD = "record" +PREF_PRELOAD_STREAM: Final = "preload_stream" -CONF_LOOKBACK = "lookback" -CONF_DURATION = "duration" +SERVICE_RECORD: Final = "record" -CAMERA_STREAM_SOURCE_TIMEOUT = 10 -CAMERA_IMAGE_TIMEOUT = 10 +CONF_LOOKBACK: Final = "lookback" +CONF_DURATION: Final = "duration" + +CAMERA_STREAM_SOURCE_TIMEOUT: Final = 10 +CAMERA_IMAGE_TIMEOUT: Final = 10 diff --git a/mypy.ini b/mypy.ini index 4624aab9dd8..ff5971ed943 100644 --- a/mypy.ini +++ b/mypy.ini @@ -710,9 +710,6 @@ ignore_errors = true [mypy-homeassistant.components.bsblan.*] ignore_errors = true -[mypy-homeassistant.components.camera.*] -ignore_errors = true - [mypy-homeassistant.components.canary.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f041ba03c5b..5c375a403da 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -31,7 +31,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.bluetooth_tracker.*", "homeassistant.components.bmw_connected_drive.*", "homeassistant.components.bsblan.*", - "homeassistant.components.camera.*", "homeassistant.components.canary.*", "homeassistant.components.cast.*", "homeassistant.components.cert_expiry.*", From 0a95aa282c9bbc32e3dfd2783a42d71ed7b1a60d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 18:29:28 +0200 Subject: [PATCH 268/852] Upgrade debugpy to 1.3.0 (#50356) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 5820887c0c0..b82f544329c 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.2.1"], + "requirements": ["debugpy==1.3.0"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 29b964099c5..1e914db9fd7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.2.1 +debugpy==1.3.0 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 235397a9560..b29d4252316 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -258,7 +258,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.2.1 +debugpy==1.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 2ca0eb61dcdbee7209fe44b35ff1725ca8f8e1fb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 19:23:55 +0200 Subject: [PATCH 269/852] Deprecate Pi-hole YAML configuration (#50358) --- homeassistant/components/pi_hole/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 7e897887d8d..34fdc9978c1 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -52,7 +52,10 @@ PI_HOLE_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [PI_HOLE_SCHEMA]))}, + vol.All( + cv.deprecated(DOMAIN), + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [PI_HOLE_SCHEMA]))}, + ), extra=vol.ALLOW_EXTRA, ) From 4e4042a869f0ff3966cd36882f9c57a031fb37ac Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 9 May 2021 10:34:21 -0700 Subject: [PATCH 270/852] Fix types for WLED (#50001) --- homeassistant/components/wled/__init__.py | 21 +++++++++++--------- homeassistant/components/wled/config_flow.py | 19 +++++++++++------- homeassistant/components/wled/const.py | 3 --- homeassistant/components/wled/light.py | 20 +++++++++++-------- homeassistant/components/wled/sensor.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - tests/components/wled/test_config_flow.py | 13 ------------ 8 files changed, 37 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index b875f65bf42..f7b9a69d1de 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -10,7 +10,14 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, + CONF_HOST, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo @@ -20,13 +27,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_SOFTWARE_VERSION, - DOMAIN, -) +from .const import DOMAIN SCAN_INTERVAL = timedelta(seconds=5) PLATFORMS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) @@ -128,6 +129,8 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): class WLEDEntity(CoordinatorEntity): """Defines a base WLED entity.""" + coordinator: WLEDDataUpdateCoordinator + def __init__( self, *, @@ -172,5 +175,5 @@ class WLEDDeviceEntity(WLEDEntity): ATTR_NAME: self.coordinator.data.info.name, ATTR_MANUFACTURER: self.coordinator.data.info.brand, ATTR_MODEL: self.coordinator.data.info.product, - ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, + ATTR_SW_VERSION: self.coordinator.data.info.version, } diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index a06ab3d37ab..bb9d4c0cfe5 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure the WLED integration.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from wled import WLED, WLEDConnectionError @@ -8,7 +10,7 @@ from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -18,16 +20,16 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" return await self._handle_config_flow(user_input) async def async_step_zeroconf( - self, discovery_info: ConfigType | None = None + self, discovery_info: DiscoveryInfoType ) -> FlowResult: """Handle zeroconf discovery.""" - if discovery_info is None: - return self.async_abort(reason="cannot_connect") # Hostname is format: wled-livingroom.local. host = discovery_info["hostname"].rstrip(".") @@ -46,13 +48,13 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): return await self._handle_config_flow(discovery_info, True) async def async_step_zeroconf_confirm( - self, user_input: ConfigType = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by zeroconf.""" return await self._handle_config_flow(user_input) async def _handle_config_flow( - self, user_input: ConfigType | None = None, prepare: bool = False + self, user_input: dict[str, Any] | None = None, prepare: bool = False ) -> FlowResult: """Config flow handler for WLED.""" source = self.context.get("source") @@ -63,6 +65,9 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): return self._show_confirm_dialog() return self._show_setup_form() + # if prepare is True, user_input can not be None. + assert user_input is not None + if source == SOURCE_ZEROCONF: user_input[CONF_HOST] = self.context.get(CONF_HOST) user_input[CONF_MAC] = self.context.get(CONF_MAC) diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index e0880dd40fd..7cc52601d79 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -7,12 +7,9 @@ DOMAIN = "wled" ATTR_COLOR_PRIMARY = "color_primary" ATTR_DURATION = "duration" ATTR_FADE = "fade" -ATTR_IDENTIFIERS = "identifiers" ATTR_INTENSITY = "intensity" ATTR_LED_COUNT = "led_count" -ATTR_MANUFACTURER = "manufacturer" ATTR_MAX_POWER = "max_power" -ATTR_MODEL = "model" ATTR_ON = "on" ATTR_PALETTE = "palette" ATTR_PLAYLIST = "playlist" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 0151737ece9..f6a51e6159a 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -58,6 +58,7 @@ async def async_setup_entry( coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( SERVICE_EFFECT, { @@ -127,7 +128,7 @@ class WLEDMasterLight(LightEntity, WLEDDeviceEntity): @wled_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" - data = {ATTR_ON: False} + data: dict[str, bool | int] = {ATTR_ON: False} if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. @@ -138,7 +139,7 @@ class WLEDMasterLight(LightEntity, WLEDDeviceEntity): @wled_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - data = {ATTR_ON: True} + data: dict[str, bool | int] = {ATTR_ON: True} if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. @@ -230,7 +231,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): } @property - def hs_color(self) -> tuple[float, float] | None: + def hs_color(self) -> tuple[float, float]: """Return the hue and saturation color value [float, float].""" color = self.coordinator.data.state.segments[self._segment].color_primary return color_util.color_RGB_to_hs(*color[:3]) @@ -295,7 +296,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): @wled_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" - data = {ATTR_ON: False} + data: dict[str, bool | int] = {ATTR_ON: False} if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. @@ -312,7 +313,10 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): @wled_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - data = {ATTR_ON: True, ATTR_SEGMENT_ID: self._segment} + data: dict[str, Any] = { + ATTR_ON: True, + ATTR_SEGMENT_ID: self._segment, + } if ATTR_COLOR_TEMP in kwargs: mireds = color_util.color_temperature_kelvin_to_mired( @@ -385,7 +389,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): speed: int | None = None, ) -> None: """Set the effect of a WLED light.""" - data = {ATTR_SEGMENT_ID: self._segment} + data: dict[str, bool | int | str | None] = {ATTR_SEGMENT_ID: self._segment} if effect is not None: data[ATTR_EFFECT] = effect @@ -419,7 +423,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): def async_update_segments( entry: ConfigEntry, coordinator: WLEDDataUpdateCoordinator, - current: dict[int, WLEDSegmentLight], + current: dict[int, WLEDSegmentLight | WLEDMasterLight], async_add_entities, ) -> None: """Update segments.""" @@ -459,7 +463,7 @@ def async_update_segments( async def async_remove_entity( index: int, coordinator: WLEDDataUpdateCoordinator, - current: dict[int, WLEDSegmentLight], + current: dict[int, WLEDSegmentLight | WLEDMasterLight], ) -> None: """Remove WLED segment light from Home Assistant.""" entity = current[index] diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index eff40caea22..4c104e1c936 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -74,7 +74,7 @@ class WLEDSensor(WLEDDeviceEntity, SensorEntity): return f"{self.coordinator.data.info.mac_address}_{self._key}" @property - def unit_of_measurement(self) -> str: + def unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/mypy.ini b/mypy.ini index ff5971ed943..3f57c90f2a8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1325,9 +1325,6 @@ ignore_errors = true [mypy-homeassistant.components.withings.*] ignore_errors = true -[mypy-homeassistant.components.wled.*] -ignore_errors = true - [mypy-homeassistant.components.wunderground.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 5c375a403da..b806fcdbf46 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -236,7 +236,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.wemo.*", "homeassistant.components.wink.*", "homeassistant.components.withings.*", - "homeassistant.components.wled.*", "homeassistant.components.wunderground.*", "homeassistant.components.xbox.*", "homeassistant.components.xiaomi_aqara.*", diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 4ed1723be77..60f2c59ecba 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -119,19 +119,6 @@ async def test_zeroconf_confirm_connection_error( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) -async def test_zeroconf_no_data( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort if zeroconf provides no data.""" - flow = config_flow.WLEDFlowHandler() - flow.hass = hass - result = await flow.async_step_zeroconf() - - assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - - async def test_user_device_exists_abort( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: From be73067f9c1408e1d28ca86f17abf1323134fcf0 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 9 May 2021 20:46:53 +0300 Subject: [PATCH 271/852] Fix Shelly type hints (#50322) --- homeassistant/components/shelly/__init__.py | 9 ++++++--- homeassistant/components/shelly/const.py | 2 ++ homeassistant/components/shelly/entity.py | 2 +- homeassistant/components/shelly/light.py | 3 ++- homeassistant/components/shelly/utils.py | 20 ++++++++++++-------- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 7 files changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 2da2c5a6ea5..d2c56217afe 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -90,8 +90,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) dev_reg = await device_registry.async_get_registry(hass) - identifier = (DOMAIN, entry.unique_id) - device_entry = dev_reg.async_get_device(identifiers={identifier}, connections=set()) + device_entry = None + if entry.unique_id is not None: + device_entry = dev_reg.async_get_device( + identifiers={(DOMAIN, entry.unique_id)}, connections=set() + ) if device_entry and entry.entry_id not in device_entry.config_entries: device_entry = None @@ -185,7 +188,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): self._async_remove_device_updates_handler = self.async_add_listener( self._async_device_updates_handler ) - self._last_input_events_count = {} + self._last_input_events_count: dict = {} hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index bff82057120..964dab31698 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -81,3 +81,5 @@ SHBTN_MODELS = ["SHBTN-1", "SHBTN-2"] KELVIN_MAX_VALUE = 6500 KELVIN_MIN_VALUE_WHITE = 2700 KELVIN_MIN_VALUE_COLOR = 3000 + +UPTIME_DEVIATION = 5 diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 675eead2155..9445c792c7b 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -178,7 +178,7 @@ class ShellyBlockEntity(entity.Entity): """Initialize Shelly entity.""" self.wrapper = wrapper self.block = block - self._name = get_entity_name(wrapper.device, block) + self._name: str | None = get_entity_name(wrapper.device, block) @property def name(self): diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index adca498c3f9..d6afcd8841e 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import logging +from typing import Any from aioshelly import Block import async_timeout @@ -212,7 +213,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): set_mode = None supported_color_modes = self._supported_color_modes - params = {"turn": "on"} + params: dict[str, Any] = {"turn": "on"} if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes): brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 2490eceaba5..37b34dfe9e8 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -1,7 +1,7 @@ """Shelly helpers functions.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging import aioshelly @@ -9,7 +9,7 @@ import aioshelly from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import singleton -from homeassistant.util.dt import parse_datetime, utcnow +from homeassistant.util.dt import utcnow from .const import ( BASIC_INPUTS_EVENTS_TYPES, @@ -21,6 +21,7 @@ from .const import ( SHBTN_INPUTS_EVENTS_TYPES, SHBTN_MODELS, SHIX3_1_INPUTS_EVENTS_TYPES, + UPTIME_DEVIATION, ) _LOGGER = logging.getLogger(__name__) @@ -118,6 +119,8 @@ def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: return True button = settings.get("relays") or settings.get("lights") or settings.get("inputs") + if button is None: + return False # Shelly 1L has two button settings in the first channel if settings["device"]["type"] == "SHSW-L": @@ -133,13 +136,14 @@ def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: def get_device_uptime(status: dict, last_uptime: str) -> str: """Return device uptime string, tolerate up to 5 seconds deviation.""" - uptime = utcnow() - timedelta(seconds=status["uptime"]) + delta_uptime = utcnow() - timedelta(seconds=status["uptime"]) - if not last_uptime: - return uptime.replace(microsecond=0).isoformat() - - if abs((uptime - parse_datetime(last_uptime)).total_seconds()) > 5: - return uptime.replace(microsecond=0).isoformat() + if ( + not last_uptime + or abs((delta_uptime - datetime.fromisoformat(last_uptime)).total_seconds()) + > UPTIME_DEVIATION + ): + return delta_uptime.replace(microsecond=0).isoformat() return last_uptime diff --git a/mypy.ini b/mypy.ini index 3f57c90f2a8..7d78261e238 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1175,9 +1175,6 @@ ignore_errors = true [mypy-homeassistant.components.sharkiq.*] ignore_errors = true -[mypy-homeassistant.components.shelly.*] -ignore_errors = true - [mypy-homeassistant.components.sma.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b806fcdbf46..85dcab6efef 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -186,7 +186,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sentry.*", "homeassistant.components.sesame.*", "homeassistant.components.sharkiq.*", - "homeassistant.components.shelly.*", "homeassistant.components.sma.*", "homeassistant.components.smart_meter_texas.*", "homeassistant.components.smartthings.*", From ec08256ff0b3dd62861aa6468619e9ce4ae4b57a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 9 May 2021 19:50:23 +0200 Subject: [PATCH 272/852] Do not use async_* in a modbus sync function (#50343) --- homeassistant/components/modbus/modbus.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index bafb4d01c3f..3c3eff9b5b0 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -22,7 +22,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.event import call_later from .const import ( ATTR_ADDRESS, @@ -109,7 +109,7 @@ def modbus_setup( hub_collect[client_name].write_coil(unit, address, state) # register function to gracefully stop modbus - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) # Register services for modbus hass.services.register( @@ -208,9 +208,7 @@ class ModbusHub: # Start counting down to allow modbus requests. if self._config_delay: - self._cancel_listener = async_call_later( - hass, self._config_delay, self.end_delay - ) + self._cancel_listener = call_later(hass, self._config_delay, self.end_delay) def end_delay(self, args): """End startup delay.""" From b2cee2e60212c1b053f6ecede2297c89b5aaabae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 20:26:26 +0200 Subject: [PATCH 273/852] Remove YAML configuration from Tuya (#50360) * Remove YAML configuration from Tuya * Keep deprecation warning --- homeassistant/components/tuya/__init__.py | 34 +---------------- homeassistant/components/tuya/config_flow.py | 13 +------ tests/components/tuya/test_config_flow.py | 40 -------------------- 3 files changed, 3 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 443042d8aff..86ba0e12c61 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -10,9 +10,8 @@ from tuyaha.tuyaapi import ( TuyaNetException, TuyaServerException, ) -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_PLATFORM, @@ -67,22 +66,7 @@ TUYA_TYPE_TO_HA = { TUYA_TRACKER = "tuya_tracker" STOP_CANCEL = "stop_event_cancel" -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_COUNTRYCODE): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PLATFORM, default="tuya"): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) def _update_discovery_interval(hass, interval): @@ -109,20 +93,6 @@ def _update_query_interval(hass, interval): _LOGGER.warning(ex) -async def async_setup(hass, config): - """Set up the Tuya integration.""" - - conf = config.get(DOMAIN) - if conf is not None: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Tuya platform.""" diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 6bec79a6470..1004a7844aa 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -89,7 +89,6 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._password = None self._platform = None self._username = None - self._is_import = False def _save_entry(self): return self.async_create_entry( @@ -116,11 +115,6 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return RESULT_SUCCESS - async def async_step_import(self, user_input=None): - """Handle configuration by yaml file.""" - self._is_import = True - return await self.async_step_user(user_input) - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if self._async_current_entries(): @@ -139,12 +133,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if result == RESULT_SUCCESS: return self._save_entry() - if result != RESULT_AUTH_FAILED or self._is_import: - if self._is_import: - _LOGGER.error( - "Error importing from configuration.yaml: %s", - RESULT_LOG_MESSAGE.get(result, "Generic Error"), - ) + if result != RESULT_AUTH_FAILED: return self.async_abort(reason=result) errors["base"] = result diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index ede6e5ac1db..6ea28cf8d2b 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -96,24 +96,6 @@ async def test_user(hass, tuya): assert not result["result"].unique_id -async def test_import(hass, tuya): - """Test import step.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=TUYA_USER_DATA, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_COUNTRYCODE] == COUNTRY_CODE - assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM - assert not result["result"].unique_id - - async def test_abort_if_already_setup(hass, tuya): """Test we abort if Tuya is already setup.""" MockConfigEntry(domain=DOMAIN, data=TUYA_USER_DATA).add_to_hass(hass) @@ -126,14 +108,6 @@ async def test_abort_if_already_setup(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == RESULT_SINGLE_INSTANCE - # Should fail, config exist (flow) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == RESULT_SINGLE_INSTANCE - async def test_abort_on_invalid_credentials(hass, tuya): """Test when we have invalid credentials.""" @@ -146,13 +120,6 @@ async def test_abort_on_invalid_credentials(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": RESULT_AUTH_FAILED} - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == RESULT_AUTH_FAILED - async def test_abort_on_connection_error(hass, tuya): """Test when we have a network error.""" @@ -165,13 +132,6 @@ async def test_abort_on_connection_error(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == RESULT_CONN_ERROR - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == RESULT_CONN_ERROR - async def test_options_flow(hass): """Test config flow options.""" From 3f463f22a1a68aaa33f5c57fe5d84aa3dc5debb5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 20:27:56 +0200 Subject: [PATCH 274/852] Deprecate Luftdaten YAML configuration (#50365) --- .../components/luftdaten/__init__.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 6775b9ec023..f03448fa3a9 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -70,18 +70,21 @@ SENSOR_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_SENSOR_ID): cv.positive_int, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_SENSOR_ID): cv.positive_int, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From 43adba4affbc1ed981aecad4275d31efc4e0bee8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 20:28:24 +0200 Subject: [PATCH 275/852] Deprecate Synology DSM YAML configuration (#50366) --- homeassistant/components/synology_dsm/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index c4d0d822d19..46e502cd155 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -90,7 +90,10 @@ CONFIG_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CONFIG_SCHEMA]))}, + vol.All( + cv.deprecated(DOMAIN), + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CONFIG_SCHEMA]))}, + ), extra=vol.ALLOW_EXTRA, ) From 0eeb147eda41507fe868409ab48f4e9aa39e0339 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 20:29:27 +0200 Subject: [PATCH 276/852] Upgrade watchdog to 1.1.0 (#50351) --- homeassistant/components/folder_watcher/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 01482a2c5fe..828d925ddbd 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==2.0.3"], + "requirements": ["watchdog==2.1.0"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 1e914db9fd7..b00e49315e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2323,7 +2323,7 @@ wakeonlan==2.0.1 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.0.3 +watchdog==2.1.0 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b29d4252316..0a3457a384f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1238,7 +1238,7 @@ vultr==0.1.2 wakeonlan==2.0.1 # homeassistant.components.folder_watcher -watchdog==2.0.3 +watchdog==2.1.0 # homeassistant.components.wiffi wiffi==1.0.1 From ba31d7d1b493aaec4cce27f0cb243ab52e094ce6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 20:30:34 +0200 Subject: [PATCH 277/852] Upgrade sentry-sdk to 1.1.0 (#50349) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 776a19673c2..0b37e6a849a 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.0.0"], + "requirements": ["sentry-sdk==1.1.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b00e49315e6..6a9d9ca91ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2052,7 +2052,7 @@ sense-hat==2.2.0 sense_energy==0.9.0 # homeassistant.components.sentry -sentry-sdk==1.0.0 +sentry-sdk==1.1.0 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a3457a384f..3b34a983f5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1098,7 +1098,7 @@ screenlogicpy==0.4.1 sense_energy==0.9.0 # homeassistant.components.sentry -sentry-sdk==1.0.0 +sentry-sdk==1.1.0 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 1b81849271e56c5da4fe9f7d42e2196ce6bc7142 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 9 May 2021 15:28:35 -0400 Subject: [PATCH 278/852] Add zwave_js support for HeatIt Z-TRM2fx (#50317) * Add zwave_js support for HeatIt Z-TRM2fx * fix docstring * use AwesomeVersion to support firmware version ranges * add guard against empty firmware range * switch guard approach to raise exception sooner * make post init more generic * Set up firmware range schema as AwesomeVersion during initialization * Update homeassistant/components/zwave_js/discovery.py Co-authored-by: Martin Hjelmare * Allow min_ver and max_ver to be None * fix docstring * reduce import scope Co-authored-by: Martin Hjelmare --- .../components/zwave_js/discovery.py | 85 +- tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_climate.py | 54 +- tests/components/zwave_js/test_discovery.py | 17 + .../climate_heatit_z_trm2fx_state.json | 1444 +++++++++++++++++ 5 files changed, 1610 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 217a0493ce2..3a47706dcaf 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -2,9 +2,10 @@ from __future__ import annotations from collections.abc import Generator -from dataclasses import dataclass +from dataclasses import asdict, dataclass, field from typing import Any +from awesomeversion import AwesomeVersion from zwave_js_server.const import THERMOSTAT_CURRENT_TEMP_PROPERTY, CommandClass from zwave_js_server.model.device_class import DeviceClassItem from zwave_js_server.model.node import Node as ZwaveNode @@ -19,6 +20,33 @@ from .discovery_data_template import ( ) +class DataclassMustHaveAtLeastOne: + """A dataclass that must have at least one input parameter that is not None.""" + + def __post_init__(self) -> None: + """Post dataclass initialization.""" + if all(val is None for val in asdict(self).values()): + raise ValueError("At least one input parameter must not be None") + + +@dataclass +class FirmwareVersionRange(DataclassMustHaveAtLeastOne): + """Firmware version range dictionary.""" + + min: str | None = None + max: str | None = None + min_ver: AwesomeVersion | None = field(default=None, init=False) + max_ver: AwesomeVersion | None = field(default=None, init=False) + + def __post_init__(self) -> None: + """Post dataclass initialization.""" + super().__post_init__() + if self.min: + self.min_ver = AwesomeVersion(self.min) + if self.max: + self.max_ver = AwesomeVersion(self.max) + + @dataclass class ZwaveDiscoveryInfo: """Info discovered from (primary) ZWave Value to create entity.""" @@ -42,7 +70,7 @@ class ZwaveDiscoveryInfo: @dataclass -class ZWaveValueDiscoverySchema: +class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne): """Z-Wave Value discovery schema. The Z-Wave Value must match these conditions. @@ -89,6 +117,8 @@ class ZWaveDiscoverySchema: product_id: set[int] | None = None # [optional] the node's product_type must match ANY of these values product_type: set[int] | None = None + # [optional] the node's firmware_version must be within this range + firmware_version_range: FirmwareVersionRange | None = None # [optional] the node's firmware_version must match ANY of these values firmware_version: set[str] | None = None # [optional] the node's basic device class must match ANY of these values @@ -274,6 +304,42 @@ DISCOVERY_SCHEMAS = [ ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0), ), ), + # Heatit Z-TRM2fx + ZWaveDiscoverySchema( + platform="climate", + hint="dynamic_current_temp", + manufacturer_id={0x019B}, + product_id={0x0202}, + product_type={0x0003}, + firmware_version_range=FirmwareVersionRange(min="3.0"), + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.THERMOSTAT_MODE}, + property={"mode"}, + type={"number"}, + ), + data_template=DynamicCurrentTempClimateDataTemplate( + { + # External Sensor + "A2": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=2, + ), + "A2F": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=2, + ), + # Floor sensor + "F": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=3, + ), + }, + ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0), + ), + ), # ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS ======= # Door lock mode config parameter. Functionality equivalent to Notification CC # list sensors. @@ -541,6 +607,21 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None ): continue + # check firmware_version_range + if schema.firmware_version_range is not None and ( + ( + schema.firmware_version_range.min is not None + and schema.firmware_version_range.min_ver + > AwesomeVersion(value.node.firmware_version) + ) + or ( + schema.firmware_version_range.max is not None + and schema.firmware_version_range.max_ver + < AwesomeVersion(value.node.firmware_version) + ) + ): + continue + # check firmware_version if ( schema.firmware_version is not None diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index e9078bcf467..14937b0ff2a 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -240,6 +240,12 @@ def climate_heatit_z_trm3_state_fixture(): return json.loads(load_fixture("zwave_js/climate_heatit_z_trm3_state.json")) +@pytest.fixture(name="climate_heatit_z_trm2fx_state", scope="session") +def climate_heatit_z_trm2fx_state_fixture(): + """Load the climate HEATIT Z-TRM2fx thermostat node state fixture data.""" + return json.loads(load_fixture("zwave_js/climate_heatit_z_trm2fx_state.json")) + + @pytest.fixture(name="nortek_thermostat_state", scope="session") def nortek_thermostat_state_fixture(): """Load the nortek thermostat node state fixture data.""" @@ -484,6 +490,14 @@ def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state): return node +@pytest.fixture(name="climate_heatit_z_trm2fx") +def climate_heatit_z_trm2fx_fixture(client, climate_heatit_z_trm2fx_state): + """Mock a climate radio HEATIT Z-TRM2fx node.""" + node = Node(client, copy.deepcopy(climate_heatit_z_trm2fx_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="nortek_thermostat") def nortek_thermostat_fixture(client, nortek_thermostat_state): """Mock a nortek thermostat node.""" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index cb84570158c..a1b86b14ebc 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -28,6 +28,7 @@ from homeassistant.components.climate.const import ( SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) @@ -436,8 +437,10 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat client.async_send_command_no_wait.reset_mock() -async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integration): - """Test a thermostat v2 command class entity.""" +async def test_thermostat_heatit_z_trm3( + hass, client, climate_heatit_z_trm3, integration +): + """Test a heatit Z-TRM3 entity.""" node = climate_heatit_z_trm3 state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) @@ -501,6 +504,53 @@ async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integratio assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 25.5 +async def test_thermostat_heatit_z_trm2fx( + hass, client, climate_heatit_z_trm2fx, integration +): + """Test a heatit Z-TRM2fx entity.""" + node = climate_heatit_z_trm2fx + state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) + + assert state + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_HVAC_MODES] == [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + ] + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 28.8 + assert state.attributes[ATTR_TEMPERATURE] == 29 + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + ) + assert state.attributes[ATTR_MIN_TEMP] == 7 + assert state.attributes[ATTR_MAX_TEMP] == 35 + + # Try switching to external sensor + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 24, + "args": { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Sensor mode", + "newValue": 4, + "prevValue": 2, + }, + }, + ) + node.receive_event(event) + state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) + assert state + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 0 + + async def test_thermostat_srt321_hrt4_zw(hass, client, srt321_hrt4_zw, integration): """Test a climate entity from a HRT4-ZW / SRT321 thermostat device. diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index cbc9d756292..8914019cd43 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -1,4 +1,11 @@ """Test discovery of entities for device-specific schemas for the Z-Wave JS integration.""" +import pytest + +from homeassistant.components.zwave_js.discovery import ( + FirmwareVersionRange, + ZWaveDiscoverySchema, + ZWaveValueDiscoverySchema, +) async def test_iblinds_v2(hass, client, iblinds_v2, integration): @@ -48,3 +55,13 @@ async def test_vision_security_zl7432( state = hass.states.get(entity_id) assert state assert state.attributes["assumed_state"] + + +async def test_firmware_version_range_exception(hass): + """Test FirmwareVersionRange exception.""" + with pytest.raises(ValueError): + ZWaveDiscoverySchema( + "test", + ZWaveValueDiscoverySchema(command_class=1), + firmware_version_range=FirmwareVersionRange(), + ) diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json b/tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json new file mode 100644 index 00000000000..2526e346a53 --- /dev/null +++ b/tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json @@ -0,0 +1,1444 @@ +{ + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 411, + "productId": 514, + "productType": 3, + "firmwareVersion": "3.6", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/usr/src/node_modules/@zwave-js/config/config/devices/0x019b/z-trm2fx_3.0.json", + "manufacturer": "ThermoFloor", + "manufacturerId": 411, + "label": "Z-TRM2fx", + "description": "Floor thermostat", + "devices": [ + { + "productType": 3, + "productId": 514 + } + ], + "firmwareVersion": { + "min": "3.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "compat": { + "valueIdRegex": {}, + "skipConfigurationInfoQuery": true + }, + "isEmbedded": true + }, + "label": "Z-TRM2fx", + "neighbors": [6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 19, 20, 24, 25], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 4, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 1, + "installerIcon": 4608, + "userIcon": 4609, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 2, + "installerIcon": 3328, + "userIcon": 3329, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 33, + "label": "Multilevel Sensor" + }, + "specific": { + "key": 1, + "label": "Routing Multilevel Sensor" + }, + "mandatorySupportedCCs": [32, 49], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 3, + "installerIcon": 3328, + "userIcon": 3329, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 33, + "label": "Multilevel Sensor" + }, + "specific": { + "key": 1, + "label": "Routing Multilevel Sensor" + }, + "mandatorySupportedCCs": [32, 49], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 4, + "installerIcon": 1792, + "userIcon": 1793, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 411 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 514 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "5.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["3.6"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.71.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "3.1.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "5.3.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 43 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 96, + "commandClassName": "Multi Channel", + "property": "endpointIndizes", + "propertyName": "endpointIndizes", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": [1, 2, 3, 4] + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Operation mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Operation mode", + "default": 0, + "min": 0, + "max": 11, + "states": { + "0": "Off. (default)", + "1": "Heating mode", + "2": "Cooling mode (not implemented)", + "11": "Energy saving heating mode" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Sensor mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Sensor mode", + "default": 0, + "min": 0, + "max": 4, + "states": { + "0": "F-mode, floor sensor mode", + "3": "A2-mode, external room sensor mode", + "4": "A2F-mode, external sensor with floor limitation" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Floor sensor type", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor sensor type", + "default": 0, + "min": 0, + "max": 5, + "states": { + "0": "10K-NTC (default)", + "1": "12K-NTC", + "2": "15K-NTC", + "3": "22K-NTC", + "4": "33K-NTC", + "5": "47K-NTC" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Temperature control hysteresis (DIFF I)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature control hysteresis (DIFF I)", + "default": 5, + "min": 3, + "max": 30, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Floor minimum temperature limit (FLo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor minimum temperature limit (FLo)", + "default": 50, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Floor maximum temperature (FHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor maximum temperature (FHi)", + "default": 400, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 400 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Air (A2) minimum temperature limit (ALo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Air (A2) minimum temperature limit (ALo)", + "default": 50, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Air (A2) maximum temperature limit (AHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Air (A2) maximum temperature limit (AHi)", + "default": 400, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 400 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Heating mode setpoint (CO)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Heating mode setpoint (CO)", + "default": 210, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 290 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Energy saving mode setpoint (ECO)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Energy saving mode setpoint (ECO)", + "default": 180, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 250 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Cooling setpoint (COOL)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Cooling setpoint (COOL)", + "default": 210, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 200 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Floor sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor sensor calibration", + "default": 0, + "min": -40, + "max": 40, + "unit": "oC", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "External sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "External sensor calibration", + "default": 0, + "min": -40, + "max": 40, + "unit": "oC", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Temperature display", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature display", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Display setpoint temperature (default)", + "1": "Display measured temperature" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Button brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button brightness - dimmed state", + "default": 50, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "Button brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button brightness - active state", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Display brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Display brightness - dimmed state", + "default": 50, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyName": "Display brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Display brightness - active state", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyName": "Temperature report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature report interval", + "default": 60, + "min": 0, + "max": 32767, + "unit": "seconds", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "Temperature report hysteresis", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature report hysteresis", + "default": 10, + "min": 1, + "max": 100, + "unit": "oC", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyName": "Meter report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Meter report interval", + "default": 60, + "min": 0, + "max": 32767, + "unit": "seconds", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyName": "Meter report delta value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Meter report delta value", + "default": 10, + "min": 0, + "max": 127, + "unit": "kWh/10", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat mode", + "min": 0, + "max": 255, + "states": { + "0": "Off", + "1": "Heat", + "2": "Cool", + "11": "Energy heat" + } + }, + "value": 1 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 1 + }, + "unit": "\u00b0C" + }, + "value": 29 + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 2 + }, + "unit": "\u00b0C" + }, + "value": 20 + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 11, + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Heating", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 11 + }, + "unit": "\u00b0C" + }, + "value": 25 + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + } + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + } + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + } + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 2, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 0 + }, + { + "endpoint": 3, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 3, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 28.8 + }, + { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + } + }, + { + "endpoint": 4, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + }, + "unit": "kWh" + }, + "value": 795.7 + }, + { + "endpoint": 4, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + }, + "unit": "W" + }, + "value": 493.57 + }, + { + "endpoint": 4, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [V]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + }, + "unit": "V" + }, + "value": 237.1 + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + } + ], + "interviewStage": "Complete" +} From ee58f6105e6842e0324de94a289c243ead52988a Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 9 May 2021 15:45:24 -0400 Subject: [PATCH 279/852] Add selectors to webostv services (#50239) --- .../components/webostv/services.yaml | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml index 430916f7c71..86e1c52aef6 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -1,42 +1,77 @@ # Describes the format for available webostv services button: + name: Button description: "Send a button press command." fields: entity_id: + name: Entity description: Name(s) of the webostv entities where to run the API method. - example: "media_player.living_room_tv" + required: true + selector: + entity: + integration: webostv + domain: media_player button: + name: Button description: >- Name of the button to press. Known possible values are LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + required: true example: "LEFT" + selector: + text: command: + name: Command description: "Send a command." fields: entity_id: + name: Entity description: Name(s) of the webostv entities where to run the API method. - example: "media_player.living_room_tv" + required: true + selector: + entity: + integration: webostv + domain: media_player command: + name: Command description: >- Endpoint of the command. Known valid endpoints are listed in https://github.com/TheRealLink/pylgtv/blob/master/pylgtv/endpoints.py + required: true example: "system.launcher/open" + selector: + text: payload: + name: Payload description: >- An optional payload to provide to the endpoint in the format of key value pair(s). example: >- target: https://www.google.com + advanced: true + selector: + object: select_sound_output: + name: Select Sound Output description: "Send the TV the command to change sound output." fields: entity_id: + name: Entity description: Name(s) of the webostv entities to change sound output on. + required: true example: "media_player.living_room_tv" + selector: + entity: + integration: webostv + domain: media_player sound_output: + name: Sound Output description: Name of the sound output to switch to. + required: true example: "external_speaker" + selector: + text: From e57634b1e02ddb981ad4c92f78ae336b960f7f0e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 9 May 2021 21:57:27 +0200 Subject: [PATCH 280/852] Bump hatasmota to 0.2.12 (#50372) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_sensor.py | 120 ++++++++++++++++++ 4 files changed, 123 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index b3a4bb09fea..a6e7a1d45a8 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.11"], + "requirements": ["hatasmota==0.2.12"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 6a9d9ca91ed..2fcec0aa9c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -735,7 +735,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.11 +hatasmota==0.2.12 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b34a983f5e..b323cb3fed0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ hangups==0.4.11 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.11 +hatasmota==0.2.12 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 0d6820f2d34..425fa3f26f6 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -43,6 +43,15 @@ DEFAULT_SENSOR_CONFIG = { } } +BAD_INDEXED_SENSOR_CONFIG_3 = { + "sn": { + "Time": "2020-09-25T12:47:15", + "ENERGY": { + "ApparentPower": [7.84, 1.23, 2.34], + }, + } +} + INDEXED_SENSOR_CONFIG = { "sn": { "Time": "2020-09-25T12:47:15", @@ -224,6 +233,117 @@ async def test_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert state.state == "7.8" +async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT where sensor is not matching configuration.""" + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(BAD_INDEXED_SENSOR_CONFIG_3) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.tasmota_energy_apparentpower_0") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + state = hass.states.get("sensor.tasmota_energy_apparentpower_1") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + state = hass.states.get("sensor.tasmota_energy_apparentpower_2") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("sensor.tasmota_energy_apparentpower_0") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + state = hass.states.get("sensor.tasmota_energy_apparentpower_1") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + state = hass.states.get("sensor.tasmota_energy_apparentpower_2") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Test periodic state update + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/SENSOR", '{"ENERGY":{"ApparentPower":[1.2,3.4,5.6]}}' + ) + state = hass.states.get("sensor.tasmota_energy_apparentpower_0") + assert state.state == "1.2" + state = hass.states.get("sensor.tasmota_energy_apparentpower_1") + assert state.state == "3.4" + state = hass.states.get("sensor.tasmota_energy_apparentpower_2") + assert state.state == "5.6" + + # Test periodic state update with too few values + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/SENSOR", '{"ENERGY":{"ApparentPower":[7.8,9.0]}}' + ) + state = hass.states.get("sensor.tasmota_energy_apparentpower_0") + assert state.state == "7.8" + state = hass.states.get("sensor.tasmota_energy_apparentpower_1") + assert state.state == "9.0" + state = hass.states.get("sensor.tasmota_energy_apparentpower_2") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/SENSOR", '{"ENERGY":{"ApparentPower":2.3}}' + ) + state = hass.states.get("sensor.tasmota_energy_apparentpower_0") + assert state.state == "2.3" + state = hass.states.get("sensor.tasmota_energy_apparentpower_1") + assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.tasmota_energy_apparentpower_2") + assert state.state == STATE_UNKNOWN + + # Test polled state update + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"ENERGY":{"ApparentPower":[1.2,3.4,5.6]}}}', + ) + state = hass.states.get("sensor.tasmota_energy_apparentpower_0") + assert state.state == "1.2" + state = hass.states.get("sensor.tasmota_energy_apparentpower_1") + assert state.state == "3.4" + state = hass.states.get("sensor.tasmota_energy_apparentpower_2") + assert state.state == "5.6" + + # Test polled state update with too few values + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"ENERGY":{"ApparentPower":[7.8,9.0]}}}', + ) + state = hass.states.get("sensor.tasmota_energy_apparentpower_0") + assert state.state == "7.8" + state = hass.states.get("sensor.tasmota_energy_apparentpower_1") + assert state.state == "9.0" + state = hass.states.get("sensor.tasmota_energy_apparentpower_2") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"ENERGY":{"ApparentPower":2.3}}}', + ) + state = hass.states.get("sensor.tasmota_energy_apparentpower_0") + assert state.state == "2.3" + state = hass.states.get("sensor.tasmota_energy_apparentpower_1") + assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.tasmota_energy_apparentpower_2") + assert state.state == STATE_UNKNOWN + + @pytest.mark.parametrize("status_sensor_disabled", [False]) async def test_status_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" From 37450567512c80db03bcfc404e15fc70d3449f18 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 22:13:09 +0200 Subject: [PATCH 281/852] Upgrade attrs to 21.2.0 (#50374) --- homeassistant/package_constraints.txt | 2 +- requirements.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 71f3a75eac9..c79992e0948 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.16.2 async_timeout==3.0.1 -attrs==20.3.0 +attrs==21.2.0 awesomeversion==21.2.3 bcrypt==3.1.7 certifi>=2020.12.5 diff --git a/requirements.txt b/requirements.txt index c134926c6fa..24e387bed58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ aiohttp==3.7.4.post0 astral==2.2 async_timeout==3.0.1 -attrs==20.3.0 +attrs==21.2.0 awesomeversion==21.2.3 bcrypt==3.1.7 certifi>=2020.12.5 diff --git a/setup.py b/setup.py index 71b3d66046c..d5b9d6e68b3 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ REQUIRES = [ "aiohttp==3.7.4.post0", "astral==2.2", "async_timeout==3.0.1", - "attrs==20.3.0", + "attrs==21.2.0", "awesomeversion==21.2.3", "bcrypt==3.1.7", "certifi>=2020.12.5", From 717f4e69d5842c3f61bb3a2e97154f472a4d8b5d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 22:23:52 +0200 Subject: [PATCH 282/852] Remove defunct Spot Crime integration (#50361) --- .coveragerc | 1 - .../components/spotcrime/__init__.py | 1 - .../components/spotcrime/manifest.json | 8 - homeassistant/components/spotcrime/sensor.py | 140 ------------------ requirements_all.txt | 3 - 5 files changed, 153 deletions(-) delete mode 100644 homeassistant/components/spotcrime/__init__.py delete mode 100644 homeassistant/components/spotcrime/manifest.json delete mode 100644 homeassistant/components/spotcrime/sensor.py diff --git a/.coveragerc b/.coveragerc index 95d699be69f..923642733d4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -942,7 +942,6 @@ omit = homeassistant/components/speedtestdotnet/* homeassistant/components/spider/* homeassistant/components/splunk/* - homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/__init__.py homeassistant/components/spotify/media_player.py homeassistant/components/spotify/system_health.py diff --git a/homeassistant/components/spotcrime/__init__.py b/homeassistant/components/spotcrime/__init__.py deleted file mode 100644 index 26bb50b8b02..00000000000 --- a/homeassistant/components/spotcrime/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The spotcrime component.""" diff --git a/homeassistant/components/spotcrime/manifest.json b/homeassistant/components/spotcrime/manifest.json deleted file mode 100644 index a668454469d..00000000000 --- a/homeassistant/components/spotcrime/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "spotcrime", - "name": "Spot Crime", - "documentation": "https://www.home-assistant.io/integrations/spotcrime", - "requirements": ["spotcrime==1.0.4"], - "codeowners": [], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/spotcrime/sensor.py b/homeassistant/components/spotcrime/sensor.py deleted file mode 100644 index 72a6fec84e9..00000000000 --- a/homeassistant/components/spotcrime/sensor.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Sensor for Spot Crime.""" - -from collections import defaultdict -from datetime import timedelta - -import spotcrime -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_LATITUDE, - ATTR_LONGITUDE, - CONF_API_KEY, - CONF_EXCLUDE, - CONF_INCLUDE, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - CONF_RADIUS, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify - -CONF_DAYS = "days" -DEFAULT_DAYS = 1 -NAME = "spotcrime" - -EVENT_INCIDENT = f"{NAME}_incident" - -SCAN_INTERVAL = timedelta(minutes=30) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_RADIUS): vol.Coerce(float), - vol.Required(CONF_API_KEY): cv.string, - vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, - vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.positive_int, - vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]), - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Crime Reports platform.""" - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - name = config[CONF_NAME] - radius = config[CONF_RADIUS] - api_key = config[CONF_API_KEY] - days = config.get(CONF_DAYS) - include = config.get(CONF_INCLUDE) - exclude = config.get(CONF_EXCLUDE) - - add_entities( - [ - SpotCrimeSensor( - name, latitude, longitude, radius, include, exclude, api_key, days - ) - ], - True, - ) - - -class SpotCrimeSensor(SensorEntity): - """Representation of a Spot Crime Sensor.""" - - def __init__( - self, name, latitude, longitude, radius, include, exclude, api_key, days - ): - """Initialize the Spot Crime sensor.""" - - self._name = name - self._include = include - self._exclude = exclude - self.api_key = api_key - self.days = days - self._spotcrime = spotcrime.SpotCrime( - (latitude, longitude), - radius, - self._include, - self._exclude, - self.api_key, - self.days, - ) - self._attributes = None - self._state = None - self._previous_incidents = set() - self._attributes = {ATTR_ATTRIBUTION: spotcrime.ATTRIBUTION} - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - def _incident_event(self, incident): - data = { - "type": incident.get("type"), - "timestamp": incident.get("timestamp"), - "address": incident.get("location"), - } - if incident.get("coordinates"): - data.update( - { - ATTR_LATITUDE: incident.get("lat"), - ATTR_LONGITUDE: incident.get("lon"), - } - ) - self.hass.bus.fire(EVENT_INCIDENT, data) - - def update(self): - """Update device state.""" - incident_counts = defaultdict(int) - incidents = self._spotcrime.get_incidents() - if len(incidents) < len(self._previous_incidents): - self._previous_incidents = set() - for incident in incidents: - incident_type = slugify(incident.get("type")) - incident_counts[incident_type] += 1 - if ( - self._previous_incidents - and incident.get("id") not in self._previous_incidents - ): - self._incident_event(incident) - self._previous_incidents.add(incident.get("id")) - self._attributes.update(incident_counts) - self._state = len(incidents) diff --git a/requirements_all.txt b/requirements_all.txt index 2fcec0aa9c5..4363cc8956c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2134,9 +2134,6 @@ speedtest-cli==2.1.3 # homeassistant.components.spider spiderpy==1.4.2 -# homeassistant.components.spotcrime -spotcrime==1.0.4 - # homeassistant.components.spotify spotipy==2.18.0 From 042822e35e6033d29887378076671eed9d15029f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 9 May 2021 22:44:55 +0200 Subject: [PATCH 283/852] Improve typing for synology_dsm (#49656) --- .strict-typing | 1 + .../components/synology_dsm/__init__.py | 112 ++++++++++++------ .../components/synology_dsm/binary_sensor.py | 21 ++-- .../components/synology_dsm/camera.py | 54 +++++---- .../components/synology_dsm/config_flow.py | 70 +++++++---- .../components/synology_dsm/const.py | 40 +++++-- .../components/synology_dsm/sensor.py | 27 +++-- .../components/synology_dsm/switch.py | 31 +++-- mypy.ini | 16 ++- script/hassfest/mypy_config.py | 1 - 10 files changed, 241 insertions(+), 132 deletions(-) diff --git a/.strict-typing b/.strict-typing index 175142e217e..a8d9466a01b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -40,6 +40,7 @@ homeassistant.components.slack.* homeassistant.components.sonos.media_player homeassistant.components.sun.* homeassistant.components.switch.* +homeassistant.components.synology_dsm.* homeassistant.components.systemmonitor.* homeassistant.components.tts.* homeassistant.components.vacuum.* diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 46e502cd155..7a316b0381d 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any +from typing import Any, Callable import async_timeout from synology_dsm import SynologyDSM @@ -15,6 +15,7 @@ from synology_dsm.api.dsm.information import SynoDSMInformation from synology_dsm.api.dsm.network import SynoDSMNetwork from synology_dsm.api.storage.storage import SynoStorage from synology_dsm.api.surveillance_station import SynoSurveillanceStation +from synology_dsm.api.surveillance_station.camera import SynoCamera from synology_dsm.exceptions import ( SynologyDSMAPIErrorException, SynologyDSMLoginFailedException, @@ -38,9 +39,14 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import entity_registry +from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import ( + DeviceEntry, + async_get_registry as get_dev_reg, +) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -74,6 +80,7 @@ from .const import ( SYSTEM_LOADED, UNDO_UPDATE_LISTENER, UTILISATION_SENSORS, + EntityInfo, ) CONFIG_SCHEMA = vol.Schema( @@ -103,7 +110,7 @@ ATTRIBUTION = "Data provided by Synology" _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Synology DSM sensors from legacy config file.""" conf = config.get(DOMAIN) @@ -122,12 +129,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Set up Synology DSM sensors.""" # Migrate old unique_id @callback - def _async_migrator(entity_entry: entity_registry.RegistryEntry): + def _async_migrator( + entity_entry: entity_registry.RegistryEntry, + ) -> dict[str, str] | None: """Migrate away from ID using label.""" # Reject if new unique_id if "SYNO." in entity_entry.unique_id: @@ -152,7 +163,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ): return None - entity_type = None + entity_type: str | None = None for entity_key, entity_attrs in entries.items(): if ( device_id @@ -170,6 +181,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if entity_attrs[ENTITY_NAME] == label: entity_type = entity_key + if entity_type is None: + return None + new_unique_id = "_".join([serial, entity_type]) if device_id: new_unique_id += f"_{device_id}" @@ -183,6 +197,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await entity_registry.async_migrate_entries(hass, entry.entry_id, _async_migrator) + # migrate device indetifiers + dev_reg = await get_dev_reg(hass) + devices: list[DeviceEntry] = device_registry.async_entries_for_config_entry( + dev_reg, entry.entry_id + ) + for device in devices: + old_identifier = list(next(iter(device.identifiers))) + if len(old_identifier) > 2: + new_identifier: set[tuple[str, ...]] = { + (old_identifier.pop(0), "_".join([str(x) for x in old_identifier])) + } + _LOGGER.debug( + "migrate identifier '%s' to '%s'", device.identifiers, new_identifier + ) + dev_reg.async_update_device(device.id, new_identifiers=new_identifier) + # Migrate existing entry configuration if entry.data.get(CONF_VERIFY_SSL) is None: hass.config_entries.async_update_entry( @@ -216,7 +246,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry, data={**entry.data, CONF_MAC: network.macs} ) - async def async_coordinator_update_data_cameras(): + async def async_coordinator_update_data_cameras() -> dict[ + str, dict[str, SynoCamera] + ] | None: """Fetch all camera data from api.""" if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: raise UpdateFailed("System not fully loaded") @@ -238,7 +270,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): } } - async def async_coordinator_update_data_central(): + async def async_coordinator_update_data_central() -> None: """Fetch all device and sensor data from api.""" try: await api.async_update() @@ -246,7 +278,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise UpdateFailed(f"Error communicating with API: {err}") from err return None - async def async_coordinator_update_data_switches(): + async def async_coordinator_update_data_switches() -> dict[ + str, dict[str, Any] + ] | None: """Fetch all switch data from api.""" if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: raise UpdateFailed("System not fully loaded") @@ -294,7 +328,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Synology DSM sensors.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: @@ -306,15 +340,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) -async def _async_setup_services(hass: HomeAssistant): +async def _async_setup_services(hass: HomeAssistant) -> None: """Service handler setup.""" - async def service_handler(call: ServiceCall): + async def service_handler(call: ServiceCall) -> None: """Handle service call.""" serial = call.data.get(CONF_SERIAL) dsm_devices = hass.data[DOMAIN] @@ -350,7 +384,7 @@ async def _async_setup_services(hass: HomeAssistant): class SynoApi: """Class to interface with Synology DSM API.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the API wrapper class.""" self._hass = hass self._entry = entry @@ -367,7 +401,7 @@ class SynoApi: self.utilisation: SynoCoreUtilization = None # Should we fetch them - self._fetching_entities = {} + self._fetching_entities: dict[str, set[str]] = {} self._with_information = True self._with_security = True self._with_storage = True @@ -376,7 +410,7 @@ class SynoApi: self._with_upgrade = True self._with_utilisation = True - async def async_setup(self): + async def async_setup(self) -> None: """Start interacting with the NAS.""" self.dsm = SynologyDSM( self._entry.data[CONF_HOST], @@ -406,7 +440,7 @@ class SynoApi: await self.async_update() @callback - def subscribe(self, api_key, unique_id): + def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: """Subscribe an entity to API fetches.""" _LOGGER.debug("Subscribe new entity: %s", unique_id) if api_key not in self._fetching_entities: @@ -424,7 +458,7 @@ class SynoApi: return unsubscribe @callback - def _async_setup_api_requests(self): + def _async_setup_api_requests(self) -> None: """Determine if we should fetch each API, if one entity needs it.""" # Entities not added yet, fetch all if not self._fetching_entities: @@ -488,7 +522,7 @@ class SynoApi: self.dsm.reset(self.utilisation) self.utilisation = None - def _fetch_device_configuration(self): + def _fetch_device_configuration(self) -> None: """Fetch initial device config.""" self.information = self.dsm.information self.network = self.dsm.network @@ -523,7 +557,7 @@ class SynoApi: ) self.surveillance_station = self.dsm.surveillance_station - async def async_reboot(self): + async def async_reboot(self) -> None: """Reboot NAS.""" try: await self._hass.async_add_executor_job(self.system.reboot) @@ -534,7 +568,7 @@ class SynoApi: ) _LOGGER.debug("Exception:%s", err) - async def async_shutdown(self): + async def async_shutdown(self) -> None: """Shutdown NAS.""" try: await self._hass.async_add_executor_job(self.system.shutdown) @@ -545,7 +579,7 @@ class SynoApi: ) _LOGGER.debug("Exception:%s", err) - async def async_unload(self): + async def async_unload(self) -> None: """Stop interacting with the NAS and prepare for removal from hass.""" try: await self._hass.async_add_executor_job(self.dsm.logout) @@ -554,7 +588,7 @@ class SynoApi: "Logout from '%s' not possible:%s", self._entry.unique_id, err ) - async def async_update(self, now=None): + async def async_update(self, now: timedelta | None = None) -> None: """Update function for updating API information.""" _LOGGER.debug("Start data update for '%s'", self._entry.unique_id) self._async_setup_api_requests() @@ -582,9 +616,9 @@ class SynologyDSMBaseEntity(CoordinatorEntity): self, api: SynoApi, entity_type: str, - entity_info: dict[str, str], - coordinator: DataUpdateCoordinator, - ): + entity_info: EntityInfo, + coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]], + ) -> None: """Initialize the Synology DSM entity.""" super().__init__(coordinator) @@ -609,12 +643,12 @@ class SynologyDSMBaseEntity(CoordinatorEntity): return self._name @property - def icon(self) -> str: + def icon(self) -> str | None: """Return the icon.""" return self._icon @property - def device_class(self) -> str: + def device_class(self) -> str | None: """Return the class of this device.""" return self._class @@ -639,7 +673,7 @@ class SynologyDSMBaseEntity(CoordinatorEntity): """Return if the entity should be enabled when first added to the entity registry.""" return self._enable_default - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register entity for updates from API.""" self.async_on_remove(self._api.subscribe(self._api_key, self.unique_id)) await super().async_added_to_hass() @@ -652,10 +686,10 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): self, api: SynoApi, entity_type: str, - entity_info: dict[str, str], - coordinator: DataUpdateCoordinator, - device_id: str = None, - ): + entity_info: EntityInfo, + coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]], + device_id: str | None = None, + ) -> None: """Initialize the Synology DSM disk or volume entity.""" super().__init__(api, entity_type, entity_info, coordinator) self._device_id = device_id @@ -691,16 +725,18 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return bool(self._api.storage) + return self._api.storage # type: ignore [no-any-return] @property def device_info(self) -> DeviceInfo: """Return the device information.""" return { - "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, + "identifiers": { + (DOMAIN, f"{self._api.information.serial}_{self._device_id}") + }, "name": f"Synology NAS ({self._device_name} - {self._device_type})", - "manufacturer": self._device_manufacturer, - "model": self._device_model, - "sw_version": self._device_firmware, + "manufacturer": self._device_manufacturer, # type: ignore[typeddict-item] + "model": self._device_model, # type: ignore[typeddict-item] + "sw_version": self._device_firmware, # type: ignore[typeddict-item] "via_device": (DOMAIN, self._api.information.serial), } diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 587f89cf16a..e94dc1a94ac 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -5,8 +5,9 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISKS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SynologyDSMBaseEntity, SynologyDSMDeviceEntity +from . import SynoApi, SynologyDSMBaseEntity, SynologyDSMDeviceEntity from .const import ( COORDINATOR_CENTRAL, DOMAIN, @@ -18,15 +19,19 @@ from .const import ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS binary sensor.""" data = hass.data[DOMAIN][entry.unique_id] - api = data[SYNO_API] + api: SynoApi = data[SYNO_API] coordinator = data[COORDINATOR_CENTRAL] - entities = [ + entities: list[ + SynoDSMSecurityBinarySensor + | SynoDSMUpgradeBinarySensor + | SynoDSMStorageBinarySensor + ] = [ SynoDSMSecurityBinarySensor( api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type], coordinator ) @@ -63,7 +68,7 @@ class SynoDSMSecurityBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return the state.""" - return getattr(self._api.security, self.entity_type) != "safe" + return getattr(self._api.security, self.entity_type) != "safe" # type: ignore[no-any-return] @property def available(self) -> bool: @@ -73,7 +78,7 @@ class SynoDSMSecurityBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity): @property def extra_state_attributes(self) -> dict[str, str]: """Return security checks details.""" - return self._api.security.status_by_check + return self._api.security.status_by_check # type: ignore[no-any-return] class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity): @@ -82,7 +87,7 @@ class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return the state.""" - return getattr(self._api.storage, self.entity_type)(self._device_id) + return bool(getattr(self._api.storage, self.entity_type)(self._device_id)) class SynoDSMUpgradeBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity): @@ -91,7 +96,7 @@ class SynoDSMUpgradeBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return the state.""" - return getattr(self._api.upgrade, self.entity_type) + return bool(getattr(self._api.upgrade, self.entity_type)) @property def available(self) -> bool: diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 6183125ee8f..9baca38c16f 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from synology_dsm.api.surveillance_station import SynoSurveillanceStation +from synology_dsm.api.surveillance_station import SynoCamera, SynoSurveillanceStation from synology_dsm.exceptions import ( SynologyDSMAPIErrorException, SynologyDSMRequestException, @@ -13,6 +13,7 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi, SynologyDSMBaseEntity @@ -31,19 +32,21 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS cameras.""" data = hass.data[DOMAIN][entry.unique_id] - api = data[SYNO_API] + api: SynoApi = data[SYNO_API] if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: return # initial data fetch - coordinator = data[COORDINATOR_CAMERAS] - await coordinator.async_refresh() + coordinator: DataUpdateCoordinator[dict[str, dict[str, SynoCamera]]] = data[ + COORDINATOR_CAMERAS + ] + await coordinator.async_config_entry_first_refresh() async_add_entities( SynoDSMCamera(api, coordinator, camera_id) @@ -54,9 +57,14 @@ async def async_setup_entry( class SynoDSMCamera(SynologyDSMBaseEntity, Camera): """Representation a Synology camera.""" + coordinator: DataUpdateCoordinator[dict[str, dict[str, SynoCamera]]] + def __init__( - self, api: SynoApi, coordinator: DataUpdateCoordinator, camera_id: int - ): + self, + api: SynoApi, + coordinator: DataUpdateCoordinator[dict[str, dict[str, SynoCamera]]], + camera_id: str, + ) -> None: """Initialize a Synology camera.""" super().__init__( api, @@ -70,13 +78,11 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): }, coordinator, ) - Camera.__init__(self) - + Camera.__init__(self) # type: ignore[no-untyped-call] self._camera_id = camera_id - self._api = api @property - def camera_data(self): + def camera_data(self) -> SynoCamera: """Camera data.""" return self.coordinator.data["cameras"][self._camera_id] @@ -87,16 +93,14 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): "identifiers": { ( DOMAIN, - self._api.information.serial, - self.camera_data.id, + f"{self._api.information.serial}_{self.camera_data.id}", ) }, "name": self.camera_data.name, "model": self.camera_data.model, "via_device": ( DOMAIN, - self._api.information.serial, - SynoSurveillanceStation.INFO_API_KEY, + f"{self._api.information.serial}_{SynoSurveillanceStation.INFO_API_KEY}", ), } @@ -111,16 +115,16 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): return SUPPORT_STREAM @property - def is_recording(self): + def is_recording(self) -> bool: """Return true if the device is recording.""" - return self.camera_data.is_recording + return self.camera_data.is_recording # type: ignore[no-any-return] @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" - return self.camera_data.is_motion_detection_enabled + return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] - def camera_image(self) -> bytes: + def camera_image(self) -> bytes | None: """Return bytes of camera image.""" _LOGGER.debug( "SynoDSMCamera.camera_image(%s)", @@ -129,7 +133,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): if not self.available: return None try: - return self._api.surveillance_station.get_camera_image(self._camera_id) + return self._api.surveillance_station.get_camera_image(self._camera_id) # type: ignore[no-any-return] except ( SynologyDSMAPIErrorException, SynologyDSMRequestException, @@ -142,7 +146,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): ) return None - async def stream_source(self) -> str: + async def stream_source(self) -> str | None: """Return the source of the stream.""" _LOGGER.debug( "SynoDSMCamera.stream_source(%s)", @@ -150,9 +154,9 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): ) if not self.available: return None - return self.camera_data.live_view.rtsp + return self.camera_data.live_view.rtsp # type: ignore[no-any-return] - def enable_motion_detection(self): + def enable_motion_detection(self) -> None: """Enable motion detection in the camera.""" _LOGGER.debug( "SynoDSMCamera.enable_motion_detection(%s)", @@ -160,7 +164,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): ) self._api.surveillance_station.enable_motion_detection(self._camera_id) - def disable_motion_detection(self): + def disable_motion_detection(self) -> None: """Disable motion detection in camera.""" _LOGGER.debug( "SynoDSMCamera.disable_motion_detection(%s)", diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 3cbc58d4cf4..85d1cb4bf7a 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -1,5 +1,8 @@ """Config flow to configure the Synology DSM integration.""" +from __future__ import annotations + import logging +from typing import Any from urllib.parse import urlparse from synology_dsm import SynologyDSM @@ -12,8 +15,14 @@ from synology_dsm.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import exceptions from homeassistant.components import ssdp +from homeassistant.config_entries import ( + CONN_CLASS_CLOUD_POLL, + ConfigEntry, + ConfigFlow, + OptionsFlow, +) from homeassistant.const import ( CONF_DISKS, CONF_HOST, @@ -28,7 +37,9 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONF_DEVICE_TOKEN, @@ -47,11 +58,11 @@ _LOGGER = logging.getLogger(__name__) CONF_OTP_CODE = "otp_code" -def _discovery_schema_with_defaults(discovery_info): +def _discovery_schema_with_defaults(discovery_info: DiscoveryInfoType) -> vol.Schema: return vol.Schema(_ordered_shared_schema(discovery_info)) -def _user_schema_with_defaults(user_input): +def _user_schema_with_defaults(user_input: dict[str, Any]) -> vol.Schema: user_schema = { vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, } @@ -60,7 +71,9 @@ def _user_schema_with_defaults(user_input): return vol.Schema(user_schema) -def _ordered_shared_schema(schema_input): +def _ordered_shared_schema( + schema_input: dict[str, Any] +) -> dict[vol.Required | vol.Optional, Any]: return { vol.Required(CONF_USERNAME, default=schema_input.get(CONF_USERNAME, "")): str, vol.Required(CONF_PASSWORD, default=schema_input.get(CONF_PASSWORD, "")): str, @@ -75,23 +88,30 @@ def _ordered_shared_schema(schema_input): } -class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> SynologyDSMOptionsFlowHandler: """Get the options flow for this handler.""" return SynologyDSMOptionsFlowHandler(config_entry) - def __init__(self): + def __init__(self) -> None: """Initialize the synology_dsm config flow.""" - self.saved_user_input = {} - self.discovered_conf = {} + self.saved_user_input: dict[str, Any] = {} + self.discovered_conf: dict[str, Any] = {} - async def _show_setup_form(self, user_input=None, errors=None): + async def _show_setup_form( + self, + user_input: dict[str, Any] | None = None, + errors: dict[str, str] | None = None, + ) -> FlowResult: """Show the setup form to the user.""" if not user_input: user_input = {} @@ -111,7 +131,9 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=self.discovered_conf or {}, ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -188,7 +210,7 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=host, data=config_data) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a discovered synology_dsm.""" parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) friendly_name = ( @@ -211,15 +233,19 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() - async def async_step_import(self, user_input=None): + async def async_step_import( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Import a config entry.""" return await self.async_step_user(user_input) - async def async_step_link(self, user_input): + async def async_step_link(self, user_input: dict[str, Any]) -> FlowResult: """Link a config entry from discovery.""" return await self.async_step_user(user_input) - async def async_step_2sa(self, user_input, errors=None): + async def async_step_2sa( + self, user_input: dict[str, Any], errors: dict[str, str] | None = None + ) -> FlowResult: """Enter 2SA code to anthenticate.""" if not self.saved_user_input: self.saved_user_input = user_input @@ -236,7 +262,7 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(user_input) - def _mac_already_configured(self, mac): + def _mac_already_configured(self, mac: str) -> bool: """See if we already have configured a NAS with this MAC address.""" existing_macs = [ mac.replace("-", "") @@ -246,14 +272,16 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return mac in existing_macs -class SynologyDSMOptionsFlowHandler(config_entries.OptionsFlow): +class SynologyDSMOptionsFlowHandler(OptionsFlow): """Handle a option flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: ConfigEntry): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -277,7 +305,7 @@ class SynologyDSMOptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=data_schema) -def _login_and_fetch_syno_info(api, otp_code): +def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str) -> str: """Login to the NAS and fetch basic data.""" # These do i/o api.login(otp_code) @@ -293,7 +321,7 @@ def _login_and_fetch_syno_info(api, otp_code): ): raise InvalidData - return api.information.serial + return api.information.serial # type: ignore[no-any-return] class InvalidData(exceptions.HomeAssistantError): diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index ba1aa393c85..334832ddf2b 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -1,4 +1,7 @@ """Constants for Synology DSM.""" +from __future__ import annotations + +from typing import Final, TypedDict from synology_dsm.api.core.security import SynoCoreSecurity from synology_dsm.api.core.upgrade import SynoCoreUpgrade @@ -17,6 +20,17 @@ from homeassistant.const import ( PERCENTAGE, ) + +class EntityInfo(TypedDict): + """TypedDict for EntityInfo.""" + + name: str + unit: str | None + icon: str | None + device_class: str | None + enable: bool + + DOMAIN = "synology_dsm" PLATFORMS = ["binary_sensor", "camera", "sensor", "switch"] COORDINATOR_CAMERAS = "coordinator_cameras" @@ -43,11 +57,11 @@ DEFAULT_TIMEOUT = 10 # sec ENTITY_UNIT_LOAD = "load" -ENTITY_NAME = "name" -ENTITY_UNIT = "unit" -ENTITY_ICON = "icon" -ENTITY_CLASS = "device_class" -ENTITY_ENABLE = "enable" +ENTITY_NAME: Final = "name" +ENTITY_UNIT: Final = "unit" +ENTITY_ICON: Final = "icon" +ENTITY_CLASS: Final = "device_class" +ENTITY_ENABLE: Final = "enable" # Services SERVICE_REBOOT = "reboot" @@ -60,7 +74,7 @@ SERVICES = [ # Entity keys should start with the API_KEY to fetch # Binary sensors -UPGRADE_BINARY_SENSORS = { +UPGRADE_BINARY_SENSORS: dict[str, EntityInfo] = { f"{SynoCoreUpgrade.API_KEY}:update_available": { ENTITY_NAME: "Update available", ENTITY_UNIT: None, @@ -70,7 +84,7 @@ UPGRADE_BINARY_SENSORS = { }, } -SECURITY_BINARY_SENSORS = { +SECURITY_BINARY_SENSORS: dict[str, EntityInfo] = { f"{SynoCoreSecurity.API_KEY}:status": { ENTITY_NAME: "Security status", ENTITY_UNIT: None, @@ -80,7 +94,7 @@ SECURITY_BINARY_SENSORS = { }, } -STORAGE_DISK_BINARY_SENSORS = { +STORAGE_DISK_BINARY_SENSORS: dict[str, EntityInfo] = { f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": { ENTITY_NAME: "Exceeded Max Bad Sectors", ENTITY_UNIT: None, @@ -98,7 +112,7 @@ STORAGE_DISK_BINARY_SENSORS = { } # Sensors -UTILISATION_SENSORS = { +UTILISATION_SENSORS: dict[str, EntityInfo] = { f"{SynoCoreUtilization.API_KEY}:cpu_other_load": { ENTITY_NAME: "CPU Utilization (Other)", ENTITY_UNIT: PERCENTAGE, @@ -212,7 +226,7 @@ UTILISATION_SENSORS = { ENTITY_ENABLE: True, }, } -STORAGE_VOL_SENSORS = { +STORAGE_VOL_SENSORS: dict[str, EntityInfo] = { f"{SynoStorage.API_KEY}:volume_status": { ENTITY_NAME: "Status", ENTITY_UNIT: None, @@ -256,7 +270,7 @@ STORAGE_VOL_SENSORS = { ENTITY_ENABLE: False, }, } -STORAGE_DISK_SENSORS = { +STORAGE_DISK_SENSORS: dict[str, EntityInfo] = { f"{SynoStorage.API_KEY}:disk_smart_status": { ENTITY_NAME: "Status (Smart)", ENTITY_UNIT: None, @@ -280,7 +294,7 @@ STORAGE_DISK_SENSORS = { }, } -INFORMATION_SENSORS = { +INFORMATION_SENSORS: dict[str, EntityInfo] = { f"{SynoDSMInformation.API_KEY}:temperature": { ENTITY_NAME: "temperature", ENTITY_UNIT: None, @@ -298,7 +312,7 @@ INFORMATION_SENSORS = { } # Switch -SURVEILLANCE_SWITCH = { +SURVEILLANCE_SWITCH: dict[str, EntityInfo] = { f"{SynoSurveillanceStation.HOME_MODE_API_KEY}:home_mode": { ENTITY_NAME: "home mode", ENTITY_UNIT: None, diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index d4a9b0bb7fc..4cf982e15f6 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -14,6 +15,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow @@ -30,19 +32,20 @@ from .const import ( SYNO_API, TEMP_SENSORS_KEYS, UTILISATION_SENSORS, + EntityInfo, ) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS Sensor.""" data = hass.data[DOMAIN][entry.unique_id] - api = data[SYNO_API] + api: SynoApi = data[SYNO_API] coordinator = data[COORDINATOR_CENTRAL] - entities = [ + entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [ SynoDSMUtilSensor( api, sensor_type, UTILISATION_SENSORS[sensor_type], coordinator ) @@ -91,7 +94,7 @@ class SynoDSMSensor(SynologyDSMBaseEntity): """Mixin for sensor specific attributes.""" @property - def unit_of_measurement(self) -> str: + def unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if self.entity_type in TEMP_SENSORS_KEYS: return self.hass.config.units.temperature_unit @@ -102,7 +105,7 @@ class SynoDSMUtilSensor(SynoDSMSensor, SensorEntity): """Representation a Synology Utilisation sensor.""" @property - def state(self): + def state(self) -> Any | None: """Return the state.""" attr = getattr(self._api.utilisation, self.entity_type) if callable(attr): @@ -134,7 +137,7 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor, SensorEntity) """Representation a Synology Storage sensor.""" @property - def state(self): + def state(self) -> Any | None: """Return the state.""" attr = getattr(self._api.storage, self.entity_type)(self._device_id) if attr is None: @@ -158,16 +161,16 @@ class SynoDSMInfoSensor(SynoDSMSensor, SensorEntity): self, api: SynoApi, entity_type: str, - entity_info: dict[str, str], - coordinator: DataUpdateCoordinator, - ): + entity_info: EntityInfo, + coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]], + ) -> None: """Initialize the Synology SynoDSMInfoSensor entity.""" super().__init__(api, entity_type, entity_info, coordinator) - self._previous_uptime = None - self._last_boot = None + self._previous_uptime: str | None = None + self._last_boot: str | None = None @property - def state(self): + def state(self) -> Any | None: """Return the state.""" attr = getattr(self._api.information, self.entity_type) if attr is None: diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 817c38674d5..27ffbfde799 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from synology_dsm.api.surveillance_station import SynoSurveillanceStation @@ -9,21 +10,28 @@ from homeassistant.components.switch import ToggleEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi, SynologyDSMBaseEntity -from .const import COORDINATOR_SWITCHES, DOMAIN, SURVEILLANCE_SWITCH, SYNO_API +from .const import ( + COORDINATOR_SWITCHES, + DOMAIN, + SURVEILLANCE_SWITCH, + SYNO_API, + EntityInfo, +) _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS switch.""" data = hass.data[DOMAIN][entry.unique_id] - api = data[SYNO_API] + api: SynoApi = data[SYNO_API] entities = [] @@ -32,7 +40,7 @@ async def async_setup_entry( version = info["data"]["CMSMinVersion"] # initial data fetch - coordinator = data[COORDINATOR_SWITCHES] + coordinator: DataUpdateCoordinator = data[COORDINATOR_SWITCHES] await coordinator.async_refresh() entities += [ SynoDSMSurveillanceHomeModeToggle( @@ -47,14 +55,16 @@ async def async_setup_entry( class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity): """Representation a Synology Surveillance Station Home Mode toggle.""" + coordinator: DataUpdateCoordinator[dict[str, dict[str, bool]]] + def __init__( self, api: SynoApi, entity_type: str, - entity_info: dict[str, str], + entity_info: EntityInfo, version: str, - coordinator: DataUpdateCoordinator, - ): + coordinator: DataUpdateCoordinator[dict[str, dict[str, bool]]], + ) -> None: """Initialize a Synology Surveillance Station Home Mode.""" super().__init__( api, @@ -69,7 +79,7 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity): """Return the state.""" return self.coordinator.data["switches"][self.entity_type] - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on Home mode.""" _LOGGER.debug( "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)", @@ -80,7 +90,7 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity): ) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off Home mode.""" _LOGGER.debug( "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)", @@ -103,8 +113,7 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity): "identifiers": { ( DOMAIN, - self._api.information.serial, - SynoSurveillanceStation.INFO_API_KEY, + f"{self._api.information.serial}_{SynoSurveillanceStation.INFO_API_KEY}", ) }, "name": "Surveillance Station", diff --git a/mypy.ini b/mypy.ini index 7d78261e238..ef86441ef4d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -529,6 +529,19 @@ warn_return_any = true warn_unreachable = true warn_unused_ignores = true +[mypy-homeassistant.components.synology_dsm.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + [mypy-homeassistant.components.systemmonitor.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1232,9 +1245,6 @@ ignore_errors = true [mypy-homeassistant.components.switcher_kis.*] ignore_errors = true -[mypy-homeassistant.components.synology_dsm.*] -ignore_errors = true - [mypy-homeassistant.components.synology_srm.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 85dcab6efef..a810e4baf98 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -205,7 +205,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.surepetcare.*", "homeassistant.components.switchbot.*", "homeassistant.components.switcher_kis.*", - "homeassistant.components.synology_dsm.*", "homeassistant.components.synology_srm.*", "homeassistant.components.system_health.*", "homeassistant.components.system_log.*", From 041838bdd714d4afe2e7a044ead88ce1788b8a52 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 May 2021 23:30:03 +0200 Subject: [PATCH 284/852] Upgrade requests-mock to 1.9.2 (#50377) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index efa1a140482..3386f3561ce 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -21,7 +21,7 @@ pytest-sugar==0.9.4 pytest-timeout==1.4.2 pytest-xdist==2.2.1 pytest==6.2.4 -requests_mock==1.8.0 +requests_mock==1.9.2 responses==0.12.0 respx==0.17.0 stdlib-list==0.7.0 From c1a316b2ecb54f354b3961f18e075461dc87eec7 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 9 May 2021 14:30:38 -0700 Subject: [PATCH 285/852] Increase httpx timeout for Tesla (#50376) --- homeassistant/components/tesla/__init__.py | 2 +- homeassistant/components/tesla/config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 54e3bab3f44..d945d87243e 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -140,7 +140,7 @@ async def async_setup_entry(hass, config_entry): hass.data.setdefault(DOMAIN, {}) config = config_entry.data # Because users can have multiple accounts, we always create a new session so they have separate cookies - async_client = httpx.AsyncClient(headers={USER_AGENT: SERVER_SOFTWARE}) + async_client = httpx.AsyncClient(headers={USER_AGENT: SERVER_SOFTWARE}, timeout=60) email = config_entry.title if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]: scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL] diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index 706c91ae59b..cbb25f8663b 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -149,7 +149,7 @@ async def validate_input(hass: core.HomeAssistant, data): """ config = {} - async_client = httpx.AsyncClient(headers={USER_AGENT: SERVER_SOFTWARE}) + async_client = httpx.AsyncClient(headers={USER_AGENT: SERVER_SOFTWARE}, timeout=60) try: controller = TeslaAPI( From e284a14ccdad86230edd9694188ce83f2e8aa188 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 10 May 2021 00:06:47 +0200 Subject: [PATCH 286/852] Upgrade async_upnp_client to 0.17.0 (#50371) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dmr/media_player.py | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 5a00ae0001e..997c0585c6d 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.16.2"], + "requirements": ["async-upnp-client==0.17.0"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 260c7c4d98f..b2999a5ae56 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -150,7 +150,7 @@ async def async_setup_platform( ) # create upnp device - factory = UpnpFactory(requester, disable_state_variable_validation=True) + factory = UpnpFactory(requester, non_strict=True) try: upnp_device = await factory.async_create_device(url) except (asyncio.TimeoutError, aiohttp.ClientError) as err: diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 76dd3976890..54cec45f1d0 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "defusedxml==0.6.0", "netdisco==2.8.3", - "async-upnp-client==0.16.2" + "async-upnp-client==0.17.0" ], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index e397d97f468..f6dd559ebd5 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.16.2"], + "requirements": ["async-upnp-client==0.17.0"], "codeowners": ["@StevenLooman"], "ssdp": [ { diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c79992e0948..13960edfed5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.0 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.16.2 +async-upnp-client==0.17.0 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.2.3 diff --git a/requirements_all.txt b/requirements_all.txt index 4363cc8956c..df1b2e27052 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -295,7 +295,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.16.2 +async-upnp-client==0.17.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b323cb3fed0..de8618ed346 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -187,7 +187,7 @@ arcam-fmj==0.5.3 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.16.2 +async-upnp-client==0.17.0 # homeassistant.components.aurora auroranoaa==0.0.2 From 65fb852e035f5c36f7b76cbc7eca5187b15d77ff Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 9 May 2021 16:52:24 -0700 Subject: [PATCH 287/852] Bump androidtv to 0.0.59 (#50367) --- 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 b86a6d9e40a..9ab02fec68a 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.3.1", - "androidtv[async]==0.0.58", + "androidtv[async]==0.0.59", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion"], diff --git a/requirements_all.txt b/requirements_all.txt index df1b2e27052..7c1ccf475fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,7 +257,7 @@ ambiclimate==0.2.1 amcrest==1.7.2 # homeassistant.components.androidtv -androidtv[async]==0.0.58 +androidtv[async]==0.0.59 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de8618ed346..168fa5c0c61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ airly==1.1.0 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.58 +androidtv[async]==0.0.59 # homeassistant.components.apns apns2==0.3.0 From dfe8ab6666ce7332433bb97530a6e572ed9ca1ec Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 10 May 2021 00:04:47 +0000 Subject: [PATCH 288/852] [ci skip] Translation update --- .../buienradar/translations/de.json | 18 ++++++++++++ .../components/cast/translations/nl.json | 15 ++++++++++ .../components/cast/translations/zh-Hant.json | 21 ++++++++++++-- .../coronavirus/translations/de.json | 2 +- .../enphase_envoy/translations/de.json | 2 +- .../components/epson/translations/nl.json | 3 +- .../epson/translations/zh-Hant.json | 3 +- .../components/flume/translations/de.json | 8 +++++- .../components/fritz/translations/de.json | 10 ++++++- .../components/lyric/translations/de.json | 4 +-- .../components/motioneye/translations/de.json | 18 ++++++++---- .../components/mutesync/translations/de.json | 5 ++-- .../components/myq/translations/de.json | 8 +++++- .../components/nam/translations/de.json | 19 +++++++++++++ .../components/nam/translations/nl.json | 24 ++++++++++++++++ .../components/nam/translations/pl.json | 24 ++++++++++++++++ .../components/nam/translations/zh-Hant.json | 24 ++++++++++++++++ .../components/picnic/translations/de.json | 2 +- .../rainmachine/translations/nl.json | 1 + .../components/syncthing/translations/de.json | 19 +++++++++++++ .../components/syncthing/translations/nl.json | 22 +++++++++++++++ .../components/syncthing/translations/pl.json | 7 +++++ .../components/syncthing/translations/ru.json | 22 +++++++++++++++ .../syncthing/translations/zh-Hant.json | 22 +++++++++++++++ .../system_bridge/translations/de.json | 28 +++++++++++++++++++ .../system_bridge/translations/pl.json | 19 +++++++++++++ .../components/timer/translations/de.json | 4 +-- .../components/weather/translations/it.json | 2 +- 28 files changed, 333 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/buienradar/translations/de.json create mode 100644 homeassistant/components/nam/translations/de.json create mode 100644 homeassistant/components/nam/translations/nl.json create mode 100644 homeassistant/components/nam/translations/pl.json create mode 100644 homeassistant/components/nam/translations/zh-Hant.json create mode 100644 homeassistant/components/syncthing/translations/de.json create mode 100644 homeassistant/components/syncthing/translations/nl.json create mode 100644 homeassistant/components/syncthing/translations/pl.json create mode 100644 homeassistant/components/syncthing/translations/ru.json create mode 100644 homeassistant/components/syncthing/translations/zh-Hant.json create mode 100644 homeassistant/components/system_bridge/translations/de.json create mode 100644 homeassistant/components/system_bridge/translations/pl.json diff --git a/homeassistant/components/buienradar/translations/de.json b/homeassistant/components/buienradar/translations/de.json new file mode 100644 index 00000000000..4fbb298d38c --- /dev/null +++ b/homeassistant/components/buienradar/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, + "error": { + "already_configured": "Standort ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/nl.json b/homeassistant/components/cast/translations/nl.json index 02bf7514761..aaf662c91f3 100644 --- a/homeassistant/components/cast/translations/nl.json +++ b/homeassistant/components/cast/translations/nl.json @@ -25,6 +25,21 @@ "invalid_known_hosts": "Bekende hosts moet een door komma's gescheiden lijst van hosts zijn." }, "step": { + "advanced_options": { + "data": { + "ignore_cec": "Negeer CEC", + "uuid": "Toegestane UUID's" + }, + "description": "Toegestane UUID's - Een door komma's gescheiden lijst met UUID's van Cast-apparaten om toe te voegen aan Home Assistant. Gebruik dit alleen als u niet alle beschikbare cast-apparaten wilt toevoegen.\n CEC negeren - Een door komma's gescheiden lijst met Chromecasts die CEC-gegevens moeten negeren om de actieve invoer te bepalen. Dit wordt doorgegeven aan pychromecast.IGNORE_CEC.", + "title": "Geavanceerde Google Cast configuratie" + }, + "basic_options": { + "data": { + "known_hosts": "Bekende hosts" + }, + "description": "Bekende hosts - Een door komma's gescheiden lijst met hostnamen of IP-adressen van cast-apparaten, te gebruiken als mDNS-detectie niet werkt.", + "title": "Google Cast configuratie" + }, "options": { "data": { "ignore_cec": "Optionele lijst die zal worden doorgegeven aan pychromecast.IGNORE_CEC.", diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json index 00810ade520..947fce7a44b 100644 --- a/homeassistant/components/cast/translations/zh-Hant.json +++ b/homeassistant/components/cast/translations/zh-Hant.json @@ -10,10 +10,10 @@ "step": { "config": { "data": { - "known_hosts": "\u5047\u5982 mDNS \u63a2\u7d22\u7121\u6cd5\u4f5c\u7528\uff0c\u5247\u70ba\u5df2\u77e5\u4e3b\u6a5f\u7684\u9078\u9805\u5217\u8868\u3002" + "known_hosts": "\u5df2\u77e5\u4e3b\u6a5f" }, - "description": "\u8acb\u8f38\u5165 Google Cast \u8a2d\u5b9a\u3002", - "title": "Google Cast" + "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u63a2\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", + "title": "Google Cast \u8a2d\u5b9a" }, "confirm": { "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" @@ -25,6 +25,21 @@ "invalid_known_hosts": "\u5df2\u77e5\u4e3b\u6a5f\u5fc5\u9808\u4ee5\u9017\u865f\u5206\u4e3b\u6a5f\u5217\u8868\u3002" }, "step": { + "advanced_options": { + "data": { + "ignore_cec": "\u5ffd\u7565 CEC", + "uuid": "\u5df2\u5141\u8a31 UUID" + }, + "description": "\u5df2\u5141\u8a31 UUID - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e UUID \u5217\u8868\u4ee5\u65b0\u589e\u81f3 Home Assistant\u3002\u50c5\u65bc\u4e0d\u60f3\u5168\u90e8\u65b0\u589e\u6642\u4f7f\u7528\u3002\n\u5ffd\u7565 CEC - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u5217\u8868\u3001\u5ffd\u7565\u5176 CEC \u63a7\u5236\u4ee5\u907f\u514d\u555f\u52d5\u8f38\u5165\u4f86\u6e90\u3002\u8cc7\u6599\u5c07\u6703\u50b3\u905e\u81f3 pychromecast.IGNORE_CEC\u3002", + "title": "Google Cast \u9032\u968e\u8a2d\u5b9a" + }, + "basic_options": { + "data": { + "known_hosts": "\u5df2\u77e5\u4e3b\u6a5f" + }, + "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u63a2\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", + "title": "Google Cast \u8a2d\u5b9a" + }, "options": { "data": { "ignore_cec": "\u9078\u9805\u5217\u8868\u5c07\u50b3\u905e\u81f3 pychromecast.IGNORE_CEC\u3002", diff --git a/homeassistant/components/coronavirus/translations/de.json b/homeassistant/components/coronavirus/translations/de.json index caa74a64ab9..45eaff64200 100644 --- a/homeassistant/components/coronavirus/translations/de.json +++ b/homeassistant/components/coronavirus/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Dieses Land ist bereits konfiguriert.", - "cannot_connect": "Verbinden fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/enphase_envoy/translations/de.json b/homeassistant/components/enphase_envoy/translations/de.json index 5aee8a03e6b..b1fb53829ad 100644 --- a/homeassistant/components/enphase_envoy/translations/de.json +++ b/homeassistant/components/enphase_envoy/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "reauth_successful": "Erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/epson/translations/nl.json b/homeassistant/components/epson/translations/nl.json index d5ae90c0e38..d7521c945f2 100644 --- a/homeassistant/components/epson/translations/nl.json +++ b/homeassistant/components/epson/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "powered_off": "Is de projector ingeschakeld? U moet de projector inschakelen voor de eerste configuratie." }, "step": { "user": { diff --git a/homeassistant/components/epson/translations/zh-Hant.json b/homeassistant/components/epson/translations/zh-Hant.json index 9ff2143ed9d..25ae09cb4b4 100644 --- a/homeassistant/components/epson/translations/zh-Hant.json +++ b/homeassistant/components/epson/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "powered_off": "\u6295\u5f71\u6a5f\u662f\u5426\u70ba\u95dc\u9589\u72c0\u614b\uff1f\u5fc5\u9808\u958b\u555f\u6295\u5f71\u6a5f\u624d\u80fd\u9032\u884c\u521d\u59cb\u8a2d\u5b9a\u3002" }, "step": { "user": { diff --git a/homeassistant/components/flume/translations/de.json b/homeassistant/components/flume/translations/de.json index c38a5593ac7..e229427ea6f 100644 --- a/homeassistant/components/flume/translations/de.json +++ b/homeassistant/components/flume/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -9,6 +10,11 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + } + }, "user": { "data": { "client_id": "Client-ID", diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index 19005c7fd23..037c9eb07f1 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -1,7 +1,15 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, "error": { - "invalid_auth": "Authentifizierung fehlgeschlagen" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "connection_error": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "flow_title": "FRITZ! Box Tools: {name}", "step": { diff --git a/homeassistant/components/lyric/translations/de.json b/homeassistant/components/lyric/translations/de.json index 6f1660f4dd3..62404451984 100644 --- a/homeassistant/components/lyric/translations/de.json +++ b/homeassistant/components/lyric/translations/de.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", - "reauth_successful": "Erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "create_entry": { "default": "Erfolgreich authentifiziert" @@ -14,7 +14,7 @@ }, "reauth_confirm": { "description": "Die Lyric-Integration muss Ihr Konto neu authentifizieren.", - "title": "Integration erneut Authentifizieren" + "title": "Integration erneut authentifizieren" } } } diff --git a/homeassistant/components/motioneye/translations/de.json b/homeassistant/components/motioneye/translations/de.json index 8bd6663d2a8..fec4680a9e1 100644 --- a/homeassistant/components/motioneye/translations/de.json +++ b/homeassistant/components/motioneye/translations/de.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Der Service ist bereits eingerichtet", - "reauth_successful": "Erneute Authentifizierung erfolgreich" + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "cannot_connect": "Fehler beim Verbinden", - "invalid_auth": "Authentifizerung fehlgeschlagen", - "invalid_url": "Ung\u00fcltige URL" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_url": "Ung\u00fcltige URL", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/de.json b/homeassistant/components/mutesync/translations/de.json index 51edf2d9226..ac8a06458b9 100644 --- a/homeassistant/components/mutesync/translations/de.json +++ b/homeassistant/components/mutesync/translations/de.json @@ -1,8 +1,9 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler", - "invalid_auth": "Aktivieren Sie die Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Aktivieren Sie die Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", + "unknown": "Unerwarteter Fehler" }, "step": { "user": { diff --git a/homeassistant/components/myq/translations/de.json b/homeassistant/components/myq/translations/de.json index fafa38c7817..0a6941d6bcb 100644 --- a/homeassistant/components/myq/translations/de.json +++ b/homeassistant/components/myq/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -9,6 +10,11 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + } + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/nam/translations/de.json b/homeassistant/components/nam/translations/de.json new file mode 100644 index 00000000000..2920ef12e11 --- /dev/null +++ b/homeassistant/components/nam/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/nl.json b/homeassistant/components/nam/translations/nl.json new file mode 100644 index 00000000000..c6171ead0f4 --- /dev/null +++ b/homeassistant/components/nam/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "device_unsupported": "Het apparaat wordt niet ondersteund." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Wilt u Nettigo Air Monitor instellen bij {host} ?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Stel Nettigo Air Monitor integratie in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/pl.json b/homeassistant/components/nam/translations/pl.json new file mode 100644 index 00000000000..bdf5014428d --- /dev/null +++ b/homeassistant/components/nam/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "device_unsupported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Czy chcesz skonfigurowa\u0107 Nettigo Air Monitor {host}?" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Konfiguracja integracji Nettigo Air Monitor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/zh-Hant.json b/homeassistant/components/nam/translations/zh-Hant.json new file mode 100644 index 00000000000..5d0b3f179af --- /dev/null +++ b/homeassistant/components/nam/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "device_unsupported": "\u88dd\u7f6e\u4e0d\u652f\u63f4\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u5740\u70ba {host} \u7684 Nettigo Air Monitor\uff1f" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8a2d\u5b9a Nettigo Air Monitor \u6574\u5408\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/de.json b/homeassistant/components/picnic/translations/de.json index 2369d323479..1a11e00664c 100644 --- a/homeassistant/components/picnic/translations/de.json +++ b/homeassistant/components/picnic/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits eingerichtet" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json index 8b767ced6c0..f5b4103b9e7 100644 --- a/homeassistant/components/rainmachine/translations/nl.json +++ b/homeassistant/components/rainmachine/translations/nl.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Ongeldige authenticatie" }, + "flow_title": "RainMachine {ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/syncthing/translations/de.json b/homeassistant/components/syncthing/translations/de.json new file mode 100644 index 00000000000..90eb00fc001 --- /dev/null +++ b/homeassistant/components/syncthing/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "url": "URL", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/nl.json b/homeassistant/components/syncthing/translations/nl.json new file mode 100644 index 00000000000..358490c6c83 --- /dev/null +++ b/homeassistant/components/syncthing/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "title": "Syncthing integratie instellen", + "token": "Token", + "url": "URL", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/pl.json b/homeassistant/components/syncthing/translations/pl.json new file mode 100644 index 00000000000..6e104374b80 --- /dev/null +++ b/homeassistant/components/syncthing/translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/ru.json b/homeassistant/components/syncthing/translations/ru.json new file mode 100644 index 00000000000..337b0214c6b --- /dev/null +++ b/homeassistant/components/syncthing/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "title": "Syncthing", + "token": "\u0422\u043e\u043a\u0435\u043d", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/zh-Hant.json b/homeassistant/components/syncthing/translations/zh-Hant.json new file mode 100644 index 00000000000..f06c16ac157 --- /dev/null +++ b/homeassistant/components/syncthing/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "title": "\u8a2d\u5b9a Syncthing \u6574\u5408", + "token": "\u6b0a\u6756", + "url": "\u7db2\u5740", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/de.json b/homeassistant/components/system_bridge/translations/de.json new file mode 100644 index 00000000000..abb9dd3f7f5 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "authenticate": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/pl.json b/homeassistant/components/system_bridge/translations/pl.json new file mode 100644 index 00000000000..423e0eb2f65 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/timer/translations/de.json b/homeassistant/components/timer/translations/de.json index cd35c9bac5e..47cf5b15f23 100644 --- a/homeassistant/components/timer/translations/de.json +++ b/homeassistant/components/timer/translations/de.json @@ -1,9 +1,9 @@ { "state": { "_": { - "active": "aktiv", + "active": "Aktiv", "idle": "Leerlauf", - "paused": "pausiert" + "paused": "Pausiert" } } } \ No newline at end of file diff --git a/homeassistant/components/weather/translations/it.json b/homeassistant/components/weather/translations/it.json index 171b29673cd..fd3f506a6d0 100644 --- a/homeassistant/components/weather/translations/it.json +++ b/homeassistant/components/weather/translations/it.json @@ -9,7 +9,7 @@ "lightning": "Temporale", "lightning-rainy": "Temporale, piovoso", "partlycloudy": "Parzialmente nuvoloso", - "pouring": "Piogge intense", + "pouring": "Rovescio", "rainy": "Piovoso", "snowy": "Nevoso", "snowy-rainy": "Nevoso, piovoso", From f5816160642887666412bb18defcaf21b7638696 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 May 2021 19:32:00 -0500 Subject: [PATCH 289/852] Loosen flume dhcp discovery matching (#50379) --- homeassistant/components/flume/manifest.json | 7 +------ homeassistant/generated/dhcp.py | 8 +------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 1f6d7a38a47..d689f5fb17f 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -7,12 +7,7 @@ "config_flow": true, "dhcp": [ { - "hostname": "flume-gw-*", - "macaddress": "ECFABC*" - }, - { - "hostname": "flume-gw-*", - "macaddress": "B4E62D*" + "hostname": "flume-gw-*" } ], "iot_class": "cloud_polling" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 06783b5d666..d7b97dd29e9 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -64,13 +64,7 @@ DHCP = [ }, { "domain": "flume", - "hostname": "flume-gw-*", - "macaddress": "ECFABC*" - }, - { - "domain": "flume", - "hostname": "flume-gw-*", - "macaddress": "B4E62D*" + "hostname": "flume-gw-*" }, { "domain": "hunterdouglas_powerview", From 1c98df5d18e6051cd097dfc1765c8a442cc53cae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 May 2021 11:53:37 +0200 Subject: [PATCH 290/852] Remove YAML configuration from Somfy MyLink (#50359) * Remove YAML configuration from Somfy MyLink * Keep deprecation warning --- .../components/somfy_mylink/__init__.py | 119 +---------- .../components/somfy_mylink/const.py | 2 - .../somfy_mylink/test_config_flow.py | 195 ------------------ 3 files changed, 5 insertions(+), 311 deletions(-) diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index dfb0220a531..ae6a77a1e2c 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -3,89 +3,25 @@ import asyncio import logging from somfy_mylink_synergy import SomfyMyLinkSynergy -import voluptuous as vol -from homeassistant.components.cover import ENTITY_ID_FORMAT -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv -from homeassistant.util import slugify -from .const import ( - CONF_DEFAULT_REVERSE, - CONF_ENTITY_CONFIG, - CONF_REVERSE, - CONF_REVERSED_TARGET_IDS, - CONF_SYSTEM_ID, - DATA_SOMFY_MYLINK, - DEFAULT_PORT, - DOMAIN, - MYLINK_STATUS, - PLATFORMS, -) +from .const import CONF_SYSTEM_ID, DATA_SOMFY_MYLINK, DOMAIN, MYLINK_STATUS, PLATFORMS -CONFIG_OPTIONS = (CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG) UNDO_UPDATE_LISTENER = "undo_update_listener" _LOGGER = logging.getLogger(__name__) - -def validate_entity_config(values): - """Validate config entry for CONF_ENTITY.""" - entity_config_schema = vol.Schema({vol.Optional(CONF_REVERSE): cv.boolean}) - if not isinstance(values, dict): - raise vol.Invalid("expected a dictionary") - entities = {} - for entity_id, config in values.items(): - entity = cv.entity_id(entity_id) - config = entity_config_schema(config) - entities[entity] = config - return entities - - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_SYSTEM_ID): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_DEFAULT_REVERSE, default=False): cv.boolean, - vol.Optional( - CONF_ENTITY_CONFIG, default={} - ): validate_entity_config, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up the MyLink platform.""" - - conf = config.get(DOMAIN) - hass.data.setdefault(DOMAIN, {}) - - if not conf: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - ) - return True +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Somfy MyLink from a config entry.""" - _async_import_options_from_data_if_missing(hass, entry) + hass.data.setdefault(DOMAIN, {}) config = entry.data somfy_mylink = SomfyMyLinkSynergy( @@ -111,8 +47,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if "result" not in mylink_status: raise ConfigEntryNotReady("The Somfy MyLink device returned an empty result") - _async_migrate_entity_config(hass, entry, mylink_status) - undo_listener = entry.add_update_listener(_async_update_listener) hass.data[DOMAIN][entry.entry_id] = { @@ -131,49 +65,6 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) -@callback -def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): - options = dict(entry.options) - data = dict(entry.data) - modified = False - - for importable_option in CONFIG_OPTIONS: - if importable_option not in options and importable_option in data: - options[importable_option] = data.pop(importable_option) - modified = True - - if modified: - hass.config_entries.async_update_entry(entry, data=data, options=options) - - -@callback -def _async_migrate_entity_config( - hass: HomeAssistant, entry: ConfigEntry, mylink_status: dict -): - if CONF_ENTITY_CONFIG not in entry.options: - return - - options = dict(entry.options) - - reversed_target_ids = options[CONF_REVERSED_TARGET_IDS] = {} - legacy_entry_config = options[CONF_ENTITY_CONFIG] - default_reverse = options.get(CONF_DEFAULT_REVERSE) - - for cover in mylink_status["result"]: - legacy_entity_id = ENTITY_ID_FORMAT.format(slugify(cover["name"])) - target_id = cover["targetID"] - - entity_config = legacy_entry_config.get(legacy_entity_id, {}) - if entity_config.get(CONF_REVERSE, default_reverse): - reversed_target_ids[target_id] = True - - for legacy_key in (CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG): - if legacy_key in options: - del options[legacy_key] - - hass.config_entries.async_update_entry(entry, data=entry.data, options=options) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/somfy_mylink/const.py b/homeassistant/components/somfy_mylink/const.py index bf58ee1af92..4d4c4b58eae 100644 --- a/homeassistant/components/somfy_mylink/const.py +++ b/homeassistant/components/somfy_mylink/const.py @@ -1,9 +1,7 @@ """Component for the Somfy MyLink device supporting the Synergy API.""" -CONF_ENTITY_CONFIG = "entity_config" CONF_SYSTEM_ID = "system_id" CONF_REVERSE = "reverse" -CONF_DEFAULT_REVERSE = "default_reverse" CONF_TARGET_NAME = "target_name" CONF_REVERSED_TARGET_IDS = "reversed_target_ids" CONF_TARGET_ID = "target_id" diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index 59f6bd37407..d74b4392f31 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -7,9 +7,6 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.somfy_mylink.const import ( - CONF_DEFAULT_REVERSE, - CONF_ENTITY_CONFIG, - CONF_REVERSE, CONF_REVERSED_TARGET_IDS, CONF_SYSTEM_ID, DOMAIN, @@ -32,8 +29,6 @@ async def test_form_user(hass): "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", return_value={"any": "data"}, ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.somfy_mylink.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -54,7 +49,6 @@ async def test_form_user(hass): CONF_PORT: 1234, CONF_SYSTEM_ID: "456", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -77,8 +71,6 @@ async def test_form_user_already_configured(hass): "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", return_value={"any": "data"}, ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.somfy_mylink.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -93,116 +85,6 @@ async def test_form_user_already_configured(hass): await hass.async_block_till_done() assert result2["type"] == "abort" - assert len(mock_setup.mock_calls) == 0 - assert len(mock_setup_entry.mock_calls) == 0 - - -async def test_form_import(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - with patch( - "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - return_value={"any": "data"}, - ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.somfy_mylink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: 456, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "MyLink 1.1.1.1" - assert result["data"] == { - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: 456, - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_import_with_entity_config(hass): - """Test we can import entity config.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - with patch( - "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - return_value={"any": "data"}, - ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.somfy_mylink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: 456, - CONF_DEFAULT_REVERSE: True, - CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "MyLink 1.1.1.1" - assert result["data"] == { - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: 456, - CONF_DEFAULT_REVERSE: True, - CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_import_already_exists(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - config_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - return_value={"any": "data"}, - ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.somfy_mylink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: "456", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "abort" - assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -354,77 +236,6 @@ async def test_options_with_targets(hass, reversed): await hass.async_block_till_done() -@pytest.mark.parametrize("reversed", [True, False]) -async def test_form_import_with_entity_config_modify_options(hass, reversed): - """Test we can import entity config and modify options.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - mock_imported_config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: "456", - CONF_DEFAULT_REVERSE: True, - CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, - }, - ) - mock_imported_config_entry.add_to_hass(hass) - - mock_status_info = { - "result": [ - {"targetID": "1.1", "name": "xyz"}, - {"targetID": "1.2", "name": "zulu"}, - ] - } - - with patch( - "homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info", - return_value=mock_status_info, - ): - assert await hass.config_entries.async_setup( - mock_imported_config_entry.entry_id - ) - await hass.async_block_till_done() - - assert mock_imported_config_entry.options == { - "reversed_target_ids": {"1.2": True} - } - - result = await hass.config_entries.options.async_init( - mock_imported_config_entry.entry_id - ) - await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"target_id": "1.2"}, - ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"reverse": reversed}, - ) - - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - - result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"target_id": None}, - ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - # Will not be altered if nothing changes - assert mock_imported_config_entry.options == { - CONF_REVERSED_TARGET_IDS: {"1.2": reversed}, - } - - await hass.async_block_till_done() - - async def test_form_user_already_configured_from_dhcp(hass): """Test we abort if already configured from dhcp.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -439,8 +250,6 @@ async def test_form_user_already_configured_from_dhcp(hass): "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", return_value={"any": "data"}, ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.somfy_mylink.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -457,7 +266,6 @@ async def test_form_user_already_configured_from_dhcp(hass): await hass.async_block_till_done() assert result["type"] == "abort" - assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -501,8 +309,6 @@ async def test_dhcp_discovery(hass): "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", return_value={"any": "data"}, ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.somfy_mylink.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -523,5 +329,4 @@ async def test_dhcp_discovery(hass): CONF_PORT: 1234, CONF_SYSTEM_ID: "456", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From b89c53f759a47d2eeb00dab5cf62e25d6c2ab059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 10 May 2021 13:13:45 +0300 Subject: [PATCH 291/852] Type hint device registry identifiers as set of str 2-tuples (#50355) * Type hint device registry identifiers as set of str 2-tuples * Fix airly test * Really fix airly test, add another migration test --- homeassistant/components/airly/__init__.py | 21 ++++++++++++---- homeassistant/components/airly/air_quality.py | 3 +-- homeassistant/components/airly/sensor.py | 3 +-- .../components/huawei_lte/__init__.py | 2 +- homeassistant/components/syncthru/__init__.py | 2 +- homeassistant/helpers/device_registry.py | 24 +++++++++---------- homeassistant/helpers/entity.py | 2 +- tests/components/airly/test_init.py | 9 ++++--- 8 files changed, 39 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 6fa8914e822..26e14a7642e 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -77,14 +77,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, unique_id=f"{latitude}-{longitude}" ) - # identifiers in device_info should use Tuple[str, str, str] type, but latitude and + # identifiers in device_info should use tuple[str, str] type, but latitude and # longitude are float, so we convert old device entries to use correct types + # We used to use a str 3-tuple here sometime, convert that to a 2-tuple too. device_registry = await async_get_registry(hass) old_ids = (DOMAIN, latitude, longitude) - device_entry = device_registry.async_get_device({old_ids}) - if device_entry and entry.entry_id in device_entry.config_entries: - new_ids = (DOMAIN, str(latitude), str(longitude)) - device_registry.async_update_device(device_entry.id, new_identifiers={new_ids}) + for old_ids in ( + (DOMAIN, latitude, longitude), + ( + DOMAIN, + str(latitude), + str(longitude), + ), + ): + device_entry = device_registry.async_get_device({old_ids}) # type: ignore[arg-type] + if device_entry and entry.entry_id in device_entry.config_entries: + new_ids = (DOMAIN, f"{latitude}-{longitude}") + device_registry.async_update_device( + device_entry.id, new_identifiers={new_ids} + ) websession = async_get_clientsession(hass) diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 190bc326d0c..337d3a723fa 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -109,8 +109,7 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): "identifiers": { ( DOMAIN, - str(self.coordinator.latitude), - str(self.coordinator.longitude), + f"{self.coordinator.latitude}-{self.coordinator.longitude}", ) }, "name": DEFAULT_NAME, diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index a3d9a2981d2..b978afb25a9 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -100,8 +100,7 @@ class AirlySensor(CoordinatorEntity, SensorEntity): "identifiers": { ( DOMAIN, - str(self.coordinator.latitude), - str(self.coordinator.longitude), + f"{self.coordinator.latitude}-{self.coordinator.longitude}", ) }, "name": DEFAULT_NAME, diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 7bd30cbdbf6..f36ae97840b 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -163,7 +163,7 @@ class Router: return DEFAULT_DEVICE_NAME @property - def device_identifiers(self) -> set[tuple[str, ...]]: + def device_identifiers(self) -> set[tuple[str, str]]: """Get router identifiers for device registry.""" try: return {(DOMAIN, self.data[KEY_DEVICE_INFORMATION]["SerialNumber"])} diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 5c28e6f029a..9045b82e2ac 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -76,7 +76,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -def device_identifiers(printer: SyncThru) -> set[tuple[str, ...]] | None: +def device_identifiers(printer: SyncThru) -> set[tuple[str, str]] | None: """Get device identifiers for device registry.""" serial = printer.serial_number() if serial is None: diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a448fd1c198..9f09bbbf642 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -45,7 +45,7 @@ ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 class _DeviceIndex(NamedTuple): - identifiers: dict[tuple[str, ...], str] + identifiers: dict[tuple[str, str], str] connections: dict[tuple[str, str], str] @@ -55,7 +55,7 @@ class DeviceEntry: config_entries: set[str] = attr.ib(converter=set, factory=set) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) - identifiers: set[tuple[str, ...]] = attr.ib(converter=set, factory=set) + identifiers: set[tuple[str, str]] = attr.ib(converter=set, factory=set) manufacturer: str | None = attr.ib(default=None) model: str | None = attr.ib(default=None) name: str | None = attr.ib(default=None) @@ -92,7 +92,7 @@ class DeletedDeviceEntry: config_entries: set[str] = attr.ib() connections: set[tuple[str, str]] = attr.ib() - identifiers: set[tuple[str, ...]] = attr.ib() + identifiers: set[tuple[str, str]] = attr.ib() id: str = attr.ib() orphaned_timestamp: float | None = attr.ib() @@ -100,7 +100,7 @@ class DeletedDeviceEntry: self, config_entry_id: str, connections: set[tuple[str, str]], - identifiers: set[tuple[str, ...]], + identifiers: set[tuple[str, str]], ) -> DeviceEntry: """Create DeviceEntry from DeletedDeviceEntry.""" return DeviceEntry( @@ -135,7 +135,7 @@ def format_mac(mac: str) -> str: def _async_get_device_id_from_index( devices_index: _DeviceIndex, - identifiers: set[tuple[str, ...]], + identifiers: set[tuple[str, str]], connections: set[tuple[str, str]] | None, ) -> str | None: """Check if device has previously been registered.""" @@ -172,7 +172,7 @@ class DeviceRegistry: @callback def async_get_device( self, - identifiers: set[tuple[str, ...]], + identifiers: set[tuple[str, str]], connections: set[tuple[str, str]] | None = None, ) -> DeviceEntry | None: """Check if device is registered.""" @@ -185,7 +185,7 @@ class DeviceRegistry: def _async_get_deleted_device( self, - identifiers: set[tuple[str, ...]], + identifiers: set[tuple[str, str]], connections: set[tuple[str, str]] | None, ) -> DeletedDeviceEntry | None: """Check if device is deleted.""" @@ -245,7 +245,7 @@ class DeviceRegistry: *, config_entry_id: str, connections: set[tuple[str, str]] | None = None, - identifiers: set[tuple[str, ...]] | None = None, + identifiers: set[tuple[str, str]] | None = None, manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, @@ -329,7 +329,7 @@ class DeviceRegistry: model: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, name_by_user: str | None | UndefinedType = UNDEFINED, - new_identifiers: set[tuple[str, ...]] | UndefinedType = UNDEFINED, + new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, via_device_id: str | None | UndefinedType = UNDEFINED, remove_config_entry_id: str | UndefinedType = UNDEFINED, @@ -360,8 +360,8 @@ class DeviceRegistry: add_config_entry_id: str | UndefinedType = UNDEFINED, remove_config_entry_id: str | UndefinedType = UNDEFINED, merge_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED, - merge_identifiers: set[tuple[str, ...]] | UndefinedType = UNDEFINED, - new_identifiers: set[tuple[str, ...]] | UndefinedType = UNDEFINED, + merge_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, + new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, @@ -519,7 +519,7 @@ class DeviceRegistry: config_entries=set(device["config_entries"]), # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] - identifiers={tuple(iden) for iden in device["identifiers"]}, + identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] id=device["id"], # Introduced in 2021.2 orphaned_timestamp=device.get("orphaned_timestamp"), diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index d9b3b28a9ef..a9843387fd9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -115,7 +115,7 @@ class DeviceInfo(TypedDict, total=False): name: str connections: set[tuple[str, str]] - identifiers: set[tuple[str, ...]] + identifiers: set[tuple[str, str]] manufacturer: str model: str suggested_area: str diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index 5dcd465774d..da545eb5193 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -1,6 +1,8 @@ """Test init of Airly integration.""" from unittest.mock import patch +import pytest + from homeassistant.components.airly import set_update_interval from homeassistant.components.airly.const import DOMAIN from homeassistant.config_entries import ( @@ -188,7 +190,8 @@ async def test_unload_entry(hass, aioclient_mock): assert not hass.data.get(DOMAIN) -async def test_migrate_device_entry(hass, aioclient_mock): +@pytest.mark.parametrize("old_identifier", ((DOMAIN, 123, 456), (DOMAIN, "123", "456"))) +async def test_migrate_device_entry(hass, aioclient_mock, old_identifier): """Test device_info identifiers migration.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -207,13 +210,13 @@ async def test_migrate_device_entry(hass, aioclient_mock): device_reg = mock_device_registry(hass) device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, 123, 456)} + config_entry_id=config_entry.entry_id, identifiers={old_identifier} ) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() migrated_device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "123", "456")} + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "123-456")} ) assert device_entry.id == migrated_device_entry.id From b745d01877fbb074a0c8d12b84d002b7212a3dcf Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Mon, 10 May 2021 12:43:43 +0100 Subject: [PATCH 292/852] Fix synology_dsm typing (#50399) --- homeassistant/components/synology_dsm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 7a316b0381d..84b392eb3fe 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -205,7 +205,7 @@ async def async_setup_entry( # noqa: C901 for device in devices: old_identifier = list(next(iter(device.identifiers))) if len(old_identifier) > 2: - new_identifier: set[tuple[str, ...]] = { + new_identifier = { (old_identifier.pop(0), "_".join([str(x) for x in old_identifier])) } _LOGGER.debug( From 3a192896df9e85968bb65d80b321d485013c284e Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Mon, 10 May 2021 13:20:25 +0100 Subject: [PATCH 293/852] Enable some strict mypy checks globally (#50398) * Enable some strict mypy checks globally * Update mypy.ini --- homeassistant/components/dyson/fan.py | 2 +- mypy.ini | 104 +------------------------- script/hassfest/mypy_config.py | 5 +- 3 files changed, 6 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 5b24b4a9df7..38b4b511df4 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -234,7 +234,7 @@ class DysonFanEntity(DysonEntity, FanEntity): """Set the exact speed of the fan.""" raise NotImplementedError - def service_set_dyson_speed(self, dyson_speed: str) -> None: + def service_set_dyson_speed(self, dyson_speed: int) -> None: """Handle the service to set dyson speed.""" if dyson_speed not in SPEED_LIST_DYSON: raise ValueError(f'"{dyson_speed}" is not a valid Dyson speed') diff --git a/mypy.ini b/mypy.ini index ef86441ef4d..65d626cbf8d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -7,9 +7,11 @@ python_version = 3.8 show_error_codes = true follow_imports = silent ignore_missing_imports = true +strict_equality = true warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true +warn_unused_ignores = true check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -17,10 +19,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.*] check_untyped_defs = false @@ -30,10 +30,8 @@ disallow_untyped_calls = false disallow_untyped_decorators = false disallow_untyped_defs = false no_implicit_optional = false -strict_equality = false warn_return_any = false warn_unreachable = false -warn_unused_ignores = false [mypy-homeassistant.components] check_untyped_defs = true @@ -43,10 +41,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.airly.*] check_untyped_defs = true @@ -56,10 +52,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.automation.*] check_untyped_defs = true @@ -69,10 +63,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.binary_sensor.*] check_untyped_defs = true @@ -82,10 +74,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.bond.*] check_untyped_defs = true @@ -95,10 +85,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.brother.*] check_untyped_defs = true @@ -108,10 +96,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.calendar.*] check_untyped_defs = true @@ -121,10 +107,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.cover.*] check_untyped_defs = true @@ -134,10 +118,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.device_automation.*] check_untyped_defs = true @@ -147,10 +129,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.elgato.*] check_untyped_defs = true @@ -160,10 +140,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.frontend.*] check_untyped_defs = true @@ -173,10 +151,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.geo_location.*] check_untyped_defs = true @@ -186,10 +162,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.group.*] check_untyped_defs = true @@ -199,10 +173,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.history.*] check_untyped_defs = true @@ -212,10 +184,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.http.*] check_untyped_defs = true @@ -225,10 +195,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.huawei_lte.*] check_untyped_defs = true @@ -238,10 +206,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.hyperion.*] check_untyped_defs = true @@ -251,10 +217,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.image_processing.*] check_untyped_defs = true @@ -264,10 +228,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.integration.*] check_untyped_defs = true @@ -277,10 +239,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.knx.*] check_untyped_defs = true @@ -290,10 +250,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.light.*] check_untyped_defs = true @@ -303,10 +261,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.lock.*] check_untyped_defs = true @@ -316,10 +272,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.mailbox.*] check_untyped_defs = true @@ -329,10 +283,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.media_player.*] check_untyped_defs = true @@ -342,10 +294,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.nam.*] check_untyped_defs = true @@ -355,10 +305,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.notify.*] check_untyped_defs = true @@ -368,10 +316,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.number.*] check_untyped_defs = true @@ -381,10 +327,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.persistent_notification.*] check_untyped_defs = true @@ -394,10 +338,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.proximity.*] check_untyped_defs = true @@ -407,10 +349,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.recorder.purge] check_untyped_defs = true @@ -420,10 +360,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.recorder.repack] check_untyped_defs = true @@ -433,10 +371,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.remote.*] check_untyped_defs = true @@ -446,10 +382,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.scene.*] check_untyped_defs = true @@ -459,10 +393,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.sensor.*] check_untyped_defs = true @@ -472,10 +404,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.slack.*] check_untyped_defs = true @@ -485,10 +415,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.sonos.media_player] check_untyped_defs = true @@ -498,10 +426,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.sun.*] check_untyped_defs = true @@ -511,10 +437,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.switch.*] check_untyped_defs = true @@ -524,10 +448,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.synology_dsm.*] check_untyped_defs = true @@ -537,10 +459,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.systemmonitor.*] check_untyped_defs = true @@ -550,10 +470,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.tts.*] check_untyped_defs = true @@ -563,10 +481,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.vacuum.*] check_untyped_defs = true @@ -576,10 +492,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.water_heater.*] check_untyped_defs = true @@ -589,10 +503,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.weather.*] check_untyped_defs = true @@ -602,10 +514,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.websocket_api.*] check_untyped_defs = true @@ -615,10 +525,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.zeroconf.*] check_untyped_defs = true @@ -628,10 +536,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.zone.*] check_untyped_defs = true @@ -641,10 +547,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.zwave_js.*] check_untyped_defs = true @@ -654,10 +558,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-tests.*] check_untyped_defs = false @@ -667,10 +569,8 @@ disallow_untyped_calls = false disallow_untyped_decorators = false disallow_untyped_defs = false no_implicit_optional = false -strict_equality = false warn_return_any = false warn_unreachable = false -warn_unused_ignores = false [mypy-homeassistant.components.adguard.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a810e4baf98..54e5b5b13a7 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -256,10 +256,13 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { "python_version": "3.8", "show_error_codes": "true", "follow_imports": "silent", + # Enable some checks globally. "ignore_missing_imports": "true", + "strict_equality": "true", "warn_incomplete_stub": "true", "warn_redundant_casts": "true", "warn_unused_configs": "true", + "warn_unused_ignores": "true", } # This is basically the list of checks which is enabled for "strict=true". @@ -272,10 +275,8 @@ STRICT_SETTINGS: Final[list[str]] = [ "disallow_untyped_decorators", "disallow_untyped_defs", "no_implicit_optional", - "strict_equality", "warn_return_any", "warn_unreachable", - "warn_unused_ignores", # TODO: turn these on, address issues # "disallow_any_generics", # "no_implicit_reexport", From 0cdb8ad892fc91fa619224857be7dfd1b985e73f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 10 May 2021 07:49:11 -0500 Subject: [PATCH 294/852] Fix location of current_play_mode (#50386) --- homeassistant/components/sonos/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 583291ce203..c3971852ac6 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -500,7 +500,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if new_status == "TRANSITIONING": return - self._play_mode = event.current_play_mode if event else self.soco.play_mode + self._play_mode = ( + variables["current_play_mode"] if variables else self.soco.play_mode + ) self._uri = None self._media_duration = None self._media_image_url = None From 8e2b3aab4467382c00155e725280e5f82a1113e9 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Mon, 10 May 2021 14:12:15 +0100 Subject: [PATCH 295/852] Enable strict type checks for camera platform (#50395) --- .strict-typing | 1 + homeassistant/components/camera/__init__.py | 223 +++++++++++------- homeassistant/components/camera/prefs.py | 39 +-- homeassistant/components/stream/__init__.py | 2 +- .../components/synology_dsm/camera.py | 2 +- mypy.ini | 11 + 6 files changed, 171 insertions(+), 107 deletions(-) diff --git a/.strict-typing b/.strict-typing index a8d9466a01b..f6ed1ba63af 100644 --- a/.strict-typing +++ b/.strict-typing @@ -9,6 +9,7 @@ homeassistant.components.binary_sensor.* homeassistant.components.bond.* homeassistant.components.brother.* homeassistant.components.calendar.* +homeassistant.components.camera.* homeassistant.components.cover.* homeassistant.components.device_automation.* homeassistant.components.elgato.* diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 054ded44d14..b52a36515d8 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -4,13 +4,14 @@ from __future__ import annotations import asyncio import base64 import collections +from collections.abc import Awaitable, Mapping from contextlib import suppress -from datetime import timedelta +from datetime import datetime, timedelta import hashlib import logging import os from random import SystemRandom -from typing import cast, final +from typing import Callable, Final, cast, final from aiohttp import web import async_timeout @@ -28,6 +29,8 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.components.stream import Stream, create_stream from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, OUTPUT_FORMATS +from homeassistant.components.websocket_api import ActiveConnection +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_FILENAME, @@ -36,7 +39,7 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.core import callback +from homeassistant.core import Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -46,6 +49,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, entity_sources from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from .const import ( @@ -59,53 +63,53 @@ from .const import ( ) from .prefs import CameraPreferences -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls _LOGGER = logging.getLogger(__name__) -SERVICE_ENABLE_MOTION = "enable_motion_detection" -SERVICE_DISABLE_MOTION = "disable_motion_detection" -SERVICE_SNAPSHOT = "snapshot" -SERVICE_PLAY_STREAM = "play_stream" +SERVICE_ENABLE_MOTION: Final = "enable_motion_detection" +SERVICE_DISABLE_MOTION: Final = "disable_motion_detection" +SERVICE_SNAPSHOT: Final = "snapshot" +SERVICE_PLAY_STREAM: Final = "play_stream" -SCAN_INTERVAL = timedelta(seconds=30) -ENTITY_ID_FORMAT = DOMAIN + ".{}" +SCAN_INTERVAL: Final = timedelta(seconds=30) +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" -ATTR_FILENAME = "filename" -ATTR_MEDIA_PLAYER = "media_player" -ATTR_FORMAT = "format" +ATTR_FILENAME: Final = "filename" +ATTR_MEDIA_PLAYER: Final = "media_player" +ATTR_FORMAT: Final = "format" -STATE_RECORDING = "recording" -STATE_STREAMING = "streaming" -STATE_IDLE = "idle" +STATE_RECORDING: Final = "recording" +STATE_STREAMING: Final = "streaming" +STATE_IDLE: Final = "idle" # Bitfield of features supported by the camera entity -SUPPORT_ON_OFF = 1 -SUPPORT_STREAM = 2 +SUPPORT_ON_OFF: Final = 1 +SUPPORT_STREAM: Final = 2 -DEFAULT_CONTENT_TYPE = "image/jpeg" -ENTITY_IMAGE_URL = "/api/camera_proxy/{0}?token={1}" +DEFAULT_CONTENT_TYPE: Final = "image/jpeg" +ENTITY_IMAGE_URL: Final = "/api/camera_proxy/{0}?token={1}" -TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) -_RND = SystemRandom() +TOKEN_CHANGE_INTERVAL: Final = timedelta(minutes=5) +_RND: Final = SystemRandom() -MIN_STREAM_INTERVAL = 0.5 # seconds +MIN_STREAM_INTERVAL: Final = 0.5 # seconds -CAMERA_SERVICE_SNAPSHOT = {vol.Required(ATTR_FILENAME): cv.template} +CAMERA_SERVICE_SNAPSHOT: Final = {vol.Required(ATTR_FILENAME): cv.template} -CAMERA_SERVICE_PLAY_STREAM = { +CAMERA_SERVICE_PLAY_STREAM: Final = { vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP), vol.Optional(ATTR_FORMAT, default="hls"): vol.In(OUTPUT_FORMATS), } -CAMERA_SERVICE_RECORD = { +CAMERA_SERVICE_RECORD: Final = { vol.Required(CONF_FILENAME): cv.template, vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), } -WS_TYPE_CAMERA_THUMBNAIL = "camera_thumbnail" -SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( +WS_TYPE_CAMERA_THUMBNAIL: Final = "camera_thumbnail" +SCHEMA_WS_CAMERA_THUMBNAIL: Final = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( { vol.Required("type"): WS_TYPE_CAMERA_THUMBNAIL, vol.Required("entity_id"): cv.entity_id, @@ -122,14 +126,16 @@ class Image: @bind_hass -async def async_request_stream(hass, entity_id, fmt): +async def async_request_stream(hass: HomeAssistant, entity_id: str, fmt: str) -> str: """Request a stream for a camera entity.""" camera = _get_camera_from_entity_id(hass, entity_id) return await _async_stream_endpoint_url(hass, camera, fmt) @bind_hass -async def async_get_image(hass, entity_id, timeout=10): +async def async_get_image( + hass: HomeAssistant, entity_id: str, timeout: int = 10 +) -> Image: """Fetch an image from a camera entity.""" camera = _get_camera_from_entity_id(hass, entity_id) @@ -144,7 +150,7 @@ async def async_get_image(hass, entity_id, timeout=10): @bind_hass -async def async_get_stream_source(hass, entity_id): +async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | None: """Fetch the stream source for a camera entity.""" camera = _get_camera_from_entity_id(hass, entity_id) @@ -152,14 +158,21 @@ async def async_get_stream_source(hass, entity_id): @bind_hass -async def async_get_mjpeg_stream(hass, request, entity_id): +async def async_get_mjpeg_stream( + hass: HomeAssistant, request: web.Request, entity_id: str +) -> web.StreamResponse: """Fetch an mjpeg stream from a camera entity.""" camera = _get_camera_from_entity_id(hass, entity_id) return await camera.handle_async_mjpeg_stream(request) -async def async_get_still_stream(request, image_cb, content_type, interval): +async def async_get_still_stream( + request: web.Request, + image_cb: Callable[[], Awaitable[bytes | None]], + content_type: str, + interval: float, +) -> web.StreamResponse: """Generate an HTTP MJPEG stream from camera images. This method must be run in the event loop. @@ -168,7 +181,7 @@ async def async_get_still_stream(request, image_cb, content_type, interval): response.content_type = CONTENT_TYPE_MULTIPART.format("--frameboundary") await response.prepare(request) - async def write_to_mjpeg_stream(img_bytes): + async def write_to_mjpeg_stream(img_bytes: bytes) -> None: """Write image to stream.""" await response.write( bytes( @@ -202,7 +215,7 @@ async def async_get_still_stream(request, image_cb, content_type, interval): return response -def _get_camera_from_entity_id(hass, entity_id): +def _get_camera_from_entity_id(hass: HomeAssistant, entity_id: str) -> Camera: """Get camera component from entity_id.""" component = hass.data.get(DOMAIN) @@ -217,10 +230,10 @@ def _get_camera_from_entity_id(hass, entity_id): if not camera.is_on: raise HomeAssistantError("Camera is off") - return camera + return cast(Camera, camera) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the camera component.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -241,8 +254,9 @@ async def async_setup(hass, config): await component.async_setup(config) - async def preload_stream(_): + async def preload_stream(_event: Event) -> None: for camera in component.entities: + camera = cast(Camera, camera) camera_prefs = prefs.get(camera.entity_id) if not camera_prefs.preload_stream: continue @@ -256,9 +270,10 @@ async def async_setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, preload_stream) @callback - def update_tokens(time): + def update_tokens(time: datetime) -> None: """Update tokens of the entities.""" for entity in component.entities: + entity = cast(Camera, entity) entity.async_update_token() entity.async_write_ha_state() @@ -287,67 +302,69 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class Camera(Entity): """The base class for camera entities.""" - def __init__(self): + def __init__(self) -> None: """Initialize a camera.""" - self.is_streaming = False - self.stream = None - self.stream_options = {} - self.content_type = DEFAULT_CONTENT_TYPE + self.is_streaming: bool = False + self.stream: Stream | None = None + self.stream_options: dict[str, str] = {} + self.content_type: str = DEFAULT_CONTENT_TYPE self.access_tokens: collections.deque = collections.deque([], 2) self.async_update_token() @property - def should_poll(self): + def should_poll(self) -> bool: """No need to poll cameras.""" return False @property - def entity_picture(self): + def entity_picture(self) -> str: """Return a link to the camera feed as entity picture.""" return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1]) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return 0 @property - def is_recording(self): + def is_recording(self) -> bool: """Return true if the device is recording.""" return False @property - def brand(self): + def brand(self) -> str | None: """Return the camera brand.""" return None @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" - return None + return False @property - def model(self): + def model(self) -> str | None: """Return the camera model.""" return None @property - def frame_interval(self): + def frame_interval(self) -> float: """Return the interval between frames of the mjpeg stream.""" - return 0.5 + return MIN_STREAM_INTERVAL async def create_stream(self) -> Stream | None: """Create a Stream for stream_source.""" @@ -360,25 +377,29 @@ class Camera(Entity): self.stream = create_stream(self.hass, source, options=self.stream_options) return self.stream - async def stream_source(self): + async def stream_source(self) -> str | None: """Return the source of the stream.""" return None - def camera_image(self): + def camera_image(self) -> bytes | None: """Return bytes of camera image.""" raise NotImplementedError() - async def async_camera_image(self): + async def async_camera_image(self) -> bytes | None: """Return bytes of camera image.""" return await self.hass.async_add_executor_job(self.camera_image) - async def handle_async_still_stream(self, request, interval): + async def handle_async_still_stream( + self, request: web.Request, interval: float + ) -> web.StreamResponse: """Generate an HTTP MJPEG stream from camera images.""" return await async_get_still_stream( request, self.async_camera_image, self.content_type, interval ) - async def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream( + self, request: web.Request + ) -> web.StreamResponse: """Serve an HTTP MJPEG stream from the camera. This method can be overridden by camera platforms to proxy @@ -387,7 +408,7 @@ class Camera(Entity): return await self.handle_async_still_stream(request, self.frame_interval) @property - def state(self): + def state(self) -> str: """Return the camera state.""" if self.is_recording: return STATE_RECORDING @@ -396,45 +417,45 @@ class Camera(Entity): return STATE_IDLE @property - def is_on(self): + def is_on(self) -> bool: """Return true if on.""" return True - def turn_off(self): + def turn_off(self) -> None: """Turn off camera.""" raise NotImplementedError() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off camera.""" await self.hass.async_add_executor_job(self.turn_off) - def turn_on(self): + def turn_on(self) -> None: """Turn off camera.""" raise NotImplementedError() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn off camera.""" await self.hass.async_add_executor_job(self.turn_on) - def enable_motion_detection(self): + def enable_motion_detection(self) -> None: """Enable motion detection in the camera.""" raise NotImplementedError() - async def async_enable_motion_detection(self): + async def async_enable_motion_detection(self) -> None: """Call the job and enable motion detection.""" await self.hass.async_add_executor_job(self.enable_motion_detection) - def disable_motion_detection(self): + def disable_motion_detection(self) -> None: """Disable motion detection in camera.""" raise NotImplementedError() - async def async_disable_motion_detection(self): + async def async_disable_motion_detection(self) -> None: """Call the job and disable motion detection.""" await self.hass.async_add_executor_job(self.disable_motion_detection) @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, str | None]: """Return the camera state attributes.""" attrs = {"access_token": self.access_tokens[-1]} @@ -450,7 +471,7 @@ class Camera(Entity): return attrs @callback - def async_update_token(self): + def async_update_token(self) -> None: """Update the used token.""" self.access_tokens.append( hashlib.sha256(_RND.getrandbits(256).to_bytes(32, "little")).hexdigest() @@ -466,7 +487,7 @@ class CameraView(HomeAssistantView): """Initialize a basic camera view.""" self.component = component - async def get(self, request: web.Request, entity_id: str) -> web.Response: + async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse: """Start a GET request.""" camera = self.component.get_entity(entity_id) @@ -489,7 +510,7 @@ class CameraView(HomeAssistantView): return await self.handle(request, camera) - async def handle(self, request, camera): + async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse: """Handle the camera request.""" raise NotImplementedError() @@ -518,7 +539,7 @@ class CameraMjpegStream(CameraView): url = "/api/camera_proxy_stream/{entity_id}" name = "api:camera:stream" - async def handle(self, request: web.Request, camera: Camera) -> web.Response: + async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse: """Serve camera stream, possibly with interval.""" interval_str = request.query.get("interval") if interval_str is None: @@ -535,7 +556,9 @@ class CameraMjpegStream(CameraView): @websocket_api.async_response -async def websocket_camera_thumbnail(hass, connection, msg): +async def websocket_camera_thumbnail( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Handle get camera thumbnail websocket command. Async friendly. @@ -566,7 +589,9 @@ async def websocket_camera_thumbnail(hass, connection, msg): } ) @websocket_api.async_response -async def ws_camera_stream(hass, connection, msg): +async def ws_camera_stream( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Handle get camera stream websocket command. Async friendly. @@ -590,7 +615,9 @@ async def ws_camera_stream(hass, connection, msg): {vol.Required("type"): "camera/get_prefs", vol.Required("entity_id"): cv.entity_id} ) @websocket_api.async_response -async def websocket_get_prefs(hass, connection, msg): +async def websocket_get_prefs( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Handle request for account info.""" prefs = hass.data[DATA_CAMERA_PREFS].get(msg["entity_id"]) connection.send_result(msg["id"], prefs.as_dict()) @@ -604,7 +631,9 @@ async def websocket_get_prefs(hass, connection, msg): } ) @websocket_api.async_response -async def websocket_update_prefs(hass, connection, msg): +async def websocket_update_prefs( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Handle request for account info.""" prefs = hass.data[DATA_CAMERA_PREFS] @@ -617,10 +646,12 @@ async def websocket_update_prefs(hass, connection, msg): connection.send_result(msg["id"], prefs.get(entity_id).as_dict()) -async def async_handle_snapshot_service(camera, service): +async def async_handle_snapshot_service( + camera: Camera, service_call: ServiceCall +) -> None: """Handle snapshot services calls.""" hass = camera.hass - filename = service.data[ATTR_FILENAME] + filename = service_call.data[ATTR_FILENAME] filename.hass = hass snapshot_file = filename.async_render(variables={ATTR_ENTITY_ID: camera}) @@ -632,8 +663,10 @@ async def async_handle_snapshot_service(camera, service): image = await camera.async_camera_image() - def _write_image(to_file, image_data): + def _write_image(to_file: str, image_data: bytes | None) -> None: """Executor helper to write image.""" + if image_data is None: + return if not os.path.exists(os.path.dirname(to_file)): os.makedirs(os.path.dirname(to_file), exist_ok=True) with open(to_file, "wb") as img_file: @@ -645,13 +678,15 @@ async def async_handle_snapshot_service(camera, service): _LOGGER.error("Can't write image to file: %s", err) -async def async_handle_play_stream_service(camera, service_call): +async def async_handle_play_stream_service( + camera: Camera, service_call: ServiceCall +) -> None: """Handle play stream services calls.""" fmt = service_call.data[ATTR_FORMAT] url = await _async_stream_endpoint_url(camera.hass, camera, fmt) hass = camera.hass - data = { + data: Mapping[str, str] = { ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}", ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], } @@ -696,7 +731,9 @@ async def async_handle_play_stream_service(camera, service_call): ) -async def _async_stream_endpoint_url(hass, camera, fmt): +async def _async_stream_endpoint_url( + hass: HomeAssistant, camera: Camera, fmt: str +) -> str: stream = await camera.create_stream() if not stream: raise HomeAssistantError( @@ -712,7 +749,9 @@ async def _async_stream_endpoint_url(hass, camera, fmt): return stream.endpoint_url(fmt) -async def async_handle_record_service(camera, call): +async def async_handle_record_service( + camera: Camera, service_call: ServiceCall +) -> None: """Handle stream recording service calls.""" stream = await camera.create_stream() @@ -720,10 +759,12 @@ async def async_handle_record_service(camera, call): raise HomeAssistantError(f"{camera.entity_id} does not support record service") hass = camera.hass - filename = call.data[CONF_FILENAME] + filename = service_call.data[CONF_FILENAME] filename.hass = hass video_path = filename.async_render(variables={ATTR_ENTITY_ID: camera}) await stream.async_record( - video_path, duration=call.data[CONF_DURATION], lookback=call.data[CONF_LOOKBACK] + video_path, + duration=service_call.data[CONF_DURATION], + lookback=service_call.data[CONF_LOOKBACK], ) diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index ec35a448407..36f3d60d0db 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,27 +1,30 @@ """Preference management for camera component.""" -from homeassistant.helpers.typing import UNDEFINED +from __future__ import annotations + +from typing import Final + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from .const import DOMAIN, PREF_PRELOAD_STREAM -# mypy: allow-untyped-defs, no-check-untyped-defs - -STORAGE_KEY = DOMAIN -STORAGE_VERSION = 1 +STORAGE_KEY: Final = DOMAIN +STORAGE_VERSION: Final = 1 class CameraEntityPreferences: """Handle preferences for camera entity.""" - def __init__(self, prefs): + def __init__(self, prefs: dict[str, bool]) -> None: """Initialize prefs.""" self._prefs = prefs - def as_dict(self): + def as_dict(self) -> dict[str, bool]: """Return dictionary version.""" return self._prefs @property - def preload_stream(self): + def preload_stream(self) -> bool: """Return if stream is loaded on hass start.""" return self._prefs.get(PREF_PRELOAD_STREAM, False) @@ -29,13 +32,13 @@ class CameraEntityPreferences: class CameraPreferences: """Handle camera preferences.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize camera prefs.""" self._hass = hass self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - self._prefs = None + self._prefs: dict[str, dict[str, bool]] | None = None - async def async_initialize(self): + async def async_initialize(self) -> None: """Finish initializing the preferences.""" prefs = await self._store.async_load() @@ -45,9 +48,15 @@ class CameraPreferences: self._prefs = prefs async def async_update( - self, entity_id, *, preload_stream=UNDEFINED, stream_options=UNDEFINED - ): + self, + entity_id: str, + *, + preload_stream: bool | UndefinedType = UNDEFINED, + stream_options: dict[str, str] | UndefinedType = UNDEFINED, + ) -> None: """Update camera preferences.""" + # Prefs already initialized. + assert self._prefs is not None if not self._prefs.get(entity_id): self._prefs[entity_id] = {} @@ -57,6 +66,8 @@ class CameraPreferences: await self._store.async_save(self._prefs) - def get(self, entity_id): + def get(self, entity_id: str) -> CameraEntityPreferences: """Get preferences for an entity.""" + # Prefs are already initialized. + assert self._prefs is not None return CameraEntityPreferences(self._prefs.get(entity_id, {})) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 0d91b63844e..63c8439d43a 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -124,7 +124,7 @@ class Stream: if self.options is None: self.options = {} - def endpoint_url(self, fmt): + def endpoint_url(self, fmt: str) -> str: """Start the stream and returns a url for the output format.""" if fmt not in self._outputs: raise ValueError(f"Stream is not configured for format '{fmt}'") diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 9baca38c16f..a969107bf83 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -78,7 +78,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): }, coordinator, ) - Camera.__init__(self) # type: ignore[no-untyped-call] + Camera.__init__(self) self._camera_id = camera_id @property diff --git a/mypy.ini b/mypy.ini index 65d626cbf8d..97507f0ec84 100644 --- a/mypy.ini +++ b/mypy.ini @@ -110,6 +110,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.camera.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.cover.*] check_untyped_defs = true disallow_incomplete_defs = true From fca56993c65cbaed623146018c50771e791fc5f5 Mon Sep 17 00:00:00 2001 From: Christer Vestermark Date: Mon, 10 May 2021 16:44:08 +0200 Subject: [PATCH 296/852] Add smhi wind gust speed and thunder probability (#50328) * Added some extra attributes Added the extra attributes wind_gust_speed and thunder_probability that were already implemented in the underlaying library (joysoftware / pypi_smhi). Also for the existing extra attribute cloudiness, it is added if "is not None" instead of just "if self.cloudiness" which would make it False (and therefore not available) if cloudiness = 0. * Trying to solve the style issues Removed white spaces and changed order of list as suggested by the tests. * New try to solve the style issues Removed some more white spaces... * Changed dictionary handling as suggested Changed dictionary handling as suggested by MartinHjelmare. * Updated test Updated test_weather.py to include the new attributes wind_gust_speed and thunder_probability. * Added missing imports Added the missing imports ATTR_SMHI_THUNDER_PROBABILITY, ATTR_SMHI_WIND_GUST_SPEED, * Renaming self.thunder to self.thunder_probability and correcting test valuesfor Renamed the new internal attribute thunder to thunder_probability, same as the exposed attribute for improved consistency. Corrected test values according to smhi.json. * Forgot to change to self.thunder_probability in one place. sorry. --- homeassistant/components/smhi/const.py | 2 ++ homeassistant/components/smhi/weather.py | 32 +++++++++++++++++++++--- tests/components/smhi/test_weather.py | 16 +++++++++++- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smhi/const.py b/homeassistant/components/smhi/const.py index 7e88f95f691..c2074416295 100644 --- a/homeassistant/components/smhi/const.py +++ b/homeassistant/components/smhi/const.py @@ -2,6 +2,8 @@ from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN ATTR_SMHI_CLOUDINESS = "cloudiness" +ATTR_SMHI_WIND_GUST_SPEED = "wind_gust_speed" +ATTR_SMHI_THUNDER_PROBABILITY = "thunder_probability" DOMAIN = "smhi" diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 86cdf72e65c..5458abb1786 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -38,7 +38,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.util import Throttle, slugify -from .const import ATTR_SMHI_CLOUDINESS, ENTITY_ID_SENSOR_FORMAT +from .const import ( + ATTR_SMHI_CLOUDINESS, + ATTR_SMHI_THUNDER_PROBABILITY, + ATTR_SMHI_WIND_GUST_SPEED, + ENTITY_ID_SENSOR_FORMAT, +) _LOGGER = logging.getLogger(__name__) @@ -167,6 +172,14 @@ class SmhiWeather(WeatherEntity): return round(self._forecasts[0].wind_speed * 18 / 5) return None + @property + def wind_gust_speed(self) -> float: + """Return the wind gust speed.""" + if self._forecasts is not None: + # Convert from m/s to km/h + return round(self._forecasts[0].wind_gust * 18 / 5) + return None + @property def wind_bearing(self) -> int: """Return the wind bearing.""" @@ -195,6 +208,13 @@ class SmhiWeather(WeatherEntity): return self._forecasts[0].cloudiness return None + @property + def thunder_probability(self) -> int: + """Return the chance of thunder, unit Percent.""" + if self._forecasts is not None: + return self._forecasts[0].thunder + return None + @property def condition(self) -> str: """Return the weather condition.""" @@ -238,5 +258,11 @@ class SmhiWeather(WeatherEntity): @property def extra_state_attributes(self) -> dict: """Return SMHI specific attributes.""" - if self.cloudiness: - return {ATTR_SMHI_CLOUDINESS: self.cloudiness} + extra_attributes = {} + if self.cloudiness is not None: + extra_attributes[ATTR_SMHI_CLOUDINESS] = self.cloudiness + if self.wind_gust_speed is not None: + extra_attributes[ATTR_SMHI_WIND_GUST_SPEED] = self.wind_gust_speed + if self.thunder_probability is not None: + extra_attributes[ATTR_SMHI_THUNDER_PROBABILITY] = self.thunder_probability + return extra_attributes diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 9170f3a9ed0..10a21f74099 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -7,7 +7,11 @@ from unittest.mock import AsyncMock, Mock, patch from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecastException from homeassistant.components.smhi import weather as weather_smhi -from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS +from homeassistant.components.smhi.const import ( + ATTR_SMHI_CLOUDINESS, + ATTR_SMHI_THUNDER_PROBABILITY, + ATTR_SMHI_WIND_GUST_SPEED, +) from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, @@ -57,6 +61,8 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: assert state.state == "sunny" assert state.attributes[ATTR_SMHI_CLOUDINESS] == 50 + assert state.attributes[ATTR_SMHI_THUNDER_PROBABILITY] == 33 + assert state.attributes[ATTR_SMHI_WIND_GUST_SPEED] == 17 assert state.attributes[ATTR_WEATHER_ATTRIBUTION].find("SMHI") >= 0 assert state.attributes[ATTR_WEATHER_HUMIDITY] == 55 assert state.attributes[ATTR_WEATHER_PRESSURE] == 1024 @@ -85,10 +91,12 @@ def test_properties_no_data(hass: HomeAssistant) -> None: assert weather.temperature is None assert weather.humidity is None assert weather.wind_speed is None + assert weather.wind_gust_speed is None assert weather.wind_bearing is None assert weather.visibility is None assert weather.pressure is None assert weather.cloudiness is None + assert weather.thunder_probability is None assert weather.condition is None assert weather.forecast is None assert weather.temperature_unit == TEMP_CELSIUS @@ -104,10 +112,12 @@ def test_properties_unknown_symbol() -> None: data.total_precipitation = 1 data.humidity = 5 data.wind_speed = 10 + data.wind_gust_speed = 17 data.wind_direction = 180 data.horizontal_visibility = 6 data.pressure = 1008 data.cloudiness = 52 + data.thunder_probability = 41 data.symbol = 100 # Faulty symbol data.valid_time = datetime(2018, 1, 1, 0, 1, 2) @@ -117,10 +127,12 @@ def test_properties_unknown_symbol() -> None: data2.total_precipitation = 1 data2.humidity = 5 data2.wind_speed = 10 + data2.wind_gust_speed = 17 data2.wind_direction = 180 data2.horizontal_visibility = 6 data2.pressure = 1008 data2.cloudiness = 52 + data2.thunder_probability = 41 data2.symbol = 100 # Faulty symbol data2.valid_time = datetime(2018, 1, 1, 12, 1, 2) @@ -130,10 +142,12 @@ def test_properties_unknown_symbol() -> None: data3.total_precipitation = 1 data3.humidity = 5 data3.wind_speed = 10 + data3.wind_gust_speed = 17 data3.wind_direction = 180 data3.horizontal_visibility = 6 data3.pressure = 1008 data3.cloudiness = 52 + data3.thunder_probability = 41 data3.symbol = 100 # Faulty symbol data3.valid_time = datetime(2018, 1, 2, 12, 1, 2) From 3fab21ebc7f171a68a2abaecd93d24a16aa60e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 10 May 2021 18:27:09 +0300 Subject: [PATCH 297/852] Skip Huawei LTE device registry setup with no identifiers or connections (#50261) Closes https://github.com/home-assistant/core/issues/50182 --- .../components/huawei_lte/__init__.py | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index f36ae97840b..ebb54ab75c6 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -393,26 +393,29 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b router.subscriptions.clear() # Set up device registry - device_data = {} - sw_version = None - if router.data.get(KEY_DEVICE_INFORMATION): - device_info = router.data[KEY_DEVICE_INFORMATION] - sw_version = device_info.get("SoftwareVersion") - if device_info.get("DeviceName"): - device_data["model"] = device_info["DeviceName"] - if not sw_version and router.data.get(KEY_DEVICE_BASIC_INFORMATION): - sw_version = router.data[KEY_DEVICE_BASIC_INFORMATION].get("SoftwareVersion") - if sw_version: - device_data["sw_version"] = sw_version - device_registry = await dr.async_get_registry(hass) - device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections=router.device_connections, - identifiers=router.device_identifiers, - name=router.device_name, - manufacturer="Huawei", - **device_data, - ) + if router.device_identifiers or router.device_connections: + device_data = {} + sw_version = None + if router.data.get(KEY_DEVICE_INFORMATION): + device_info = router.data[KEY_DEVICE_INFORMATION] + sw_version = device_info.get("SoftwareVersion") + if device_info.get("DeviceName"): + device_data["model"] = device_info["DeviceName"] + if not sw_version and router.data.get(KEY_DEVICE_BASIC_INFORMATION): + sw_version = router.data[KEY_DEVICE_BASIC_INFORMATION].get( + "SoftwareVersion" + ) + if sw_version: + device_data["sw_version"] = sw_version + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections=router.device_connections, + identifiers=router.device_identifiers, + name=router.device_name, + manufacturer="Huawei", + **device_data, + ) # Forward config entry setup to platforms hass.config_entries.async_setup_platforms(config_entry, CONFIG_ENTRY_PLATFORMS) From f8584c3deded54900018c4ecee36fec8552ba73c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 May 2021 08:32:29 -0700 Subject: [PATCH 298/852] Use zoneinfo instead of dateutil (#50387) Co-authored-by: J. Nick Koston --- homeassistant/package_constraints.txt | 2 +- homeassistant/util/dt.py | 34 ++++++++++++++++++++++----- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 13960edfed5..713c2f1a62f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,6 +8,7 @@ async-upnp-client==0.17.0 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.2.3 +backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 @@ -24,7 +25,6 @@ paho-mqtt==1.5.1 pillow==8.1.2 pip>=8.0.3,<20.3 pyroute2==0.5.18 -python-dateutil==2.8.1 python-slugify==4.0.1 pyyaml==5.4.1 requests==2.25.1 diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index a144918713e..c818c955370 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -6,8 +6,12 @@ import datetime as dt import re from typing import Any +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + import ciso8601 -from dateutil import tz from homeassistant.const import MATCH_ALL @@ -43,7 +47,10 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: Async friendly. """ - return tz.gettz(time_zone_str) + try: + return zoneinfo.ZoneInfo(time_zone_str) # type: ignore + except zoneinfo.ZoneInfoNotFoundError: + return None def utcnow() -> dt.datetime: @@ -311,7 +318,7 @@ def find_next_time_expression_time( if result.tzinfo in (None, UTC): return result - if tz.datetime_ambiguous(result): + if _datetime_ambiguous(result): # This happens when we're leaving daylight saving time and local # clocks are rolled back. In this case, we want to trigger # on both the DST and non-DST time. So when "now" is in the DST @@ -320,7 +327,7 @@ def find_next_time_expression_time( if result.fold != fold: result = result.replace(fold=fold) - if not tz.datetime_exists(result): + if not _datetime_exists(result): # This happens when we're entering daylight saving time and local # clocks are rolled forward, thus there are local times that do # not exist. In this case, we want to trigger on the next time @@ -337,11 +344,26 @@ def find_next_time_expression_time( # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST) # we should trigger next on 28.10.2018 2:30 (out of DST), but our # algorithm above would produce 29.10.2018 2:30 (out of DST) - if tz.datetime_ambiguous(now): + if _datetime_ambiguous(now): check_result = find_next_time_expression_time( now + _dst_offset_diff(now), seconds, minutes, hours ) - if tz.datetime_ambiguous(check_result): + if _datetime_ambiguous(check_result): return check_result return result + + +def _datetime_exists(dattim: dt.datetime) -> bool: + """Check if a datetime exists.""" + assert dattim.tzinfo is not None + original_tzinfo = dattim.tzinfo + # Check if we can round trip to UTC + return dattim == dattim.astimezone(UTC).astimezone(original_tzinfo) + + +def _datetime_ambiguous(dattim: dt.datetime) -> bool: + """Check whether a datetime is ambiguous.""" + assert dattim.tzinfo is not None + opposite_fold = dattim.replace(fold=not dattim.fold) + return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() diff --git a/requirements.txt b/requirements.txt index 24e387bed58..39a30934d4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ astral==2.2 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.2.3 +backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 @@ -15,7 +16,6 @@ PyJWT==1.7.1 cryptography==3.3.2 pip>=8.0.3,<20.3 python-slugify==4.0.1 -python-dateutil==2.8.1 pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 diff --git a/setup.py b/setup.py index d5b9d6e68b3..43a8107743e 100755 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ REQUIRES = [ "async_timeout==3.0.1", "attrs==21.2.0", "awesomeversion==21.2.3", + 'backports.zoneinfo;python_version<"3.9"', "bcrypt==3.1.7", "certifi>=2020.12.5", "ciso8601==2.1.3", @@ -47,7 +48,6 @@ REQUIRES = [ "cryptography==3.3.2", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", - "python-dateutil==2.8.1", "pyyaml==5.4.1", "requests==2.25.1", "ruamel.yaml==0.15.100", From 9ae021a284ac168e9c5c938bcdcc656e6e579818 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 10 May 2021 10:56:18 -0500 Subject: [PATCH 299/852] Bump pysonos to 0.0.45 (#50407) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 00ee92ed079..b4c53d96fd6 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.44"], + "requirements": ["pysonos==0.0.45"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 7c1ccf475fb..6c52eac0d3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1741,7 +1741,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.44 +pysonos==0.0.45 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 168fa5c0c61..181f59b9617 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -965,7 +965,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.44 +pysonos==0.0.45 # homeassistant.components.spc pyspcwebgw==0.4.0 From 70b09ed9a1e9f0b4db4502f5f1304d428ad25515 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 10 May 2021 19:28:38 +0200 Subject: [PATCH 300/852] Handle relation between scan_interval and pymodbus timeout in modbus (#50363) * Control scan_interval compared to pymodbus timeout. add MINIMUM_SCAN_INTERVAL=5 seconds and validata with Voluptous. Keep modbus.py 100% coverage. * Please pylint. * Review comments. * pylint. --- homeassistant/components/modbus/__init__.py | 38 +++++++++++++++++++++ homeassistant/components/modbus/const.py | 26 +++++++++++--- homeassistant/components/modbus/modbus.py | 22 ++---------- tests/components/modbus/test_init.py | 13 ++++--- 4 files changed, 70 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 2ebab4635b3..8e0d9220718 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,6 +1,7 @@ """Support for Modbus.""" from __future__ import annotations +import logging from typing import Any import voluptuous as vol @@ -95,10 +96,14 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_STRUCTURE_PREFIX, DEFAULT_TEMP_UNIT, + MINIMUM_SCAN_INTERVAL, MODBUS_DOMAIN as DOMAIN, + PLATFORMS, ) from .modbus import modbus_setup +_LOGGER = logging.getLogger(__name__) + BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) @@ -121,6 +126,38 @@ def number(value: Any) -> int | float: raise vol.Invalid(f"invalid number {value}") from err +def control_scan_interval(config: dict) -> dict: + """Control scan_interval.""" + for hub in config: + minimum_scan_interval = DEFAULT_SCAN_INTERVAL + for component, conf_key in PLATFORMS: + if conf_key not in hub: + continue + + for entry in hub[conf_key]: + scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + if scan_interval < MINIMUM_SCAN_INTERVAL: + _LOGGER.warning( + "%s %s scan_interval(%d) is adjusted to minimum(%d)", + component, + entry.get(CONF_NAME), + scan_interval, + MINIMUM_SCAN_INTERVAL, + ) + scan_interval = MINIMUM_SCAN_INTERVAL + entry[CONF_SCAN_INTERVAL] = scan_interval + minimum_scan_interval = min(scan_interval, minimum_scan_interval) + if CONF_TIMEOUT in hub and hub[CONF_TIMEOUT] > minimum_scan_interval - 1: + _LOGGER.warning( + "Modbus %s timeout(%d) is adjusted(%d) due to scan_interval", + hub.get(CONF_NAME, ""), + hub[CONF_TIMEOUT], + minimum_scan_interval - 1, + ) + hub[CONF_TIMEOUT] = minimum_scan_interval - 1 + return config + + BASE_COMPONENT_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, @@ -279,6 +316,7 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( cv.ensure_list, + control_scan_interval, [ vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA), ], diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index bddaf4b789a..d817108b4d3 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -1,13 +1,22 @@ """Constants used in modbus integration.""" +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.climate.const import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + CONF_BINARY_SENSORS, + CONF_COVERS, + CONF_SENSORS, + CONF_SWITCHES, +) + # configuration names CONF_BAUDRATE = "baudrate" -CONF_BINARY_SENSOR = "binary_sensor" CONF_BYTESIZE = "bytesize" -CONF_CLIMATE = "climate" CONF_CLIMATES = "climates" CONF_COILS = "coils" -CONF_COVER = "cover" CONF_CURRENT_TEMP = "current_temp_register" CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" CONF_DATA_COUNT = "data_count" @@ -24,7 +33,6 @@ CONF_REGISTERS = "registers" CONF_REVERSE_ORDER = "reverse_order" CONF_PRECISION = "precision" CONF_SCALE = "scale" -CONF_SENSOR = "sensor" CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" CONF_STATE_OFF = "state_off" @@ -40,7 +48,6 @@ CONF_SWAP_BYTE = "byte" CONF_SWAP_NONE = "none" CONF_SWAP_WORD = "word" CONF_SWAP_WORD_BYTE = "word_byte" -CONF_SWITCH = "switch" CONF_TARGET_TEMP = "target_temp_register" CONF_VERIFY = "verify" CONF_VERIFY_REGISTER = "verify_register" @@ -74,6 +81,7 @@ SERVICE_WRITE_REGISTER = "write_register" # integration names DEFAULT_HUB = "modbus_hub" +MINIMUM_SCAN_INTERVAL = 5 # seconds DEFAULT_SCAN_INTERVAL = 15 # seconds DEFAULT_SLAVE = 1 DEFAULT_STRUCTURE_PREFIX = ">f" @@ -84,3 +92,11 @@ DEFAULT_STRUCT_FORMAT = { } DEFAULT_TEMP_UNIT = "C" MODBUS_DOMAIN = "modbus" + +PLATFORMS = ( + (CLIMATE_DOMAIN, CONF_CLIMATES), + (COVER_DOMAIN, CONF_COVERS), + (BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS), + (SENSOR_DOMAIN, CONF_SENSORS), + (SWITCH_DOMAIN, CONF_SWITCHES), +) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 3c3eff9b5b0..d9db6cf980c 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -8,15 +8,11 @@ from pymodbus.exceptions import ModbusException from pymodbus.transaction import ModbusRtuFramer from homeassistant.const import ( - CONF_BINARY_SENSORS, - CONF_COVERS, CONF_DELAY, CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, - CONF_SENSORS, - CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, @@ -31,17 +27,12 @@ from .const import ( ATTR_UNIT, ATTR_VALUE, CONF_BAUDRATE, - CONF_BINARY_SENSOR, CONF_BYTESIZE, - CONF_CLIMATE, - CONF_CLIMATES, - CONF_COVER, CONF_PARITY, - CONF_SENSOR, CONF_STOPBITS, - CONF_SWITCH, DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN, + PLATFORMS, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, ) @@ -63,13 +54,7 @@ def modbus_setup( hub_collect[conf_hub[CONF_NAME]].setup(hass) # load platforms - for component, conf_key in ( - (CONF_CLIMATE, CONF_CLIMATES), - (CONF_COVER, CONF_COVERS), - (CONF_BINARY_SENSOR, CONF_BINARY_SENSORS), - (CONF_SENSOR, CONF_SENSORS), - (CONF_SWITCH, CONF_SWITCHES), - ): + for component, conf_key in PLATFORMS: if conf_key in conf_hub: load_platform(hass, component, DOMAIN, conf_hub, config) @@ -140,8 +125,7 @@ class ModbusHub: self._config_port = client_config[CONF_PORT] self._config_timeout = client_config[CONF_TIMEOUT] self._config_delay = client_config[CONF_DELAY] - - Defaults.Timeout = 10 + Defaults.Timeout = client_config[CONF_TIMEOUT] if self._config_type == "serial": # serial configuration self._config_method = client_config[CONF_METHOD] diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 99a8ba467f6..7c0c7453abb 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -343,11 +343,14 @@ async def _read_helper(hass, do_group, do_type, do_return, do_exception, mock_py CONF_HOST: "modbusTestHost", CONF_PORT: 5501, CONF_NAME: TEST_MODBUS_NAME, - do_group: { - CONF_INPUT_TYPE: do_type, - CONF_NAME: TEST_SENSOR_NAME, - CONF_ADDRESS: 51, - }, + do_group: [ + { + CONF_INPUT_TYPE: do_type, + CONF_NAME: TEST_SENSOR_NAME, + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 1, + } + ], } ] } From a404c138fa0926eddaa8b164390be97e48bb224a Mon Sep 17 00:00:00 2001 From: Colin Robbins Date: Mon, 10 May 2021 18:48:00 +0100 Subject: [PATCH 301/852] Support multiple disks in systemmonitor (#50362) * Fix #50158 - add support for multiple disks * Rework as a tuple --- .../components/systemmonitor/sensor.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 6cec3f1d81b..6e23faf606c 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -204,7 +204,7 @@ async def async_setup_platform( ) -> None: """Set up the system monitor sensors.""" entities = [] - sensor_registry: dict[str, SensorData] = {} + sensor_registry: dict[tuple[str, str], SensorData] = {} for resource in config[CONF_RESOURCES]: type_ = resource[CONF_TYPE] @@ -226,7 +226,9 @@ async def async_setup_platform( _LOGGER.warning("Cannot read CPU / processor temperature information") continue - sensor_registry[type_] = SensorData(argument, None, None, None, None) + sensor_registry[(type_, argument)] = SensorData( + argument, None, None, None, None + ) entities.append(SystemMonitorSensor(sensor_registry, type_, argument)) scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) @@ -237,7 +239,7 @@ async def async_setup_platform( async def async_setup_sensor_registry_updates( hass: HomeAssistant, - sensor_registry: dict[str, SensorData], + sensor_registry: dict[tuple[str, str], SensorData], scan_interval: datetime.timedelta, ) -> None: """Update the registry and create polling.""" @@ -246,11 +248,11 @@ async def async_setup_sensor_registry_updates( def _update_sensors() -> None: """Update sensors and store the result in the registry.""" - for type_, data in sensor_registry.items(): + for (type_, argument), data in sensor_registry.items(): try: state, value, update_time = _update(type_, data) except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Error updating sensor: %s", type_) + _LOGGER.exception("Error updating sensor: %s (%s)", type_, argument) data.last_exception = ex else: data.state = state @@ -296,7 +298,7 @@ class SystemMonitorSensor(SensorEntity): def __init__( self, - sensor_registry: dict[str, SensorData], + sensor_registry: dict[tuple[str, str], SensorData], sensor_type: str, argument: str = "", ) -> None: @@ -305,6 +307,7 @@ class SystemMonitorSensor(SensorEntity): self._name: str = f"{self.sensor_type[SENSOR_TYPE_NAME]} {argument}".rstrip() self._unique_id: str = slugify(f"{sensor_type}_{argument}") self._sensor_registry = sensor_registry + self._argument: str = argument @property def name(self) -> str: @@ -354,7 +357,7 @@ class SystemMonitorSensor(SensorEntity): @property def data(self) -> SensorData: """Return registry entry for the data.""" - return self._sensor_registry[self._type] + return self._sensor_registry[(self._type, self._argument)] async def async_added_to_hass(self) -> None: """When entity is added to hass.""" From 24a46d91d30fbd6f24f42349e9adf5ba71d6f1b6 Mon Sep 17 00:00:00 2001 From: "Julien \"_FrnchFrgg_\" Rivaud" Date: Mon, 10 May 2021 19:49:08 +0200 Subject: [PATCH 302/852] Fix amcrest detection of sensor reset (#50249) Co-authored-by: Pascal Vizeli --- homeassistant/components/amcrest/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index f6ddc210415..8d274f12044 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -201,11 +201,15 @@ def _monitor_events(hass, name, api, event_codes): while True: api.available_flag.wait() try: - for code, start in api.event_actions("All", retries=5): - event_data = {"camera": name, "event": code, "payload": start} + for code, payload in api.event_actions("All", retries=5): + event_data = {"camera": name, "event": code, "payload": payload} hass.bus.fire("amcrest", event_data) if code in event_codes: signal = service_signal(SERVICE_EVENT, name, code) + start = any( + str(key).lower() == "action" and str(val).lower() == "start" + for key, val in payload.items() + ) _LOGGER.debug("Sending signal: '%s': %s", signal, start) dispatcher_send(hass, signal, start) except AmcrestError as error: From baacf1b787abff8d1df8c1bffa5373aedef4ba20 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Mon, 10 May 2021 20:35:32 +0200 Subject: [PATCH 303/852] Bumps aioasuswrt to 1.3.4 (#50414) --- homeassistant/components/asuswrt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index fef0c7a14cb..b66c3bb5db9 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -3,7 +3,7 @@ "name": "ASUSWRT", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/asuswrt", - "requirements": ["aioasuswrt==1.3.1"], + "requirements": ["aioasuswrt==1.3.4"], "codeowners": ["@kennedyshead", "@ollo69"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 6c52eac0d3b..d211720f5cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -138,7 +138,7 @@ aio_georss_gdacs==0.4 aioambient==1.2.4 # homeassistant.components.asuswrt -aioasuswrt==1.3.1 +aioasuswrt==1.3.4 # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 181f59b9617..ccfb5b9db3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ aio_georss_gdacs==0.4 aioambient==1.2.4 # homeassistant.components.asuswrt -aioasuswrt==1.3.1 +aioasuswrt==1.3.4 # homeassistant.components.azure_devops aioazuredevops==1.3.5 From 84984b0223dc7c096e2c75aa1f284f761cf3f295 Mon Sep 17 00:00:00 2001 From: Khole Date: Mon, 10 May 2021 19:38:35 +0100 Subject: [PATCH 304/852] Bump Pyhiveapi (#50368) --- homeassistant/components/hive/manifest.json | 11 ++++++++--- homeassistant/components/hive/switch.py | 1 + homeassistant/components/hive/water_heater.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index e09e06c8676..5f23eef642b 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,12 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.4.1"], - "codeowners": ["@Rendili", "@KJonline"], + "requirements": [ + "pyhiveapi==0.4.2" + ], + "codeowners": [ + "@Rendili", + "@KJonline" + ], "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 1151fcf346b..7ad81a25f0e 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -84,3 +84,4 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.switch.getSwitch(self.device) + self.attributes.update(self.device.get("attributes", {})) diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index cca919b81d6..b9377a378c3 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -139,9 +139,9 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): async def async_hot_water_boost(self, time_period, on_off): """Handle the service call.""" if on_off == "on": - await self.hive.hotwater.turnBoostOn(self.device, time_period) + await self.hive.hotwater.setBoostOn(self.device, time_period) elif on_off == "off": - await self.hive.hotwater.turnBoostOff(self.device) + await self.hive.hotwater.setBoostOff(self.device) async def async_update(self): """Update all Node data from Hive.""" diff --git a/requirements_all.txt b/requirements_all.txt index d211720f5cf..04531f6f0e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1446,7 +1446,7 @@ pyheos==0.7.2 pyhik==0.2.8 # homeassistant.components.hive -pyhiveapi==0.4.1 +pyhiveapi==0.4.2 # homeassistant.components.homematic pyhomematic==0.1.72 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccfb5b9db3b..f91b249c477 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -790,7 +790,7 @@ pyhaversion==21.3.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.4.1 +pyhiveapi==0.4.2 # homeassistant.components.homematic pyhomematic==0.1.72 From 12342437e2f2e9293104793eb2e20f44b6275411 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 10 May 2021 20:40:44 +0200 Subject: [PATCH 305/852] Fix battery attribute (#50405) --- homeassistant/components/netatmo/climate.py | 55 ++------------------- tests/components/netatmo/test_climate.py | 31 +----------- 2 files changed, 5 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1fb49d0c90d..763ecdbc8ef 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -506,7 +506,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._target_temperature = roomstatus["target_temperature"] self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] self._hvac_mode = HVAC_MAP_NETATMO[self._preset] - self._battery_level = roomstatus.get("battery_level") + self._battery_level = roomstatus.get("battery_state") self._connected = True self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] @@ -546,7 +546,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): roomstatus["heating_status"] = self._boilerstatus batterylevel = self._home_status.thermostats[ roomstatus["module_id"] - ].get("battery_level") + ].get("battery_state") elif roomstatus["module_type"] == NA_VALVE: roomstatus["heating_power_request"] = self._room_status[ "heating_power_request" @@ -557,16 +557,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._boilerstatus and roomstatus["heating_status"] ) batterylevel = self._home_status.valves[roomstatus["module_id"]].get( - "battery_level" + "battery_state" ) if batterylevel: - batterypct = interpolate(batterylevel, roomstatus["module_type"]) - if ( - not roomstatus.get("battery_level") - or batterypct < roomstatus["battery_level"] - ): - roomstatus["battery_level"] = batterypct + roomstatus["battery_state"] = batterylevel return roomstatus @@ -602,48 +597,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return {**super().device_info, "suggested_area": self._room_data["name"]} -def interpolate(batterylevel: int, module_type: str) -> int: - """Interpolate battery level depending on device type.""" - na_battery_levels = { - NA_THERM: { - "full": 4100, - "high": 3600, - "medium": 3300, - "low": 3000, - "empty": 2800, - }, - NA_VALVE: { - "full": 3200, - "high": 2700, - "medium": 2400, - "low": 2200, - "empty": 2200, - }, - } - - levels = sorted(na_battery_levels[module_type].values()) - steps = [20, 50, 80, 100] - - na_battery_level = na_battery_levels[module_type] - if batterylevel >= na_battery_level["full"]: - return 100 - if batterylevel >= na_battery_level["high"]: - i = 3 - elif batterylevel >= na_battery_level["medium"]: - i = 2 - elif batterylevel >= na_battery_level["low"]: - i = 1 - else: - return 0 - - pct = steps[i - 1] + ( - (steps[i] - steps[i - 1]) - * (batterylevel - levels[i]) - / (levels[i + 1] - levels[i]) - ) - return int(pct) - - def get_all_home_ids(home_data: pyatmo.HomeData) -> list[str]: """Get all the home ids returned by NetAtmo API.""" if home_data is None: diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index ecec2871df8..a3cad1e6d81 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -1,8 +1,6 @@ """The tests for the Netatmo climate platform.""" from unittest.mock import Mock, patch -import pytest - from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, @@ -21,12 +19,7 @@ from homeassistant.components.climate.const import ( PRESET_BOOST, ) from homeassistant.components.netatmo import climate -from homeassistant.components.netatmo.climate import ( - NA_THERM, - NA_VALVE, - PRESET_FROST_GUARD, - PRESET_SCHEDULE, -) +from homeassistant.components.netatmo.climate import PRESET_FROST_GUARD, PRESET_SCHEDULE from homeassistant.components.netatmo.const import ( ATTR_SCHEDULE_NAME, SERVICE_SET_SCHEDULE, @@ -653,28 +646,6 @@ async def test_valves_service_turn_on(hass, climate_entry): assert hass.states.get(climate_entity_entrada).state == "auto" -@pytest.mark.parametrize( - "batterylevel, module_type, expected", - [ - (4101, NA_THERM, 100), - (3601, NA_THERM, 80), - (3450, NA_THERM, 65), - (3301, NA_THERM, 50), - (3001, NA_THERM, 20), - (2799, NA_THERM, 0), - (3201, NA_VALVE, 100), - (2701, NA_VALVE, 80), - (2550, NA_VALVE, 65), - (2401, NA_VALVE, 50), - (2201, NA_VALVE, 20), - (2001, NA_VALVE, 0), - ], -) -async def test_interpolate(batterylevel, module_type, expected): - """Test interpolation of battery levels depending on device type.""" - assert climate.interpolate(batterylevel, module_type) == expected - - async def test_get_all_home_ids(): """Test extracting all home ids returned by NetAtmo API.""" # Test with backend returning no data From 85f758380a1f6766f4e268b67724a417dd978399 Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Mon, 10 May 2021 22:46:50 +0200 Subject: [PATCH 306/852] Add Growatt Server Config flow (#41303) * Growatt Server Config flow * Use reference strings Co-authored-by: SNoof85 * Remove configuration.yaml import logic * Removed import test * Re-added PLATFORM_SCHEMA validation * Import yaml from old yaml configuration * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Feedback * Use Executor for IO only * Fix imports * update requirements * Fix flake8 * Run every section of fetching devices in single executor * Config flow feedback * Clean up * Fix plan step * Fix config flow test * Remove duplicate test * Test import step * Test already configured entry * Clean up tests * Add asserts * Mock out entry setup * Add warning if set up via yaml Co-authored-by: SNoof85 Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + .../components/growatt_server/__init__.py | 18 ++ .../components/growatt_server/config_flow.py | 78 ++++++++ .../components/growatt_server/const.py | 10 + .../components/growatt_server/manifest.json | 1 + .../components/growatt_server/sensor.py | 47 +++-- .../components/growatt_server/strings.json | 27 +++ .../growatt_server/translations/en.json | 27 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/growatt_server/__init__.py | 1 + .../growatt_server/test_config_flow.py | 188 ++++++++++++++++++ 12 files changed, 390 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/growatt_server/config_flow.py create mode 100644 homeassistant/components/growatt_server/const.py create mode 100644 homeassistant/components/growatt_server/strings.json create mode 100644 homeassistant/components/growatt_server/translations/en.json create mode 100644 tests/components/growatt_server/__init__.py create mode 100644 tests/components/growatt_server/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 923642733d4..02cd35bcd60 100644 --- a/.coveragerc +++ b/.coveragerc @@ -374,6 +374,7 @@ omit = homeassistant/components/greenwave/light.py homeassistant/components/group/notify.py homeassistant/components/growatt_server/sensor.py + homeassistant/components/growatt_server/__init__.py homeassistant/components/gstreamer/media_player.py homeassistant/components/gtfs/sensor.py homeassistant/components/guardian/__init__.py diff --git a/homeassistant/components/growatt_server/__init__.py b/homeassistant/components/growatt_server/__init__.py index 14205e8d9ba..8fcc7c3f34d 100644 --- a/homeassistant/components/growatt_server/__init__.py +++ b/homeassistant/components/growatt_server/__init__.py @@ -1 +1,19 @@ """The Growatt server PV inverter sensor integration.""" +from homeassistant import config_entries +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS + + +async def async_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Load the saved entities.""" + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py new file mode 100644 index 00000000000..300c96746e7 --- /dev/null +++ b/homeassistant/components/growatt_server/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow for growatt server integration.""" +import growattServer +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import CONF_PLANT_ID, DOMAIN + + +class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow class.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialise growatt server flow.""" + self.api = growattServer.GrowattApi() + self.user_id = None + self.data = {} + + @callback + def _async_show_user_form(self, errors=None): + """Show the form to the user.""" + data_schema = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + if not user_input: + return self._async_show_user_form() + + login_response = await self.hass.async_add_executor_job( + self.api.login, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) + + if not login_response["success"] and login_response["errCode"] == "102": + return self._async_show_user_form({"base": "invalid_auth"}) + self.user_id = login_response["userId"] + + self.data = user_input + return await self.async_step_plant() + + async def async_step_plant(self, user_input=None): + """Handle adding a "plant" to Home Assistant.""" + plant_info = await self.hass.async_add_executor_job( + self.api.plant_list, self.user_id + ) + + if not plant_info["data"]: + return self.async_abort(reason="no_plants") + + plants = {plant["plantId"]: plant["plantName"] for plant in plant_info["data"]} + + if user_input is None and len(plant_info["data"]) > 1: + data_schema = vol.Schema({vol.Required(CONF_PLANT_ID): vol.In(plants)}) + + return self.async_show_form(step_id="plant", data_schema=data_schema) + + if user_input is None and len(plant_info["data"]) == 1: + user_input = {CONF_PLANT_ID: plant_info["data"][0]["plantId"]} + + user_input[CONF_NAME] = plants[user_input[CONF_PLANT_ID]] + await self.async_set_unique_id(user_input[CONF_PLANT_ID]) + self._abort_if_unique_id_configured() + self.data.update(user_input) + return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) + + async def async_step_import(self, import_data): + """Migrate old yaml config to config flow.""" + return await self.async_step_user(import_data) diff --git a/homeassistant/components/growatt_server/const.py b/homeassistant/components/growatt_server/const.py new file mode 100644 index 00000000000..4dc09988e6f --- /dev/null +++ b/homeassistant/components/growatt_server/const.py @@ -0,0 +1,10 @@ +"""Define constants for the Growatt Server component.""" +CONF_PLANT_ID = "plant_id" + +DEFAULT_PLANT_ID = "0" + +DEFAULT_NAME = "Growatt" + +DOMAIN = "growatt_server" + +PLATFORMS = ["sensor"] diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index f3376ba4ae2..94fc293b8d7 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -1,6 +1,7 @@ { "domain": "growatt_server", "name": "Growatt", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", "requirements": ["growattServer==1.0.0"], "codeowners": ["@indykoning", "@muppet3000"], diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 6464dee6729..0ccdc9425f6 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -8,6 +8,7 @@ import growattServer import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -32,11 +33,10 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, dt +from .const import CONF_PLANT_ID, DEFAULT_NAME, DEFAULT_PLANT_ID, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_PLANT_ID = "plant_id" -DEFAULT_PLANT_ID = "0" -DEFAULT_NAME = "Growatt" SCAN_INTERVAL = datetime.timedelta(minutes=1) # Sensor type order is: Sensor name, Unit of measurement, api data name, additional options @@ -558,17 +558,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Growatt sensor.""" - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - plant_id = config[CONF_PLANT_ID] - name = config[CONF_NAME] +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up growatt server from yaml.""" + if not hass.config_entries.async_entries(DOMAIN): + _LOGGER.warning( + "Loading Growatt via platform setup is deprecated." + "Please remove it from your configuration" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) - api = growattServer.GrowattApi() + +def get_device_list(api, config): + """Retrieve the device list for the selected plant.""" + plant_id = config[CONF_PLANT_ID] # Log in to api and fetch first plant if no plant id is defined. - login_response = api.login(username, password) + login_response = api.login(config[CONF_USERNAME], config[CONF_PASSWORD]) if not login_response["success"] and login_response["errCode"] == "102": _LOGGER.error("Username or Password may be incorrect!") return @@ -579,6 +588,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Get a list of devices for specified plant to add sensors for. devices = api.device_list(plant_id) + return [devices, plant_id] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Growatt sensor.""" + config = config_entry.data + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + name = config[CONF_NAME] + + api = growattServer.GrowattApi() + + devices, plant_id = await hass.async_add_executor_job(get_device_list, api, config) + entities = [] probe = GrowattData(api, username, password, plant_id, "total") for sensor in TOTAL_SENSOR_TYPES: @@ -616,7 +639,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) ) - add_entities(entities, True) + async_add_entities(entities, True) class GrowattInverter(SensorEntity): diff --git a/homeassistant/components/growatt_server/strings.json b/homeassistant/components/growatt_server/strings.json new file mode 100644 index 00000000000..903ba400a6f --- /dev/null +++ b/homeassistant/components/growatt_server/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "No plants have been found on this account" + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "step": { + "plant": { + "data": { + "plant_id": "Plant" + }, + "title": "Select your plant" + }, + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "password": "[%key:common::config_flow::data::name%]", + "username": "[%key:common::config_flow::data::username%]" + }, + "title": "Enter your Growatt information" + } + } + }, + "title": "Growatt Server" +} diff --git a/homeassistant/components/growatt_server/translations/en.json b/homeassistant/components/growatt_server/translations/en.json new file mode 100644 index 00000000000..11bb6a64b32 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "No plants have been found on this account" + }, + "error": { + "invalid_auth": "Invalid authentication" + }, + "step": { + "plant": { + "data": { + "plant_id": "Plant" + }, + "title": "Select your plant" + }, + "user": { + "data": { + "name": "Name", + "password": "Password", + "username": "Username" + }, + "title": "Enter your Growatt information" + } + } + }, + "title": "Growatt Server" +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bb346ff5b0f..fe62725cc82 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -92,6 +92,7 @@ FLOWS = [ "google_travel_time", "gpslogger", "gree", + "growatt_server", "guardian", "habitica", "hangouts", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f91b249c477..c7f63ab3692 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -386,6 +386,9 @@ googlemaps==2.5.1 # homeassistant.components.gree greeclimate==0.11.4 +# homeassistant.components.growatt_server +growattServer==1.0.0 + # homeassistant.components.profiler guppy3==3.1.0 diff --git a/tests/components/growatt_server/__init__.py b/tests/components/growatt_server/__init__.py new file mode 100644 index 00000000000..999e1782a9f --- /dev/null +++ b/tests/components/growatt_server/__init__.py @@ -0,0 +1 @@ +"""Tests for the growatt_server component.""" diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py new file mode 100644 index 00000000000..cc11c2f8bf2 --- /dev/null +++ b/tests/components/growatt_server/test_config_flow.py @@ -0,0 +1,188 @@ +"""Tests for the Growatt server config flow.""" +from copy import deepcopy +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.growatt_server.const import CONF_PLANT_ID, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +FIXTURE_USER_INPUT = {CONF_USERNAME: "username", CONF_PASSWORD: "password"} + +GROWATT_PLANT_LIST_RESPONSE = { + "data": [ + { + "plantMoneyText": "474.9 (€)", + "plantName": "Plant name", + "plantId": "123456", + "isHaveStorage": "false", + "todayEnergy": "2.6 kWh", + "totalEnergy": "2.37 MWh", + "currentPower": "628.8 W", + } + ], + "totalData": { + "currentPowerSum": "628.8 W", + "CO2Sum": "2.37 KT", + "isHaveStorage": "false", + "eTotalMoneyText": "474.9 (€)", + "todayEnergySum": "2.6 kWh", + "totalEnergySum": "2.37 MWh", + }, + "success": True, +} +GROWATT_LOGIN_RESPONSE = {"userId": 123456, "userLevel": 1, "success": True} + + +async def test_show_authenticate_form(hass): + """Test that the setup form is served.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_incorrect_username(hass): + """Test that it shows the appropriate error when an incorrect username is entered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "growattServer.GrowattApi.login", + return_value={"errCode": "102", "success": False}, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_no_plants_on_account(hass): + """Test registering an integration and finishing flow with an entered plant_id.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + user_input = FIXTURE_USER_INPUT.copy() + plant_list = deepcopy(GROWATT_PLANT_LIST_RESPONSE) + plant_list["data"] = [] + + with patch( + "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE + ), patch("growattServer.GrowattApi.plant_list", return_value=plant_list): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + assert result["type"] == "abort" + assert result["reason"] == "no_plants" + + +async def test_multiple_plant_ids(hass): + """Test registering an integration and finishing flow with an entered plant_id.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + user_input = FIXTURE_USER_INPUT.copy() + plant_list = deepcopy(GROWATT_PLANT_LIST_RESPONSE) + plant_list["data"].append(plant_list["data"][0]) + + with patch( + "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE + ), patch("growattServer.GrowattApi.plant_list", return_value=plant_list), patch( + "homeassistant.components.growatt_server.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "plant" + + user_input = {CONF_PLANT_ID: "123456"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] + assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + assert result["data"][CONF_PLANT_ID] == "123456" + + +async def test_one_plant_on_account(hass): + """Test registering an integration and finishing flow with an entered plant_id.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + user_input = FIXTURE_USER_INPUT.copy() + + with patch( + "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE + ), patch( + "growattServer.GrowattApi.plant_list", + return_value=GROWATT_PLANT_LIST_RESPONSE, + ), patch( + "homeassistant.components.growatt_server.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] + assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + assert result["data"][CONF_PLANT_ID] == "123456" + + +async def test_import_one_plant(hass): + """Test import step with a single plant.""" + import_data = FIXTURE_USER_INPUT.copy() + + with patch( + "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE + ), patch( + "growattServer.GrowattApi.plant_list", + return_value=GROWATT_PLANT_LIST_RESPONSE, + ), patch( + "homeassistant.components.growatt_server.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=import_data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] + assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + assert result["data"][CONF_PLANT_ID] == "123456" + + +async def test_existing_plant_configured(hass): + """Test entering an existing plant_id.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id="123456") + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + user_input = FIXTURE_USER_INPUT.copy() + + with patch( + "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE + ), patch( + "growattServer.GrowattApi.plant_list", + return_value=GROWATT_PLANT_LIST_RESPONSE, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" From ce15f286420c7a1be53eaeb20c6a8c94e8d1b462 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Mon, 10 May 2021 22:30:47 +0100 Subject: [PATCH 307/852] Add missing type hints in http component (#50411) --- homeassistant/components/http/__init__.py | 176 ++++++++++-------- homeassistant/components/http/auth.py | 35 ++-- homeassistant/components/http/ban.py | 44 +++-- homeassistant/components/http/const.py | 10 +- homeassistant/components/http/cors.py | 38 ++-- homeassistant/components/http/forwarded.py | 26 ++- .../components/http/request_context.py | 16 +- .../components/http/security_filter.py | 18 +- homeassistant/components/http/static.py | 19 +- homeassistant/components/http/view.py | 10 +- homeassistant/components/http/web_runner.py | 9 +- homeassistant/const.py | 2 +- .../helpers/config_entry_oauth2_flow.py | 2 +- 13 files changed, 245 insertions(+), 160 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index ed3a9510f5a..8bd20e31628 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -6,16 +6,18 @@ from ipaddress import ip_network import logging import os import ssl -from typing import Any, Optional, cast +from typing import Any, Final, Optional, TypedDict, cast from aiohttp import web -from aiohttp.web_exceptions import HTTPMovedPermanently +from aiohttp.typedefs import StrOrURL +from aiohttp.web_exceptions import HTTPMovedPermanently, HTTPRedirection import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.setup import async_start_setup, async_when_setup_or_start import homeassistant.util as hass_util @@ -29,44 +31,42 @@ from .forwarded import async_setup_forwarded from .request_context import setup_request_context from .security_filter import setup_security_filter from .static import CACHE_HEADERS, CachingStaticResource -from .view import HomeAssistantView # noqa: F401 +from .view import HomeAssistantView from .web_runner import HomeAssistantTCPSite -# mypy: allow-untyped-defs, no-check-untyped-defs +DOMAIN: Final = "http" -DOMAIN = "http" +CONF_SERVER_HOST: Final = "server_host" +CONF_SERVER_PORT: Final = "server_port" +CONF_BASE_URL: Final = "base_url" +CONF_SSL_CERTIFICATE: Final = "ssl_certificate" +CONF_SSL_PEER_CERTIFICATE: Final = "ssl_peer_certificate" +CONF_SSL_KEY: Final = "ssl_key" +CONF_CORS_ORIGINS: Final = "cors_allowed_origins" +CONF_USE_X_FORWARDED_FOR: Final = "use_x_forwarded_for" +CONF_TRUSTED_PROXIES: Final = "trusted_proxies" +CONF_LOGIN_ATTEMPTS_THRESHOLD: Final = "login_attempts_threshold" +CONF_IP_BAN_ENABLED: Final = "ip_ban_enabled" +CONF_SSL_PROFILE: Final = "ssl_profile" -CONF_SERVER_HOST = "server_host" -CONF_SERVER_PORT = "server_port" -CONF_BASE_URL = "base_url" -CONF_SSL_CERTIFICATE = "ssl_certificate" -CONF_SSL_PEER_CERTIFICATE = "ssl_peer_certificate" -CONF_SSL_KEY = "ssl_key" -CONF_CORS_ORIGINS = "cors_allowed_origins" -CONF_USE_X_FORWARDED_FOR = "use_x_forwarded_for" -CONF_TRUSTED_PROXIES = "trusted_proxies" -CONF_LOGIN_ATTEMPTS_THRESHOLD = "login_attempts_threshold" -CONF_IP_BAN_ENABLED = "ip_ban_enabled" -CONF_SSL_PROFILE = "ssl_profile" +SSL_MODERN: Final = "modern" +SSL_INTERMEDIATE: Final = "intermediate" -SSL_MODERN = "modern" -SSL_INTERMEDIATE = "intermediate" +_LOGGER: Final = logging.getLogger(__name__) -_LOGGER = logging.getLogger(__name__) - -DEFAULT_DEVELOPMENT = "0" +DEFAULT_DEVELOPMENT: Final = "0" # Cast to be able to load custom cards. # My to be able to check url and version info. -DEFAULT_CORS = ["https://cast.home-assistant.io"] -NO_LOGIN_ATTEMPT_THRESHOLD = -1 +DEFAULT_CORS: Final[list[str]] = ["https://cast.home-assistant.io"] +NO_LOGIN_ATTEMPT_THRESHOLD: Final = -1 -MAX_CLIENT_SIZE: int = 1024 ** 2 * 16 +MAX_CLIENT_SIZE: Final = 1024 ** 2 * 16 -STORAGE_KEY = DOMAIN -STORAGE_VERSION = 1 -SAVE_DELAY = 180 +STORAGE_KEY: Final = DOMAIN +STORAGE_VERSION: Final = 1 +SAVE_DELAY: Final = 180 -HTTP_SCHEMA = vol.All( +HTTP_SCHEMA: Final = vol.All( cv.deprecated(CONF_BASE_URL), vol.Schema( { @@ -96,7 +96,24 @@ HTTP_SCHEMA = vol.All( ), ) -CONFIG_SCHEMA = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA) + + +class ConfData(TypedDict, total=False): + """Typed dict for config data.""" + + server_host: list[str] + server_port: int + base_url: str + ssl_certificate: str + ssl_peer_certificate: str + ssl_key: str + cors_allowed_origins: list[str] + use_x_forwarded_for: bool + trusted_proxies: list[str] + login_attempts_threshold: int + ip_ban_enabled: bool + ssl_profile: str @bind_hass @@ -113,8 +130,8 @@ class ApiConfig: self, local_ip: str, host: str, - port: int | None = SERVER_PORT, - use_ssl: bool = False, + port: int, + use_ssl: bool, ) -> None: """Initialize a new API config object.""" self.local_ip = local_ip @@ -123,12 +140,12 @@ class ApiConfig: self.use_ssl = use_ssl -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HTTP API and debug interface.""" - conf = config.get(DOMAIN) + conf: ConfData | None = config.get(DOMAIN) if conf is None: - conf = HTTP_SCHEMA({}) + conf = cast(ConfData, HTTP_SCHEMA({})) server_host = conf.get(CONF_SERVER_HOST) server_port = conf[CONF_SERVER_PORT] @@ -137,7 +154,7 @@ async def async_setup(hass, config): ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf[CONF_CORS_ORIGINS] use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) - trusted_proxies = conf.get(CONF_TRUSTED_PROXIES, []) + trusted_proxies = conf.get(CONF_TRUSTED_PROXIES) or [] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] ssl_profile = conf[CONF_SSL_PROFILE] @@ -165,6 +182,8 @@ async def async_setup(hass, config): """Start the server.""" with async_start_setup(hass, ["http"]): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) + # We already checked it's not None. + assert conf is not None await start_http_server_and_save_config(hass, dict(conf), server) async_when_setup_or_start(hass, "frontend", start_server) @@ -190,19 +209,19 @@ class HomeAssistantHTTP: def __init__( self, - hass, - ssl_certificate, - ssl_peer_certificate, - ssl_key, - server_host, - server_port, - cors_origins, - use_x_forwarded_for, - trusted_proxies, - login_threshold, - is_ban_enabled, - ssl_profile, - ): + hass: HomeAssistant, + ssl_certificate: str | None, + ssl_peer_certificate: str | None, + ssl_key: str | None, + server_host: list[str] | None, + server_port: int, + cors_origins: list[str], + use_x_forwarded_for: bool, + trusted_proxies: list[str], + login_threshold: int, + is_ban_enabled: bool, + ssl_profile: str, + ) -> None: """Initialize the HTTP Home Assistant server.""" app = self.app = web.Application( middlewares=[], client_max_size=MAX_CLIENT_SIZE @@ -237,10 +256,10 @@ class HomeAssistantHTTP: self.is_ban_enabled = is_ban_enabled self.ssl_profile = ssl_profile self._handler = None - self.runner = None - self.site = None + self.runner: web.AppRunner | None = None + self.site: HomeAssistantTCPSite | None = None - def register_view(self, view): + def register_view(self, view: HomeAssistantView) -> None: """Register a view with the WSGI server. The view argument must be a class that inherits from HomeAssistantView. @@ -261,7 +280,13 @@ class HomeAssistantHTTP: view.register(self.app, self.app.router) - def register_redirect(self, url, redirect_to, *, redirect_exc=HTTPMovedPermanently): + def register_redirect( + self, + url: str, + redirect_to: StrOrURL, + *, + redirect_exc: type[HTTPRedirection] = HTTPMovedPermanently, + ) -> None: """Register a redirect with the server. If given this must be either a string or callable. In case of a @@ -271,38 +296,39 @@ class HomeAssistantHTTP: rule syntax. """ - async def redirect(request): + async def redirect(request: web.Request) -> web.StreamResponse: """Redirect to location.""" - raise redirect_exc(redirect_to) + # Should be instance of aiohttp.web_exceptions._HTTPMove. + raise redirect_exc(redirect_to) # type: ignore[arg-type,misc] self.app.router.add_route("GET", url, redirect) - def register_static_path(self, url_path, path, cache_headers=True): + def register_static_path( + self, url_path: str, path: str, cache_headers: bool = True + ) -> web.FileResponse | None: """Register a folder or file to serve as a static path.""" if os.path.isdir(path): if cache_headers: - resource = CachingStaticResource + resource: type[ + CachingStaticResource | web.StaticResource + ] = CachingStaticResource else: resource = web.StaticResource self.app.router.register_resource(resource(url_path, path)) - return + return None - if cache_headers: - - async def serve_file(request): - """Serve file from disk.""" + async def serve_file(request: web.Request) -> web.FileResponse: + """Serve file from disk.""" + if cache_headers: return web.FileResponse(path, headers=CACHE_HEADERS) - - else: - - async def serve_file(request): - """Serve file from disk.""" - return web.FileResponse(path) + return web.FileResponse(path) self.app.router.add_route("GET", url_path, serve_file) + return None - async def start(self): + async def start(self) -> None: """Start the aiohttp server.""" + context: ssl.SSLContext | None if self.ssl_certificate: try: if self.ssl_profile == SSL_INTERMEDIATE: @@ -334,7 +360,7 @@ class HomeAssistantHTTP: # This will now raise a RunTimeError. # To work around this we now prevent the router from getting frozen # pylint: disable=protected-access - self.app._router.freeze = lambda: None + self.app._router.freeze = lambda: None # type: ignore[assignment] self.runner = web.AppRunner(self.app) await self.runner.setup() @@ -351,17 +377,19 @@ class HomeAssistantHTTP: _LOGGER.info("Now listening on port %d", self.server_port) - async def stop(self): + async def stop(self) -> None: """Stop the aiohttp server.""" - await self.site.stop() - await self.runner.cleanup() + if self.site is not None: + await self.site.stop() + if self.runner is not None: + await self.runner.cleanup() async def start_http_server_and_save_config( hass: HomeAssistant, conf: dict, server: HomeAssistantHTTP ) -> None: """Startup the http server and save the config.""" - await server.start() # type: ignore + await server.start() # If we are set up successful, we store the HTTP settings for safe mode. store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 38275819483..7004b279bd0 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,28 +1,33 @@ """Authentication for HTTP component.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from datetime import timedelta import logging import secrets +from typing import Final from urllib.parse import unquote from aiohttp import hdrs -from aiohttp.web import middleware +from aiohttp.web import Application, Request, StreamResponse, middleware import jwt -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util from .const import KEY_AUTHENTICATED, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) -DATA_API_PASSWORD = "api_password" -DATA_SIGN_SECRET = "http.auth.sign_secret" -SIGN_QUERY_PARAM = "authSig" +DATA_API_PASSWORD: Final = "api_password" +DATA_SIGN_SECRET: Final = "http.auth.sign_secret" +SIGN_QUERY_PARAM: Final = "authSig" @callback -def async_sign_path(hass, refresh_token_id, path, expiration): +def async_sign_path( + hass: HomeAssistant, refresh_token_id: str, path: str, expiration: timedelta +) -> str: """Sign a path for temporary access without auth header.""" secret = hass.data.get(DATA_SIGN_SECRET) @@ -44,17 +49,19 @@ def async_sign_path(hass, refresh_token_id, path, expiration): @callback -def setup_auth(hass, app): +def setup_auth(hass: HomeAssistant, app: Application) -> None: """Create auth middleware for the app.""" - async def async_validate_auth_header(request): + async def async_validate_auth_header(request: Request) -> bool: """ Test authorization header against access token. Basic auth_type is legacy code, should be removed with api_password. """ try: - auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(" ", 1) + auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION, "").split( + " ", 1 + ) except ValueError: # If no space in authorization header return False @@ -71,7 +78,7 @@ def setup_auth(hass, app): request[KEY_HASS_REFRESH_TOKEN_ID] = refresh_token.id return True - async def async_validate_signed_request(request): + async def async_validate_signed_request(request: Request) -> bool: """Validate a signed request.""" secret = hass.data.get(DATA_SIGN_SECRET) @@ -103,7 +110,9 @@ def setup_auth(hass, app): return True @middleware - async def auth_middleware(request, handler): + async def auth_middleware( + request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] + ) -> StreamResponse: """Authenticate as middleware.""" authenticated = False diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 5350ae5d4c8..10776f11b00 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -2,13 +2,15 @@ from __future__ import annotations from collections import defaultdict +from collections.abc import Awaitable, Callable from contextlib import suppress from datetime import datetime from ipaddress import ip_address import logging from socket import gethostbyaddr, herror +from typing import Any, Final -from aiohttp.web import middleware +from aiohttp.web import Application, Request, StreamResponse, middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized import voluptuous as vol @@ -19,33 +21,33 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util, yaml -# mypy: allow-untyped-defs, no-check-untyped-defs +from .view import HomeAssistantView -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -KEY_BANNED_IPS = "ha_banned_ips" -KEY_FAILED_LOGIN_ATTEMPTS = "ha_failed_login_attempts" -KEY_LOGIN_THRESHOLD = "ha_login_threshold" +KEY_BANNED_IPS: Final = "ha_banned_ips" +KEY_FAILED_LOGIN_ATTEMPTS: Final = "ha_failed_login_attempts" +KEY_LOGIN_THRESHOLD: Final = "ha_login_threshold" -NOTIFICATION_ID_BAN = "ip-ban" -NOTIFICATION_ID_LOGIN = "http-login" +NOTIFICATION_ID_BAN: Final = "ip-ban" +NOTIFICATION_ID_LOGIN: Final = "http-login" -IP_BANS_FILE = "ip_bans.yaml" -ATTR_BANNED_AT = "banned_at" +IP_BANS_FILE: Final = "ip_bans.yaml" +ATTR_BANNED_AT: Final = "banned_at" -SCHEMA_IP_BAN_ENTRY = vol.Schema( +SCHEMA_IP_BAN_ENTRY: Final = vol.Schema( {vol.Optional("banned_at"): vol.Any(None, cv.datetime)} ) @callback -def setup_bans(hass, app, login_threshold): +def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> None: """Create IP Ban middleware for the app.""" app.middlewares.append(ban_middleware) app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int) app[KEY_LOGIN_THRESHOLD] = login_threshold - async def ban_startup(app): + async def ban_startup(app: Application) -> None: """Initialize bans when app starts up.""" app[KEY_BANNED_IPS] = await async_load_ip_bans_config( hass, hass.config.path(IP_BANS_FILE) @@ -55,7 +57,9 @@ def setup_bans(hass, app, login_threshold): @middleware -async def ban_middleware(request, handler): +async def ban_middleware( + request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] +) -> StreamResponse: """IP Ban middleware.""" if KEY_BANNED_IPS not in request.app: _LOGGER.error("IP Ban middleware loaded but banned IPs not loaded") @@ -77,10 +81,14 @@ async def ban_middleware(request, handler): raise -def log_invalid_auth(func): +def log_invalid_auth( + func: Callable[..., Awaitable[StreamResponse]] +) -> Callable[..., Awaitable[StreamResponse]]: """Decorate function to handle invalid auth or failed login attempts.""" - async def handle_req(view, request, *args, **kwargs): + async def handle_req( + view: HomeAssistantView, request: Request, *args: Any, **kwargs: Any + ) -> StreamResponse: """Try to log failed login attempts if response status >= 400.""" resp = await func(view, request, *args, **kwargs) if resp.status >= HTTP_BAD_REQUEST: @@ -90,7 +98,7 @@ def log_invalid_auth(func): return handle_req -async def process_wrong_login(request): +async def process_wrong_login(request: Request) -> None: """Process a wrong login attempt. Increase failed login attempts counter for remote IP address. @@ -152,7 +160,7 @@ async def process_wrong_login(request): ) -async def process_success_login(request): +async def process_success_login(request: Request) -> None: """Process a success login attempt. Reset failed login attempts counter for remote IP address. diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index 3a32635bb27..df27122b64a 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -1,5 +1,7 @@ """HTTP specific constants.""" -KEY_AUTHENTICATED = "ha_authenticated" -KEY_HASS = "hass" -KEY_HASS_USER = "hass_user" -KEY_HASS_REFRESH_TOKEN_ID = "hass_refresh_token_id" +from typing import Final + +KEY_AUTHENTICATED: Final = "ha_authenticated" +KEY_HASS: Final = "hass" +KEY_HASS_USER: Final = "hass_user" +KEY_HASS_REFRESH_TOKEN_ID: Final = "hass_refresh_token_id" diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 2d99a049e4b..d9310c8937f 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,24 +1,33 @@ """Provide CORS support for the HTTP component.""" +from __future__ import annotations + +from typing import Final + from aiohttp.hdrs import ACCEPT, AUTHORIZATION, CONTENT_TYPE, ORIGIN -from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource +from aiohttp.web import Application +from aiohttp.web_urldispatcher import ( + AbstractResource, + AbstractRoute, + Resource, + ResourceRoute, + StaticResource, +) from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback -# mypy: allow-untyped-defs, no-check-untyped-defs - -ALLOWED_CORS_HEADERS = [ +ALLOWED_CORS_HEADERS: Final[list[str]] = [ ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE, AUTHORIZATION, ] -VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource) +VALID_CORS_TYPES: Final = (Resource, ResourceRoute, StaticResource) @callback -def setup_cors(app, origins): +def setup_cors(app: Application, origins: list[str]) -> None: """Set up CORS.""" # This import should remain here. That way the HTTP integration can always # be imported by other integrations without it's requirements being installed. @@ -37,9 +46,12 @@ def setup_cors(app, origins): cors_added = set() - def _allow_cors(route, config=None): + def _allow_cors( + route: AbstractRoute | AbstractResource, + config: dict[str, aiohttp_cors.ResourceOptions] | None = None, + ) -> None: """Allow CORS on a route.""" - if hasattr(route, "resource"): + if isinstance(route, AbstractRoute): path = route.resource else: path = route @@ -47,16 +59,16 @@ def setup_cors(app, origins): if not isinstance(path, VALID_CORS_TYPES): return - path = path.canonical + path_str = path.canonical - if path.startswith("/api/hassio_ingress/"): + if path_str.startswith("/api/hassio_ingress/"): return - if path in cors_added: + if path_str in cors_added: return cors.add(route, config) - cors_added.add(path) + cors_added.add(path_str) app["allow_cors"] = lambda route: _allow_cors( route, @@ -70,7 +82,7 @@ def setup_cors(app, origins): if not origins: return - async def cors_startup(app): + async def cors_startup(app: Application) -> None: """Initialize CORS when app starts up.""" for resource in list(app.router.resources()): _allow_cors(resource) diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index bf6bb811a81..5c5726a2597 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -1,19 +1,20 @@ """Middleware to handle forwarded data by a reverse proxy.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable from ipaddress import ip_address import logging from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO -from aiohttp.web import HTTPBadRequest, middleware +from aiohttp.web import Application, HTTPBadRequest, Request, StreamResponse, middleware from homeassistant.core import callback _LOGGER = logging.getLogger(__name__) -# mypy: allow-untyped-defs - @callback -def async_setup_forwarded(app, trusted_proxies): +def async_setup_forwarded(app: Application, trusted_proxies: list[str]) -> None: """Create forwarded middleware for the app. Process IP addresses, proto and host information in the forwarded for headers. @@ -60,17 +61,20 @@ def async_setup_forwarded(app, trusted_proxies): """ @middleware - async def forwarded_middleware(request, handler): + async def forwarded_middleware( + request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] + ) -> StreamResponse: """Process forwarded data by a reverse proxy.""" - overrides = {} + overrides: dict[str, str] = {} # Handle X-Forwarded-For - forwarded_for_headers = request.headers.getall(X_FORWARDED_FOR, []) + forwarded_for_headers: list[str] = request.headers.getall(X_FORWARDED_FOR, []) if not forwarded_for_headers: # No forwarding headers, continue as normal return await handler(request) # Ensure the IP of the connected peer is trusted + assert request.transport is not None connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) if not any(connected_ip in trusted_proxy for trusted_proxy in trusted_proxies): _LOGGER.warning( @@ -111,7 +115,9 @@ def async_setup_forwarded(app, trusted_proxies): overrides["remote"] = str(forwarded_for[-1]) # Handle X-Forwarded-Proto - forwarded_proto_headers = request.headers.getall(X_FORWARDED_PROTO, []) + forwarded_proto_headers: list[str] = request.headers.getall( + X_FORWARDED_PROTO, [] + ) if forwarded_proto_headers: if len(forwarded_proto_headers) > 1: _LOGGER.error( @@ -151,7 +157,7 @@ def async_setup_forwarded(app, trusted_proxies): overrides["scheme"] = forwarded_proto[forwarded_for_index] # Handle X-Forwarded-Host - forwarded_host_headers = request.headers.getall(X_FORWARDED_HOST, []) + forwarded_host_headers: list[str] = request.headers.getall(X_FORWARDED_HOST, []) if forwarded_host_headers: # Multiple X-Forwarded-Host headers if len(forwarded_host_headers) > 1: @@ -168,7 +174,7 @@ def async_setup_forwarded(app, trusted_proxies): overrides["host"] = forwarded_host # Done, create a new request based on gathered data. - request = request.clone(**overrides) + request = request.clone(**overrides) # type: ignore[arg-type] return await handler(request) app.middlewares.append(forwarded_middleware) diff --git a/homeassistant/components/http/request_context.py b/homeassistant/components/http/request_context.py index 23a85214c3f..032f3bfd49e 100644 --- a/homeassistant/components/http/request_context.py +++ b/homeassistant/components/http/request_context.py @@ -1,18 +1,24 @@ """Middleware to set the request context.""" +from __future__ import annotations -from aiohttp.web import middleware +from collections.abc import Awaitable, Callable +from contextvars import ContextVar + +from aiohttp.web import Application, Request, StreamResponse, middleware from homeassistant.core import callback -# mypy: allow-untyped-defs - @callback -def setup_request_context(app, context): +def setup_request_context( + app: Application, context: ContextVar[Request | None] +) -> None: """Create request context middleware for the app.""" @middleware - async def request_context_middleware(request, handler): + async def request_context_middleware( + request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] + ) -> StreamResponse: """Request context middleware.""" context.set(request) return await handler(request) diff --git a/homeassistant/components/http/security_filter.py b/homeassistant/components/http/security_filter.py index eab0a2b0764..57ae9063170 100644 --- a/homeassistant/components/http/security_filter.py +++ b/homeassistant/components/http/security_filter.py @@ -1,17 +1,19 @@ """Middleware to add some basic security filtering to requests.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable import logging import re +from typing import Final -from aiohttp.web import HTTPBadRequest, middleware +from aiohttp.web import Application, HTTPBadRequest, Request, StreamResponse, middleware from homeassistant.core import callback _LOGGER = logging.getLogger(__name__) -# mypy: allow-untyped-defs - # fmt: off -FILTERS = re.compile( +FILTERS: Final = re.compile( r"(?:" # Common exploits @@ -34,12 +36,14 @@ FILTERS = re.compile( @callback -def setup_security_filter(app): +def setup_security_filter(app: Application) -> None: """Create security filter middleware for the app.""" @middleware - async def security_filter_middleware(request, handler): - """Process request and block commonly known exploit attempts.""" + async def security_filter_middleware( + request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] + ) -> StreamResponse: + """Process request and tblock commonly known exploit attempts.""" if FILTERS.search(request.path): _LOGGER.warning( "Filtered a potential harmful request to: %s", request.raw_path diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index a5fe686a651..112549553eb 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -1,21 +1,25 @@ """Static file handling for HTTP component.""" +from __future__ import annotations + +from collections.abc import Mapping from pathlib import Path +from typing import Final from aiohttp import hdrs -from aiohttp.web import FileResponse +from aiohttp.web import FileResponse, Request, StreamResponse from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound from aiohttp.web_urldispatcher import StaticResource -# mypy: allow-untyped-defs - -CACHE_TIME = 31 * 86400 # = 1 month -CACHE_HEADERS = {hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}"} +CACHE_TIME: Final = 31 * 86400 # = 1 month +CACHE_HEADERS: Final[Mapping[str, str]] = { + hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}" +} class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" - async def _handle(self, request): + async def _handle(self, request: Request) -> StreamResponse: rel_url = request.match_info["filename"] try: filename = Path(rel_url) @@ -42,7 +46,6 @@ class CachingStaticResource(StaticResource): return FileResponse( filepath, chunk_size=self._chunk_size, - # type ignore: https://github.com/aio-libs/aiohttp/pull/3976 - headers=CACHE_HEADERS, # type: ignore + headers=CACHE_HEADERS, ) raise HTTPNotFound diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index b4dbb845638..9abf0914b06 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Callable import json import logging -from typing import Any, Callable +from typing import Any from aiohttp import web from aiohttp.typedefs import LooseHeaders @@ -13,6 +14,7 @@ from aiohttp.web_exceptions import ( HTTPInternalServerError, HTTPUnauthorized, ) +from aiohttp.web_urldispatcher import AbstractRoute import voluptuous as vol from homeassistant import exceptions @@ -81,7 +83,7 @@ class HomeAssistantView: """Register the view with a router.""" assert self.url is not None, "No url set for view" urls = [self.url] + self.extra_urls - routes = [] + routes: list[AbstractRoute] = [] for method in ("get", "post", "delete", "put", "patch", "head", "options"): handler = getattr(self, method, None) @@ -101,7 +103,9 @@ class HomeAssistantView: app["allow_cors"](route) -def request_handler_factory(view: HomeAssistantView, handler: Callable) -> Callable: +def request_handler_factory( + view: HomeAssistantView, handler: Callable +) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: """Wrap the handler classes.""" assert asyncio.iscoroutinefunction(handler) or is_callback( handler diff --git a/homeassistant/components/http/web_runner.py b/homeassistant/components/http/web_runner.py index f3dd59bf9d7..6d03b874a64 100644 --- a/homeassistant/components/http/web_runner.py +++ b/homeassistant/components/http/web_runner.py @@ -23,7 +23,7 @@ class HomeAssistantTCPSite(web.BaseSite): __slots__ = ("_host", "_port", "_reuse_address", "_reuse_port", "_hosturl") - def __init__( # noqa: D107 + def __init__( self, runner: web.BaseRunner, host: None | str | list[str], @@ -35,6 +35,7 @@ class HomeAssistantTCPSite(web.BaseSite): reuse_address: bool | None = None, reuse_port: bool | None = None, ) -> None: + """Initialize HomeAssistantTCPSite.""" super().__init__( runner, shutdown_timeout=shutdown_timeout, @@ -47,12 +48,14 @@ class HomeAssistantTCPSite(web.BaseSite): self._reuse_port = reuse_port @property - def name(self) -> str: # noqa: D102 + def name(self) -> str: + """Return server URL.""" scheme = "https" if self._ssl_context else "http" host = self._host[0] if isinstance(self._host, list) else "0.0.0.0" return str(URL.build(scheme=scheme, host=host, port=self._port)) - async def start(self) -> None: # noqa: D102 + async def start(self) -> None: + """Start server.""" await super().start() loop = asyncio.get_running_loop() server = self._runner.server diff --git a/homeassistant/const.py b/homeassistant/const.py index 8aafc77356d..489652b3c12 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -593,7 +593,7 @@ SERVICE_TOGGLE_COVER_TILT = "toggle_cover_tilt" SERVICE_SELECT_OPTION = "select_option" # #### API / REMOTE #### -SERVER_PORT = 8123 +SERVER_PORT: Final = 8123 URL_ROOT = "/" URL_API = "/api/" diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index be9afe385ca..74cc56fd9f6 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -334,7 +334,7 @@ def async_register_implementation( if isinstance(implementation, LocalOAuth2Implementation) and not hass.data.get( DATA_VIEW_REGISTERED, False ): - hass.http.register_view(OAuth2AuthorizeCallbackView()) # type: ignore + hass.http.register_view(OAuth2AuthorizeCallbackView()) hass.data[DATA_VIEW_REGISTERED] = True implementations = hass.data.setdefault(DATA_IMPLEMENTATIONS, {}) From 887ec2d9b52ac1f111194ce70d321cd547dcedb3 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 11 May 2021 00:04:41 +0000 Subject: [PATCH 308/852] [ci skip] Translation update --- .../binary_sensor/translations/nl.json | 2 +- .../components/cast/translations/no.json | 21 ++++++++++++--- .../components/cast/translations/sv.json | 9 +++++++ .../components/elgato/translations/nl.json | 2 +- .../components/elgato/translations/no.json | 8 +++--- .../components/epson/translations/no.json | 3 ++- .../components/fritzbox/translations/nl.json | 2 +- .../growatt_server/translations/en.json | 4 +-- .../growatt_server/translations/it.json | 27 +++++++++++++++++++ .../motion_blinds/translations/de.json | 3 +++ .../components/motioneye/translations/de.json | 4 +++ .../components/mqtt/translations/no.json | 2 +- .../components/nam/translations/no.json | 24 +++++++++++++++++ .../components/nam/translations/sv.json | 18 +++++++++++++ .../components/syncthing/translations/de.json | 3 ++- .../components/syncthing/translations/no.json | 22 +++++++++++++++ .../components/syncthing/translations/sv.json | 14 ++++++++++ 17 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/growatt_server/translations/it.json create mode 100644 homeassistant/components/nam/translations/no.json create mode 100644 homeassistant/components/nam/translations/sv.json create mode 100644 homeassistant/components/syncthing/translations/no.json create mode 100644 homeassistant/components/syncthing/translations/sv.json diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index 36dbb530fdb..9352bfa8d47 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -57,7 +57,7 @@ "moving": "{entity_name} begon te bewegen", "no_gas": "{entity_name} is gestopt met het detecteren van gas", "no_light": "{entity_name} gestopt met het detecteren van licht", - "no_motion": "{entity_name} gestopt met het detecteren van beweging", + "no_motion": "{entity_name} is gestopt met het detecteren van beweging", "no_problem": "{entity_name} gestopt met het detecteren van het probleem", "no_smoke": "{entity_name} gestopt met het detecteren van rook", "no_sound": "{entity_name} gestopt met het detecteren van geluid", diff --git a/homeassistant/components/cast/translations/no.json b/homeassistant/components/cast/translations/no.json index 0c9b3d93dce..315926a84be 100644 --- a/homeassistant/components/cast/translations/no.json +++ b/homeassistant/components/cast/translations/no.json @@ -10,10 +10,10 @@ "step": { "config": { "data": { - "known_hosts": "Valgfri liste over kjente verter hvis mDNS-oppdagelse ikke fungerer." + "known_hosts": "Kjente verter" }, - "description": "Angi Google Cast-konfigurasjonen.", - "title": "Google Cast" + "description": "Kjente verter - En kommaseparert liste over vertsnavn eller IP-adresser til cast-enheter, bruk hvis mDNS discovery ikke fungerer.", + "title": "Google Cast-konfigurasjon" }, "confirm": { "description": "Vil du starte oppsettet?" @@ -25,6 +25,21 @@ "invalid_known_hosts": "Kjente verter m\u00e5 v\u00e6re en kommaseparert liste over verter." }, "step": { + "advanced_options": { + "data": { + "ignore_cec": "Ignorer CEC", + "uuid": "Tillatte UUIDer" + }, + "description": "Tillatte UUID-er - En komma-separert liste over UUID-er for Cast-enheter \u00e5 legge til i Home Assistant. Bruk bare hvis du ikke vil legge til alle tilgjengelige cast-enheter.\n Ignorer CEC - En kommaseparert liste over Chromecasts som b\u00f8r ignorere CEC-data for \u00e5 bestemme den aktive inngangen. Dette vil bli sendt til pychromecast.IGNORE_CEC.", + "title": "Avansert Google Cast-konfigurasjon" + }, + "basic_options": { + "data": { + "known_hosts": "Kjente verter" + }, + "description": "Kjente verter - En kommaseparert liste over vertsnavn eller IP-adresser til cast-enheter, bruk hvis mDNS discovery ikke fungerer.", + "title": "Google Cast-konfigurasjon" + }, "options": { "data": { "ignore_cec": "Valgfri liste som sendes til pychromecast.IGNORE_CEC.", diff --git a/homeassistant/components/cast/translations/sv.json b/homeassistant/components/cast/translations/sv.json index 056c00b1765..982b52b65dd 100644 --- a/homeassistant/components/cast/translations/sv.json +++ b/homeassistant/components/cast/translations/sv.json @@ -9,5 +9,14 @@ "description": "Vill du konfigurera Google Cast?" } } + }, + "options": { + "step": { + "advanced_options": { + "data": { + "ignore_cec": "Ignorera CEC" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/nl.json b/homeassistant/components/elgato/translations/nl.json index fcda6a7ca84..165d64df6b4 100644 --- a/homeassistant/components/elgato/translations/nl.json +++ b/homeassistant/components/elgato/translations/nl.json @@ -17,7 +17,7 @@ "description": "Stel uw Elgato Key Light in om te integreren met Home Assistant." }, "zeroconf_confirm": { - "description": "Wilt u de Elgato Key Light met serienummer ` {serial_number} ` toevoegen aan Home Assistant?", + "description": "Wilt u de Elgato Key Light met serienummer `{serial_number}` toevoegen aan Home Assistant?", "title": "Elgato Key Light apparaat ontdekt" } } diff --git a/homeassistant/components/elgato/translations/no.json b/homeassistant/components/elgato/translations/no.json index 8a44fd67972..8059138e366 100644 --- a/homeassistant/components/elgato/translations/no.json +++ b/homeassistant/components/elgato/translations/no.json @@ -7,18 +7,18 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "Elgato Light: {serial_number}", "step": { "user": { "data": { "host": "Vert", "port": "Port" }, - "description": "Sett opp Elgato Key Light for \u00e5 integrere med Home Assistant." + "description": "Sett opp Elgato Light for \u00e5 integrere med Home Assistant." }, "zeroconf_confirm": { - "description": "Vil du legge Elgato Key Light med serienummer ` {serial_number} til Home Assistant?", - "title": "Oppdaget Elgato Key Light-enheten" + "description": "Vil du legge til Elgato Light med serienummeret \" {serial_number} \" i Home Assistant?", + "title": "Oppdaget Elgato Light-enhet" } } } diff --git a/homeassistant/components/epson/translations/no.json b/homeassistant/components/epson/translations/no.json index 0b164f050f4..fc4bf7dcf36 100644 --- a/homeassistant/components/epson/translations/no.json +++ b/homeassistant/components/epson/translations/no.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "powered_off": "Er projektoren sl\u00e5tt p\u00e5? Du m\u00e5 sl\u00e5 p\u00e5 projektoren for \u00e5 f\u00e5 den f\u00f8rste konfigurasjonen." }, "step": { "user": { diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json index aa4f796f44c..c1f9c83e395 100644 --- a/homeassistant/components/fritzbox/translations/nl.json +++ b/homeassistant/components/fritzbox/translations/nl.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Ongeldige authenticatie" }, - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "AVM FRITZ!SmartHome: {name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/growatt_server/translations/en.json b/homeassistant/components/growatt_server/translations/en.json index 11bb6a64b32..365f577a007 100644 --- a/homeassistant/components/growatt_server/translations/en.json +++ b/homeassistant/components/growatt_server/translations/en.json @@ -16,7 +16,7 @@ "user": { "data": { "name": "Name", - "password": "Password", + "password": "Name", "username": "Username" }, "title": "Enter your Growatt information" @@ -24,4 +24,4 @@ } }, "title": "Growatt Server" -} +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/it.json b/homeassistant/components/growatt_server/translations/it.json new file mode 100644 index 00000000000..7676ecfff3c --- /dev/null +++ b/homeassistant/components/growatt_server/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "Nessuna pianta trovata per questo account" + }, + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "plant": { + "data": { + "plant_id": "Pianta" + }, + "title": "Seleziona la pianta" + }, + "user": { + "data": { + "name": "Nome", + "password": "Nome", + "username": "Utente" + }, + "title": "Inserisci le tue informazioni Growatt" + } + } + }, + "title": "Server Growatt" +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json index 01eba9c7ecd..6c145898598 100644 --- a/homeassistant/components/motion_blinds/translations/de.json +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -5,6 +5,9 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "connection_error": "Verbindung fehlgeschlagen" }, + "error": { + "discovery_error": "Motion-Gateway konnte nicht gefunden werden" + }, "flow_title": "Jalousien", "step": { "connect": { diff --git a/homeassistant/components/motioneye/translations/de.json b/homeassistant/components/motioneye/translations/de.json index fec4680a9e1..94b86f04b2d 100644 --- a/homeassistant/components/motioneye/translations/de.json +++ b/homeassistant/components/motioneye/translations/de.json @@ -13,6 +13,10 @@ "step": { "user": { "data": { + "admin_password": "Admin Passwort", + "admin_username": "Admin Benutzername", + "surveillance_password": "Surveillance Passwort", + "surveillance_username": "Surveillance Benutzername", "url": "URL" } } diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index d12237deba1..fee6505862a 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -79,7 +79,7 @@ "will_retain": "Testament melding behold", "will_topic": "Testament melding emne" }, - "description": "Vennligst velg MQTT-alternativer.", + "description": "Discovery - Hvis oppdagelse er aktivert (anbefales), vil Home Assistant automatisk oppdage enheter og enheter som publiserer konfigurasjonen p\u00e5 MQTT-megleren. Hvis s\u00f8k er deaktivert, m\u00e5 all konfigurasjon utf\u00f8res manuelt.\nF\u00f8dselsmelding - F\u00f8dselsmeldingen vil bli sendt hver gang Home Assistant (re) kobles til MQTT megleren.\nWill message - Will-meldingen vil bli sendt hver gang Home Assistant mister forbindelsen til megleren, b\u00e5de i tilfelle en ren (f.eks. at Home Assistant avsluttes) og i tilfelle en uren (f.eks. hjemmeassistent krasjer eller mister nettverkstilkoblingen) koble fra.", "title": "MQTT-alternativer" } } diff --git a/homeassistant/components/nam/translations/no.json b/homeassistant/components/nam/translations/no.json new file mode 100644 index 00000000000..923efe4937b --- /dev/null +++ b/homeassistant/components/nam/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "device_unsupported": "Enheten st\u00f8ttes ikke." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Vil du konfigurere Nettigo Air Monitor p\u00e5 {host} ?" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Sett opp integrering av Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sv.json b/homeassistant/components/nam/translations/sv.json new file mode 100644 index 00000000000..15a583f12a2 --- /dev/null +++ b/homeassistant/components/nam/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "device_unsupported": "Enheten st\u00f6ds ej" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta ", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/de.json b/homeassistant/components/syncthing/translations/de.json index 90eb00fc001..25562c2b4f9 100644 --- a/homeassistant/components/syncthing/translations/de.json +++ b/homeassistant/components/syncthing/translations/de.json @@ -15,5 +15,6 @@ } } } - } + }, + "title": "Syncthing" } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/no.json b/homeassistant/components/syncthing/translations/no.json new file mode 100644 index 00000000000..9fdb9f5f305 --- /dev/null +++ b/homeassistant/components/syncthing/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "title": "Setup Syncthing integrasjon", + "token": "Token", + "url": "URL", + "verify_ssl": "Verifisere SSL-sertifikat" + } + } + } + }, + "title": "Synkronisering" +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/sv.json b/homeassistant/components/syncthing/translations/sv.json new file mode 100644 index 00000000000..7862919ad54 --- /dev/null +++ b/homeassistant/components/syncthing/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL", + "verify_ssl": "Verifiera SSL-certifikat" + } + } + } + }, + "title": "Synkronisering" +} \ No newline at end of file From b36c840909b088422be6b2f8effd5bc15f5f6957 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 May 2021 21:26:15 -0500 Subject: [PATCH 309/852] Add dhcp support to guardian (#50378) --- .../components/guardian/config_flow.py | 58 ++++++++++++------- .../components/guardian/manifest.json | 12 +++- .../components/guardian/strings.json | 2 +- .../components/guardian/translations/en.json | 6 +- homeassistant/generated/dhcp.py | 10 ++++ tests/components/guardian/test_config_flow.py | 55 +++++++++++++++--- 6 files changed, 110 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index 5e7834f4a3f..05be79da344 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -4,13 +4,19 @@ from aioguardian.errors import GuardianError import voluptuous as vol from homeassistant import config_entries, core +from homeassistant.components.dhcp import IP_ADDRESS from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import callback from .const import CONF_UID, DOMAIN, LOGGER +DEFAULT_PORT = 7777 + DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PORT, default=7777): int} + { + vol.Required(CONF_IP_ADDRESS): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } ) UNIQUE_ID = "guardian_{0}" @@ -53,7 +59,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_set_unique_id(self, pin): """Set the config entry's unique ID (based on the device's 4-digit PIN).""" await self.async_set_unique_id(UNIQUE_ID.format(pin)) - self._abort_if_unique_id_configured() + if self.discovery_info: + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: self.discovery_info[CONF_IP_ADDRESS]} + ) + else: + self._abort_if_unique_id_configured() async def async_step_user(self, user_input=None): """Handle configuration via the UI.""" @@ -79,31 +90,38 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=info[CONF_UID], data={CONF_UID: info["uid"], **user_input} ) + async def async_step_dhcp(self, discovery_info): + """Handle the configuration via dhcp.""" + self.discovery_info = { + CONF_IP_ADDRESS: discovery_info[IP_ADDRESS], + CONF_PORT: DEFAULT_PORT, + } + return await self._async_handle_discovery() + async def async_step_zeroconf(self, discovery_info): """Handle the configuration via zeroconf.""" - if discovery_info is None: - return self.async_abort(reason="cannot_connect") - - pin = async_get_pin_from_discovery_hostname(discovery_info["hostname"]) - await self._async_set_unique_id(pin) - - self.context[CONF_IP_ADDRESS] = discovery_info["host"] - - if any( - discovery_info["host"] == flow["context"][CONF_IP_ADDRESS] - for flow in self._async_in_progress() - ): - return self.async_abort(reason="already_in_progress") - self.discovery_info = { CONF_IP_ADDRESS: discovery_info["host"], CONF_PORT: discovery_info["port"], } + pin = async_get_pin_from_discovery_hostname(discovery_info["hostname"]) + await self._async_set_unique_id(pin) + return await self._async_handle_discovery() - return await self.async_step_zeroconf_confirm() + async def _async_handle_discovery(self): + """Handle any discovery.""" + self.context[CONF_IP_ADDRESS] = self.discovery_info[CONF_IP_ADDRESS] + if any( + self.context[CONF_IP_ADDRESS] == flow["context"][CONF_IP_ADDRESS] + for flow in self._async_in_progress() + ): + return self.async_abort(reason="already_in_progress") - async def async_step_zeroconf_confirm(self, user_input=None): - """Finish the configuration via zeroconf.""" + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm(self, user_input=None): + """Finish the configuration via any discovery.""" if user_input is None: - return self.async_show_form(step_id="zeroconf_confirm") + self._set_confirm_only() + return self.async_show_form(step_id="discovery_confirm") return await self.async_step_user(self.discovery_info) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index 28f46a9bf14..60411c5292b 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -6,5 +6,15 @@ "requirements": ["aioguardian==1.0.4"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "dhcp": [ + { + "hostname": "gvc*", + "macaddress": "30AEA4*" + }, + { + "hostname": "guardian*", + "macaddress": "30AEA4*" + } + ] } diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index 59c838c7e2d..4c60bfe4572 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -8,7 +8,7 @@ "port": "[%key:common::config_flow::data::port%]" } }, - "zeroconf_confirm": { + "discovery_confirm": { "description": "Do you want to set up this Guardian device?" } }, diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index 3b038935662..310f550bcc1 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -6,15 +6,15 @@ "cannot_connect": "Failed to connect" }, "step": { + "discovery_confirm": { + "description": "Do you want to set up this Guardian device?" + }, "user": { "data": { "ip_address": "IP Address", "port": "Port" }, "description": "Configure a local Elexa Guardian device." - }, - "zeroconf_confirm": { - "description": "Do you want to set up this Guardian device?" } } } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index d7b97dd29e9..368d0189ab4 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -66,6 +66,16 @@ DHCP = [ "domain": "flume", "hostname": "flume-gw-*" }, + { + "domain": "guardian", + "hostname": "gvc*", + "macaddress": "30AEA4*" + }, + { + "domain": "guardian", + "hostname": "guardian*", + "macaddress": "30AEA4*" + }, { "domain": "hunterdouglas_powerview", "hostname": "hunter*", diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index e9b53b4e629..4fbff7d7e48 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -4,12 +4,13 @@ from unittest.mock import patch from aioguardian.errors import GuardianError from homeassistant import data_entry_flow +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.guardian import CONF_UID, DOMAIN from homeassistant.components.guardian.config_flow import ( async_get_pin_from_discovery_hostname, async_get_pin_from_uid, ) -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from tests.common import MockConfigEntry @@ -95,7 +96,7 @@ async def test_step_zeroconf(hass, ping_client): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "zeroconf_confirm" + assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -124,7 +125,7 @@ async def test_step_zeroconf_already_in_progress(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "zeroconf_confirm" + assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data @@ -133,10 +134,48 @@ async def test_step_zeroconf_already_in_progress(hass): assert result["reason"] == "already_in_progress" -async def test_step_zeroconf_no_discovery_info(hass): - """Test the zeroconf step aborting because no discovery info came along.""" +async def test_step_dhcp(hass, ping_client): + """Test the dhcp step.""" + dhcp_data = { + IP_ADDRESS: "192.168.1.100", + HOSTNAME: "GVC1-ABCD.local.", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + } + result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF} + DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "ABCDEF123456" + assert result["data"] == { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PORT: 7777, + CONF_UID: "ABCDEF123456", + } + + +async def test_step_dhcp_already_in_progress(hass): + """Test the zeroconf step aborting because it's already in progress.""" + dhcp_data = { + IP_ADDRESS: "192.168.1.100", + HOSTNAME: "GVC1-ABCD.local.", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data + ) + assert result["type"] == "abort" + assert result["reason"] == "already_in_progress" From d4cdb8e7e89cb4bfde6da9219b299f2c503f13c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 00:06:29 -0500 Subject: [PATCH 310/852] Include mac address in roomba device info when available (#50437) --- homeassistant/components/roomba/irobot_base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 9d6a0f5cafc..45a69d38576 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -21,6 +21,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import Entity from . import roomba_reported_state @@ -92,13 +93,18 @@ class IRobotEntity(Entity): @property def device_info(self): """Return the device info of the vacuum cleaner.""" - return { + info = { "identifiers": {(DOMAIN, self.robot_unique_id)}, "manufacturer": "iRobot", "name": str(self._name), "sw_version": self._version, "model": self._sku, } + if mac_address := self.vacuum_state.get("hwPartsRev", {}).get( + "wlan0HwAddr", self.vacuum_state.get("mac") + ): + info["connections"] = {(dr.CONNECTION_NETWORK_MAC, mac_address)} + return info @property def _battery_level(self): From 34320ef61714ad859bc26b9e7c374094fa8d5a1e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 00:06:49 -0500 Subject: [PATCH 311/852] Include mac address in rainmachine device info (#50438) --- homeassistant/components/rainmachine/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 66fcc8939fb..09af357617d 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -196,6 +197,7 @@ class RainMachineEntity(CoordinatorEntity): """Return device registry information for this entity.""" return { "identifiers": {(DOMAIN, self._controller.mac)}, + "connections": {(dr.CONNECTION_NETWORK_MAC, self._controller.mac)}, "name": self._controller.name, "manufacturer": "RainMachine", "model": ( From 44a790ab47fec24e2b8a86e0d00775797b3e7476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 11 May 2021 08:11:51 +0300 Subject: [PATCH 312/852] Entity.device_info typing fixes (#49974) --- homeassistant/components/esphome/__init__.py | 12 ++++++------ homeassistant/components/mobile_app/helpers.py | 5 +++-- homeassistant/components/onewire/onewire_entities.py | 4 ++-- homeassistant/components/starline/account.py | 3 ++- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index a3b3f187906..e62cb995b98 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -5,12 +5,12 @@ import asyncio import functools import logging import math -from typing import Any, Callable +from typing import Callable from aioesphomeapi import ( APIClient, APIConnectionError, - DeviceInfo, + DeviceInfo as EsphomeDeviceInfo, EntityInfo, EntityState, HomeassistantServiceCall, @@ -36,7 +36,7 @@ from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.service import async_set_service_schema @@ -448,7 +448,7 @@ class ReconnectLogic(RecordUpdateListener): async def _async_setup_device_registry( - hass: HomeAssistant, entry: ConfigEntry, device_info: DeviceInfo + hass: HomeAssistant, entry: ConfigEntry, device_info: EsphomeDeviceInfo ): """Set up device registry feature for a particular config entry.""" sw_version = device_info.esphome_version @@ -769,7 +769,7 @@ class EsphomeBaseEntity(Entity): return self._entry_data.old_info[self._component_key].get(self._key) @property - def _device_info(self) -> DeviceInfo: + def _device_info(self) -> EsphomeDeviceInfo: return self._entry_data.device_info @property @@ -803,7 +803,7 @@ class EsphomeBaseEntity(Entity): return self._static_info.unique_id @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" return { "connections": {(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)} diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 7fe4bb5ecd6..9902e1d93d7 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -16,6 +16,7 @@ from homeassistant.const import ( HTTP_OK, ) from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.json import JSONEncoder from .const import ( @@ -166,12 +167,12 @@ def webhook_response( ) -def device_info(registration: dict) -> dict: +def device_info(registration: dict) -> DeviceInfo: """Return the device info for this registration.""" return { "identifiers": {(DOMAIN, registration[ATTR_DEVICE_ID])}, "manufacturer": registration[ATTR_MANUFACTURER], "model": registration[ATTR_MODEL], - "device_name": registration[ATTR_DEVICE_NAME], + "name": registration[ATTR_DEVICE_NAME], "sw_version": registration[ATTR_OS_VERSION], } diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index 2581958eeb5..3927d2626ac 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -28,7 +28,7 @@ class OneWireBaseEntity(Entity): device_file, entity_type: str, entity_name: str = None, - device_info=None, + device_info: DeviceInfo | None = None, default_disabled: bool = False, unique_id: str = None, ): @@ -82,7 +82,7 @@ class OneWireProxyEntity(OneWireBaseEntity): self, device_id: str, device_name: str, - device_info: dict[str, Any], + device_info: DeviceInfo, entity_path: str, entity_specs: dict[str, Any], owproxy: protocol._Proxy, diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 3f82b816cd5..fc7be6582a7 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -8,6 +8,7 @@ from starline import StarlineApi, StarlineDevice from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from .const import ( @@ -125,7 +126,7 @@ class StarlineAccount: self._unsubscribe_auto_obd_updater = None @staticmethod - def device_info(device: StarlineDevice) -> dict[str, Any]: + def device_info(device: StarlineDevice) -> DeviceInfo: """Device information for entities.""" return { "identifiers": {(DOMAIN, device.device_id)}, From d5e39e8748662876696fe571c3add0fe09b9136b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 00:14:33 -0500 Subject: [PATCH 313/852] Remove redundant names from config flow titles (#50380) --- .../components/apple_tv/strings.json | 2 +- .../components/apple_tv/translations/en.json | 2 +- .../components/arcam_fmj/strings.json | 2 +- .../components/arcam_fmj/translations/en.json | 3 ++- .../components/azure_devops/strings.json | 2 +- .../azure_devops/translations/en.json | 2 +- homeassistant/components/blebox/strings.json | 2 +- .../components/blebox/translations/en.json | 2 +- homeassistant/components/bond/strings.json | 2 +- .../components/bond/translations/en.json | 2 +- homeassistant/components/brother/strings.json | 4 ++-- .../components/brother/translations/en.json | 2 +- homeassistant/components/bsblan/strings.json | 2 +- .../components/bsblan/translations/en.json | 2 +- homeassistant/components/canary/strings.json | 2 +- .../components/canary/translations/en.json | 2 +- .../components/cloudflare/strings.json | 2 +- .../cloudflare/translations/en.json | 2 +- homeassistant/components/deconz/strings.json | 2 +- .../components/deconz/translations/en.json | 2 +- .../components/denonavr/strings.json | 2 +- .../components/denonavr/translations/en.json | 2 +- homeassistant/components/directv/strings.json | 2 +- .../components/directv/translations/en.json | 3 ++- .../components/doorbird/strings.json | 2 +- .../components/doorbird/translations/en.json | 2 +- homeassistant/components/elgato/strings.json | 2 +- .../components/elgato/translations/en.json | 2 +- .../components/emonitor/strings.json | 2 +- .../components/emonitor/translations/en.json | 2 +- .../components/enphase_envoy/strings.json | 4 ++-- .../enphase_envoy/translations/en.json | 2 +- homeassistant/components/esphome/strings.json | 2 +- .../components/esphome/translations/en.json | 2 +- .../components/forked_daapd/strings.json | 2 +- .../forked_daapd/translations/en.json | 2 +- homeassistant/components/fritz/strings.json | 2 +- .../components/fritz/translations/en.json | 2 +- .../components/fritzbox/strings.json | 4 ++-- .../components/fritzbox/translations/en.json | 2 +- .../fritzbox_callmonitor/strings.json | 2 +- .../fritzbox_callmonitor/translations/en.json | 2 +- homeassistant/components/harmony/strings.json | 2 +- .../components/harmony/translations/en.json | 2 +- .../homekit_controller/strings.json | 2 +- .../homekit_controller/translations/en.json | 2 +- .../components/huawei_lte/strings.json | 2 +- .../huawei_lte/translations/en.json | 3 +-- homeassistant/components/ipp/strings.json | 2 +- .../components/ipp/translations/en.json | 2 +- homeassistant/components/isy994/strings.json | 2 +- .../components/isy994/translations/en.json | 2 +- homeassistant/components/kodi/strings.json | 2 +- .../components/kodi/translations/en.json | 2 +- .../components/lutron_caseta/strings.json | 2 +- .../lutron_caseta/translations/en.json | 2 +- .../components/motion_blinds/strings.json | 1 - .../motion_blinds/translations/en.json | 2 -- .../components/nightscout/strings.json | 1 - .../nightscout/translations/en.json | 1 - homeassistant/components/nzbget/strings.json | 2 +- .../components/nzbget/translations/en.json | 2 +- .../components/ovo_energy/strings.json | 2 +- .../ovo_energy/translations/en.json | 2 +- .../components/plugwise/strings.json | 2 +- .../components/plugwise/translations/en.json | 2 +- .../components/powerwall/strings.json | 2 +- .../components/powerwall/translations/en.json | 2 +- .../components/rainmachine/strings.json | 2 +- .../rainmachine/translations/en.json | 2 +- homeassistant/components/roku/strings.json | 2 +- .../components/roku/translations/en.json | 7 ++----- homeassistant/components/roomba/strings.json | 2 +- .../components/roomba/translations/en.json | 13 +----------- .../components/samsungtv/strings.json | 2 +- .../components/samsungtv/translations/en.json | 2 +- .../components/screenlogic/strings.json | 4 ++-- .../screenlogic/translations/en.json | 2 +- homeassistant/components/smappee/strings.json | 2 +- .../components/smappee/translations/en.json | 2 +- .../components/somfy_mylink/strings.json | 5 ++--- .../somfy_mylink/translations/en.json | 14 ++----------- homeassistant/components/sonarr/strings.json | 2 +- .../components/sonarr/translations/en.json | 2 +- homeassistant/components/songpal/strings.json | 2 +- .../components/songpal/translations/en.json | 2 +- .../components/squeezebox/strings.json | 2 +- .../squeezebox/translations/en.json | 2 +- .../components/syncthru/strings.json | 2 +- .../components/syncthru/translations/en.json | 2 +- .../components/synology_dsm/strings.json | 2 +- .../synology_dsm/translations/en.json | 2 +- .../components/system_bridge/strings.json | 3 +-- .../system_bridge/translations/en.json | 5 ++--- homeassistant/components/tuya/strings.json | 1 - .../components/tuya/translations/en.json | 1 - homeassistant/components/unifi/strings.json | 2 +- .../components/unifi/translations/en.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/upnp/strings.json | 2 +- .../components/upnp/translations/en.json | 3 ++- homeassistant/components/wilight/strings.json | 2 +- .../components/wilight/translations/en.json | 2 +- .../components/withings/strings.json | 2 +- .../components/withings/translations/en.json | 2 +- homeassistant/components/wled/strings.json | 2 +- .../components/wled/translations/en.json | 2 +- .../components/xiaomi_aqara/strings.json | 2 +- .../xiaomi_aqara/translations/en.json | 2 +- .../components/xiaomi_miio/strings.json | 2 +- .../xiaomi_miio/translations/en.json | 20 +------------------ homeassistant/components/zha/strings.json | 2 +- .../components/zha/translations/en.json | 2 +- 113 files changed, 118 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/apple_tv/strings.json b/homeassistant/components/apple_tv/strings.json index e990fa0de06..00dd92cac89 100644 --- a/homeassistant/components/apple_tv/strings.json +++ b/homeassistant/components/apple_tv/strings.json @@ -1,7 +1,7 @@ { "title": "Apple TV", "config": { - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "user": { "title": "Setup a new Apple TV", diff --git a/homeassistant/components/apple_tv/translations/en.json b/homeassistant/components/apple_tv/translations/en.json index 0fe914c3d86..304a43363a0 100644 --- a/homeassistant/components/apple_tv/translations/en.json +++ b/homeassistant/components/apple_tv/translations/en.json @@ -16,7 +16,7 @@ "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", "unknown": "Unexpected error" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!", diff --git a/homeassistant/components/arcam_fmj/strings.json b/homeassistant/components/arcam_fmj/strings.json index b6a7d6ee559..154727baf9f 100644 --- a/homeassistant/components/arcam_fmj/strings.json +++ b/homeassistant/components/arcam_fmj/strings.json @@ -6,7 +6,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "error": {}, - "flow_title": "Arcam FMJ on {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?" diff --git a/homeassistant/components/arcam_fmj/translations/en.json b/homeassistant/components/arcam_fmj/translations/en.json index c770bf89e2d..20a71df9d67 100644 --- a/homeassistant/components/arcam_fmj/translations/en.json +++ b/homeassistant/components/arcam_fmj/translations/en.json @@ -5,7 +5,8 @@ "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect" }, - "flow_title": "Arcam FMJ on {host}", + "error": {}, + "flow_title": "{host}", "step": { "confirm": { "description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?" diff --git a/homeassistant/components/azure_devops/strings.json b/homeassistant/components/azure_devops/strings.json index a0e2bbf864a..8dfd203c84b 100644 --- a/homeassistant/components/azure_devops/strings.json +++ b/homeassistant/components/azure_devops/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/azure_devops/translations/en.json b/homeassistant/components/azure_devops/translations/en.json index b0f803778f8..66ee72007b9 100644 --- a/homeassistant/components/azure_devops/translations/en.json +++ b/homeassistant/components/azure_devops/translations/en.json @@ -9,7 +9,7 @@ "invalid_auth": "Invalid authentication", "project_error": "Could not get project info." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json index 9a3c261a34a..b179f0d097b 100644 --- a/homeassistant/components/blebox/strings.json +++ b/homeassistant/components/blebox/strings.json @@ -9,7 +9,7 @@ "unsupported_version": "BleBox device has outdated firmware. Please upgrade it first.", "unknown": "[%key:common::config_flow::error::unknown%]" }, - "flow_title": "BleBox device: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "description": "Set up your BleBox to integrate with Home Assistant.", diff --git a/homeassistant/components/blebox/translations/en.json b/homeassistant/components/blebox/translations/en.json index 7ff38e25343..d6f9f11498a 100644 --- a/homeassistant/components/blebox/translations/en.json +++ b/homeassistant/components/blebox/translations/en.json @@ -9,7 +9,7 @@ "unknown": "Unexpected error", "unsupported_version": "BleBox device has outdated firmware. Please upgrade it first." }, - "flow_title": "BleBox device: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/bond/strings.json b/homeassistant/components/bond/strings.json index f8eff6ddd9e..e923ded939e 100644 --- a/homeassistant/components/bond/strings.json +++ b/homeassistant/components/bond/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Bond: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "description": "Do you want to set up {name}?", diff --git a/homeassistant/components/bond/translations/en.json b/homeassistant/components/bond/translations/en.json index d9ce8ab0fe4..52f6f139987 100644 --- a/homeassistant/components/bond/translations/en.json +++ b/homeassistant/components/bond/translations/en.json @@ -9,7 +9,7 @@ "old_firmware": "Unsupported old firmware on the Bond device - please upgrade before continuing", "unknown": "Unexpected error" }, - "flow_title": "Bond: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json index 358f76e77b8..0b00a3b30cd 100644 --- a/homeassistant/components/brother/strings.json +++ b/homeassistant/components/brother/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Brother Printer: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "description": "Set up Brother printer integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/brother", @@ -27,4 +27,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/brother/translations/en.json b/homeassistant/components/brother/translations/en.json index 66c5db4282d..8ec9d84c7fb 100644 --- a/homeassistant/components/brother/translations/en.json +++ b/homeassistant/components/brother/translations/en.json @@ -9,7 +9,7 @@ "snmp_error": "SNMP server turned off or printer not supported.", "wrong_host": "Invalid hostname or IP address." }, - "flow_title": "Brother Printer: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/strings.json b/homeassistant/components/bsblan/strings.json index 0bb084fb20d..92c0d2b565a 100644 --- a/homeassistant/components/bsblan/strings.json +++ b/homeassistant/components/bsblan/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "title": "Connect to the BSB-Lan device", diff --git a/homeassistant/components/bsblan/translations/en.json b/homeassistant/components/bsblan/translations/en.json index 4aa8b881cb4..296986b341e 100644 --- a/homeassistant/components/bsblan/translations/en.json +++ b/homeassistant/components/bsblan/translations/en.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/canary/strings.json b/homeassistant/components/canary/strings.json index 504a5dc2ac1..9555756deff 100644 --- a/homeassistant/components/canary/strings.json +++ b/homeassistant/components/canary/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "title": "Connect to Canary", diff --git a/homeassistant/components/canary/translations/en.json b/homeassistant/components/canary/translations/en.json index 1e04d1825f3..e4408a88231 100644 --- a/homeassistant/components/canary/translations/en.json +++ b/homeassistant/components/canary/translations/en.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/cloudflare/strings.json b/homeassistant/components/cloudflare/strings.json index caf3da68876..bdadfde4800 100644 --- a/homeassistant/components/cloudflare/strings.json +++ b/homeassistant/components/cloudflare/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "user": { "title": "Connect to Cloudflare", diff --git a/homeassistant/components/cloudflare/translations/en.json b/homeassistant/components/cloudflare/translations/en.json index 4016e22d8ee..3dcd60ac4a9 100644 --- a/homeassistant/components/cloudflare/translations/en.json +++ b/homeassistant/components/cloudflare/translations/en.json @@ -9,7 +9,7 @@ "invalid_auth": "Invalid authentication", "invalid_zone": "Invalid zone" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index fbb321959c1..0fa929f9e63 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "deCONZ Zigbee gateway ({host})", + "flow_title": "{host}", "step": { "user": { "data": { diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json index 14ddb6890d4..a8e09fd20d7 100644 --- a/homeassistant/components/deconz/translations/en.json +++ b/homeassistant/components/deconz/translations/en.json @@ -11,7 +11,7 @@ "error": { "no_key": "Couldn't get an API key" }, - "flow_title": "deCONZ Zigbee gateway ({host})", + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the add-on {addon}?", diff --git a/homeassistant/components/denonavr/strings.json b/homeassistant/components/denonavr/strings.json index 5e5c7665a47..75deafee776 100644 --- a/homeassistant/components/denonavr/strings.json +++ b/homeassistant/components/denonavr/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Denon AVR Network Receiver: {name}", + "flow_title": "{name}", "step": { "user": { "title": "Denon AVR Network Receivers", diff --git a/homeassistant/components/denonavr/translations/en.json b/homeassistant/components/denonavr/translations/en.json index a538dad62b9..7adcd0e2412 100644 --- a/homeassistant/components/denonavr/translations/en.json +++ b/homeassistant/components/denonavr/translations/en.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Failed to discover a Denon AVR Network Receiver" }, - "flow_title": "Denon AVR Network Receiver: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Please confirm adding the receiver", diff --git a/homeassistant/components/directv/strings.json b/homeassistant/components/directv/strings.json index 9e30a366cc0..e6c54d0d4aa 100644 --- a/homeassistant/components/directv/strings.json +++ b/homeassistant/components/directv/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "data": {}, diff --git a/homeassistant/components/directv/translations/en.json b/homeassistant/components/directv/translations/en.json index 0275d50d8fc..118c693c891 100644 --- a/homeassistant/components/directv/translations/en.json +++ b/homeassistant/components/directv/translations/en.json @@ -7,9 +7,10 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { + "data": {}, "description": "Do you want to set up {name}?" }, "user": { diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index 076124cf095..e710c587d5d 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -26,7 +26,7 @@ "link_local_address": "Link local addresses are not supported", "not_doorbird_device": "This device is not a DoorBird" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]", diff --git a/homeassistant/components/doorbird/translations/en.json b/homeassistant/components/doorbird/translations/en.json index 8ddf95f3773..db1cea2d73f 100644 --- a/homeassistant/components/doorbird/translations/en.json +++ b/homeassistant/components/doorbird/translations/en.json @@ -10,7 +10,7 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/strings.json b/homeassistant/components/elgato/strings.json index 577ed6c0206..fc0007ac301 100644 --- a/homeassistant/components/elgato/strings.json +++ b/homeassistant/components/elgato/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Elgato Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "description": "Set up your Elgato Light to integrate with Home Assistant.", diff --git a/homeassistant/components/elgato/translations/en.json b/homeassistant/components/elgato/translations/en.json index fc75c20032c..dc7f551607e 100644 --- a/homeassistant/components/elgato/translations/en.json +++ b/homeassistant/components/elgato/translations/en.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "Elgato Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/emonitor/strings.json b/homeassistant/components/emonitor/strings.json index aac15dfaae2..9f62efc4d8c 100644 --- a/homeassistant/components/emonitor/strings.json +++ b/homeassistant/components/emonitor/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/emonitor/translations/en.json b/homeassistant/components/emonitor/translations/en.json index 6e24bbac7a3..de153509dde 100644 --- a/homeassistant/components/emonitor/translations/en.json +++ b/homeassistant/components/emonitor/translations/en.json @@ -7,7 +7,7 @@ "cannot_connect": "Failed to connect", "unknown": "Unexpected error" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Do you want to setup {name} ({host})?", diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 1af58a32fa7..b42f6bfb50f 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { @@ -20,4 +20,4 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json index 58c69e90eef..2cdb75a6b53 100644 --- a/homeassistant/components/enphase_envoy/translations/en.json +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -9,7 +9,7 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 88072a4d241..6d1c9a91e3d 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -28,6 +28,6 @@ "title": "Discovered ESPHome node" } }, - "flow_title": "ESPHome: {name}" + "flow_title": "{name}" } } diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index d70c984b532..c57c9d1acb0 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -9,7 +9,7 @@ "invalid_auth": "Invalid authentication", "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/forked_daapd/strings.json b/homeassistant/components/forked_daapd/strings.json index d0523073699..671538210ff 100644 --- a/homeassistant/components/forked_daapd/strings.json +++ b/homeassistant/components/forked_daapd/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "forked-daapd server: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "title": "Set up forked-daapd device", diff --git a/homeassistant/components/forked_daapd/translations/en.json b/homeassistant/components/forked_daapd/translations/en.json index 4a5426e6f83..cf7a1e5281b 100644 --- a/homeassistant/components/forked_daapd/translations/en.json +++ b/homeassistant/components/forked_daapd/translations/en.json @@ -12,7 +12,7 @@ "wrong_password": "Incorrect password.", "wrong_server_type": "The forked-daapd integration requires a forked-daapd server with version >= 27.0." }, - "flow_title": "forked-daapd server: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 3a94d39a50c..3f6cb4adba4 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "FRITZ!Box Tools: {name}", + "flow_title": "{name}", "step": { "confirm": { "title": "Setup FRITZ!Box Tools", diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index 7497383dcfc..de1b61763f6 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -11,7 +11,7 @@ "connection_error": "Failed to connect", "invalid_auth": "Invalid authentication" }, - "flow_title": "FRITZ!Box Tools: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index 50b86611814..336671fd7a8 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "AVM FRITZ!SmartHome: {name}", + "flow_title": "{name}", "step": { "user": { "description": "Enter your AVM FRITZ!Box information.", @@ -36,4 +36,4 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/fritzbox/translations/en.json b/homeassistant/components/fritzbox/translations/en.json index 1988dcde1a4..5eb34096da0 100644 --- a/homeassistant/components/fritzbox/translations/en.json +++ b/homeassistant/components/fritzbox/translations/en.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Invalid authentication" }, - "flow_title": "AVM FRITZ!SmartHome: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/strings.json b/homeassistant/components/fritzbox_callmonitor/strings.json index d0325a07637..6b2fa2943f9 100644 --- a/homeassistant/components/fritzbox_callmonitor/strings.json +++ b/homeassistant/components/fritzbox_callmonitor/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "AVM FRITZ!Box call monitor: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/en.json b/homeassistant/components/fritzbox_callmonitor/translations/en.json index 286bed1d5bc..3a62053cad7 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/en.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/en.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Invalid authentication" }, - "flow_title": "AVM FRITZ!Box call monitor: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json index f953639e22d..1a156ca863c 100644 --- a/homeassistant/components/harmony/strings.json +++ b/homeassistant/components/harmony/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "user": { "title": "Setup Logitech Harmony Hub", diff --git a/homeassistant/components/harmony/translations/en.json b/homeassistant/components/harmony/translations/en.json index 0e4243ae49c..dbc0dee16c5 100644 --- a/homeassistant/components/harmony/translations/en.json +++ b/homeassistant/components/harmony/translations/en.json @@ -7,7 +7,7 @@ "cannot_connect": "Failed to connect", "unknown": "Unexpected error" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "Do you want to setup {name} ({host})?", diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 3c6b8405e31..d170693bb6f 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -1,7 +1,7 @@ { "title": "HomeKit Controller", "config": { - "flow_title": "{name} via HomeKit Accessory Protocol", + "flow_title": "{name}", "step": { "user": { "title": "Device selection", diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index 90d4356e40f..67409ed02cf 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -17,7 +17,7 @@ "unable_to_pair": "Unable to pair, please try again.", "unknown_error": "Device reported an unknown error. Pairing failed." }, - "flow_title": "{name} via HomeKit Accessory Protocol", + "flow_title": "{name}", "step": { "busy_error": { "description": "Abort pairing on all controllers, or try restarting the device, then continue to resume pairing.", diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 5cff2165dc3..9cfa49604ae 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -15,7 +15,7 @@ "response_error": "Unknown error from device", "unknown": "[%key:common::config_flow::error::unknown%]" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 36e99b3420c..61b8a2e50d1 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -15,7 +15,7 @@ "response_error": "Unknown error from device", "unknown": "Unexpected error" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { @@ -34,7 +34,6 @@ "data": { "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", - "track_new_devices": "Track new devices", "track_wired_clients": "Track wired network clients" } } diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index b61c89f6df1..c43b9a25463 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Printer: {name}", + "flow_title": "{name}", "step": { "user": { "title": "Link your printer", diff --git a/homeassistant/components/ipp/translations/en.json b/homeassistant/components/ipp/translations/en.json index 69bc0b091ab..a8bfba5ac32 100644 --- a/homeassistant/components/ipp/translations/en.json +++ b/homeassistant/components/ipp/translations/en.json @@ -13,7 +13,7 @@ "cannot_connect": "Failed to connect", "connection_upgrade": "Failed to connect to printer. Please try again with SSL/TLS option checked." }, - "flow_title": "Printer: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index b08b40ac0c5..08092c2482c 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Universal Devices ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json index a2c71fbfd95..724160c6d3d 100644 --- a/homeassistant/components/isy994/translations/en.json +++ b/homeassistant/components/isy994/translations/en.json @@ -9,7 +9,7 @@ "invalid_host": "The host entry was not in full URL format, e.g., http://192.168.10.100:80", "unknown": "Unexpected error" }, - "flow_title": "Universal Devices ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/kodi/strings.json b/homeassistant/components/kodi/strings.json index f1bb5342903..6315fffb193 100644 --- a/homeassistant/components/kodi/strings.json +++ b/homeassistant/components/kodi/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "user": { "description": "Kodi connection information. Please make sure to enable \"Allow control of Kodi via HTTP\" in System/Settings/Network/Services.", diff --git a/homeassistant/components/kodi/translations/en.json b/homeassistant/components/kodi/translations/en.json index be6af6a7f91..c9a81c3fc69 100644 --- a/homeassistant/components/kodi/translations/en.json +++ b/homeassistant/components/kodi/translations/en.json @@ -12,7 +12,7 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json index 9464523fcce..a89b0c4bbce 100644 --- a/homeassistant/components/lutron_caseta/strings.json +++ b/homeassistant/components/lutron_caseta/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Lutron Caséta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "title": "Failed to import Caséta bridge configuration.", diff --git a/homeassistant/components/lutron_caseta/translations/en.json b/homeassistant/components/lutron_caseta/translations/en.json index 2088cd52327..d5245dae2a4 100644 --- a/homeassistant/components/lutron_caseta/translations/en.json +++ b/homeassistant/components/lutron_caseta/translations/en.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "Couldn\u2019t setup bridge (host: {host}) imported from configuration.yaml.", diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index d922923d472..4511b316cd6 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -1,6 +1,5 @@ { "config": { - "flow_title": "Motion Blinds", "step": { "user": { "title": "Motion Blinds", diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 3a968bc6491..02716d73e84 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -8,7 +8,6 @@ "error": { "discovery_error": "Failed to discover a Motion Gateway" }, - "flow_title": "Motion Blinds", "step": { "connect": { "data": { @@ -26,7 +25,6 @@ }, "user": { "data": { - "api_key": "API Key", "host": "IP Address" }, "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", diff --git a/homeassistant/components/nightscout/strings.json b/homeassistant/components/nightscout/strings.json index 2240bcec02b..709788c5818 100644 --- a/homeassistant/components/nightscout/strings.json +++ b/homeassistant/components/nightscout/strings.json @@ -1,6 +1,5 @@ { "config": { - "flow_title": "Nightscout", "step": { "user": { "title": "Enter your Nightscout server information.", diff --git a/homeassistant/components/nightscout/translations/en.json b/homeassistant/components/nightscout/translations/en.json index d8b4c441283..af29cff5b56 100644 --- a/homeassistant/components/nightscout/translations/en.json +++ b/homeassistant/components/nightscout/translations/en.json @@ -8,7 +8,6 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/strings.json b/homeassistant/components/nzbget/strings.json index 96049ea9369..fc7d8508a12 100644 --- a/homeassistant/components/nzbget/strings.json +++ b/homeassistant/components/nzbget/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "title": "Connect to NZBGet", diff --git a/homeassistant/components/nzbget/translations/en.json b/homeassistant/components/nzbget/translations/en.json index b46c7b1d799..76b42126ff2 100644 --- a/homeassistant/components/nzbget/translations/en.json +++ b/homeassistant/components/nzbget/translations/en.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ovo_energy/strings.json b/homeassistant/components/ovo_energy/strings.json index df19d4898f2..87605e2b3c4 100644 --- a/homeassistant/components/ovo_energy/strings.json +++ b/homeassistant/components/ovo_energy/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", diff --git a/homeassistant/components/ovo_energy/translations/en.json b/homeassistant/components/ovo_energy/translations/en.json index 7b3160af97e..3539d91220d 100644 --- a/homeassistant/components/ovo_energy/translations/en.json +++ b/homeassistant/components/ovo_energy/translations/en.json @@ -5,7 +5,7 @@ "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 2ed6721bab3..e5a7ab5a6a9 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -37,6 +37,6 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }, - "flow_title": "Smile: {name}" + "flow_title": "{name}" } } diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 7f2f20e1947..3ee5551fd83 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -8,7 +8,7 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index 5deacd6a8f9..e1b2f2dbd3b 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "title": "Connect to the powerwall", diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json index 06fc09804d9..3be711d94c5 100644 --- a/homeassistant/components/powerwall/translations/en.json +++ b/homeassistant/components/powerwall/translations/en.json @@ -10,7 +10,7 @@ "unknown": "Unexpected error", "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index ec65d1c7c09..7634c0a69c5 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "RainMachine {ip}", + "flow_title": "{ip}", "step": { "user": { "title": "Fill in your information", diff --git a/homeassistant/components/rainmachine/translations/en.json b/homeassistant/components/rainmachine/translations/en.json index 0c8b6eef766..9369eeae4c8 100644 --- a/homeassistant/components/rainmachine/translations/en.json +++ b/homeassistant/components/rainmachine/translations/en.json @@ -6,7 +6,7 @@ "error": { "invalid_auth": "Invalid authentication" }, - "flow_title": "RainMachine {ip}", + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 3523615ff33..235cf4ad159 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "user": { "description": "Enter your Roku information.", diff --git a/homeassistant/components/roku/translations/en.json b/homeassistant/components/roku/translations/en.json index 2b54cafe890..5d43a016afb 100644 --- a/homeassistant/components/roku/translations/en.json +++ b/homeassistant/components/roku/translations/en.json @@ -8,13 +8,10 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "Do you want to set up {name}?", - "title": "Roku" - }, - "ssdp_confirm": { + "data": {}, "description": "Do you want to set up {name}?", "title": "Roku" }, diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index 16371041a15..867d2bf633f 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "title": "Automatically connect to the device", diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index 5cc06f0cb5d..705706c7fd5 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { @@ -36,17 +36,6 @@ }, "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-` or `Roomba-`. Please follow the steps outlined in the documentation at: {auth_help_url}", "title": "Manually connect to the device" - }, - "user": { - "data": { - "blid": "BLID", - "continuous": "Continuous", - "delay": "Delay", - "host": "Host", - "password": "Password" - }, - "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "Connect to the device" } } }, diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index b326f2ab548..3854d040d3e 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Samsung TV: {model}", + "flow_title": "{model}", "step": { "user": { "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 5c84880380e..8f05775eb0c 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -7,7 +7,7 @@ "cannot_connect": "Failed to connect", "not_supported": "This Samsung TV device is currently not supported." }, - "flow_title": "Samsung TV: {model}", + "flow_title": "{model}", "step": { "confirm": { "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization. Manual configurations for this TV will be overwritten.", diff --git a/homeassistant/components/screenlogic/strings.json b/homeassistant/components/screenlogic/strings.json index 155eeb3043e..8a9ec196c91 100644 --- a/homeassistant/components/screenlogic/strings.json +++ b/homeassistant/components/screenlogic/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, @@ -36,4 +36,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/screenlogic/translations/en.json b/homeassistant/components/screenlogic/translations/en.json index 2572fdf38fa..5d1eabed5d3 100644 --- a/homeassistant/components/screenlogic/translations/en.json +++ b/homeassistant/components/screenlogic/translations/en.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/smappee/strings.json b/homeassistant/components/smappee/strings.json index 25fa2846117..58abeb57186 100644 --- a/homeassistant/components/smappee/strings.json +++ b/homeassistant/components/smappee/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "description": "Set up your Smappee to integrate with Home Assistant.", diff --git a/homeassistant/components/smappee/translations/en.json b/homeassistant/components/smappee/translations/en.json index 9220b1c3ec4..1e6241ba5c6 100644 --- a/homeassistant/components/smappee/translations/en.json +++ b/homeassistant/components/smappee/translations/en.json @@ -9,7 +9,7 @@ "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/somfy_mylink/strings.json b/homeassistant/components/somfy_mylink/strings.json index ca3d83e402b..81fb9b78c74 100644 --- a/homeassistant/components/somfy_mylink/strings.json +++ b/homeassistant/components/somfy_mylink/strings.json @@ -1,7 +1,6 @@ { - "title": "Somfy MyLink", "config": { - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service.", @@ -41,4 +40,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/somfy_mylink/translations/en.json b/homeassistant/components/somfy_mylink/translations/en.json index 13115b36e5c..e646bf9abcf 100644 --- a/homeassistant/components/somfy_mylink/translations/en.json +++ b/homeassistant/components/somfy_mylink/translations/en.json @@ -8,7 +8,7 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { @@ -25,17 +25,8 @@ "cannot_connect": "Failed to connect" }, "step": { - "entity_config": { - "data": { - "reverse": "Cover is reversed" - }, - "description": "Configure options for `{entity_id}`", - "title": "Configure Entity" - }, "init": { "data": { - "default_reverse": "Default reversal status for unconfigured covers", - "entity_id": "Configure a specific entity.", "target_id": "Configure options for a cover." }, "title": "Configure MyLink Options" @@ -48,6 +39,5 @@ "title": "Configure MyLink Cover" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/sonarr/strings.json b/homeassistant/components/sonarr/strings.json index 2b9a795b8a7..2281b6cec57 100644 --- a/homeassistant/components/sonarr/strings.json +++ b/homeassistant/components/sonarr/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/en.json b/homeassistant/components/sonarr/translations/en.json index 638b9a1668a..74232e9f8b7 100644 --- a/homeassistant/components/sonarr/translations/en.json +++ b/homeassistant/components/sonarr/translations/en.json @@ -9,7 +9,7 @@ "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "The Sonarr integration needs to be manually re-authenticated with the Sonarr API hosted at: {host}", diff --git a/homeassistant/components/songpal/strings.json b/homeassistant/components/songpal/strings.json index 65c42ddef6a..62bff00c786 100644 --- a/homeassistant/components/songpal/strings.json +++ b/homeassistant/components/songpal/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/songpal/translations/en.json b/homeassistant/components/songpal/translations/en.json index a44ba6e8668..50e7b5d9a4a 100644 --- a/homeassistant/components/songpal/translations/en.json +++ b/homeassistant/components/songpal/translations/en.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "Do you want to set up {name} ({host})?" diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index afe68d01947..4ae8d69bacd 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "user": { "data": { diff --git a/homeassistant/components/squeezebox/translations/en.json b/homeassistant/components/squeezebox/translations/en.json index 614cb07af77..c1d43148b19 100644 --- a/homeassistant/components/squeezebox/translations/en.json +++ b/homeassistant/components/squeezebox/translations/en.json @@ -10,7 +10,7 @@ "no_server_found": "Could not automatically discover server.", "unknown": "Unexpected error" }, - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/syncthru/strings.json b/homeassistant/components/syncthru/strings.json index 67f50e84a98..c4087bdee04 100644 --- a/homeassistant/components/syncthru/strings.json +++ b/homeassistant/components/syncthru/strings.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "Device does not support SyncThru", "unknown_state": "Printer state unknown, verify URL and network connectivity" }, - "flow_title": "Samsung SyncThru Printer: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/syncthru/translations/en.json b/homeassistant/components/syncthru/translations/en.json index 7c5ce0f13dc..149e3cefe00 100644 --- a/homeassistant/components/syncthru/translations/en.json +++ b/homeassistant/components/syncthru/translations/en.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "Device does not support SyncThru", "unknown_state": "Printer state unknown, verify URL and network connectivity" }, - "flow_title": "Samsung SyncThru Printer: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index 91933571028..1464b8a6a06 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "title": "Synology DSM", diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index 1501aa89485..397bad8b14e 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -10,7 +10,7 @@ "otp_failed": "Two-step authentication failed, retry with a new pass code", "unknown": "Unexpected error" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/system_bridge/strings.json b/homeassistant/components/system_bridge/strings.json index eeaad92fd1b..209bce9078a 100644 --- a/homeassistant/components/system_bridge/strings.json +++ b/homeassistant/components/system_bridge/strings.json @@ -1,12 +1,11 @@ { - "title": "System Bridge", "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, - "flow_title": "System Bridge: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/system_bridge/translations/en.json b/homeassistant/components/system_bridge/translations/en.json index 71d10c54476..dc94dfe2ac6 100644 --- a/homeassistant/components/system_bridge/translations/en.json +++ b/homeassistant/components/system_bridge/translations/en.json @@ -10,7 +10,7 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "flow_title": "System Bridge: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { @@ -27,6 +27,5 @@ "description": "Please enter your connection details." } } - }, - "title": "System Bridge" + } } \ No newline at end of file diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 4ce02139529..61ea46c6a9f 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -1,6 +1,5 @@ { "config": { - "flow_title": "Tuya configuration", "step": { "user": { "title": "Tuya", diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index ee304ff30cd..8fa5af38f92 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -8,7 +8,6 @@ "error": { "invalid_auth": "Invalid authentication" }, - "flow_title": "Tuya configuration", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index be0bda37971..d625ff79117 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "UniFi Network {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "title": "Set up UniFi Controller", diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index 41507faa430..508e908b14a 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -10,7 +10,7 @@ "service_unavailable": "Failed to connect", "unknown_client_mac": "No client available on that MAC address" }, - "flow_title": "UniFi Network {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index f6dd559ebd5..eee840381e1 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -1,6 +1,6 @@ { "domain": "upnp", - "name": "UPnP", + "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": ["async-upnp-client==0.17.0"], diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index 97e91c490e3..e68a9a9eae5 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "init": { }, diff --git a/homeassistant/components/upnp/translations/en.json b/homeassistant/components/upnp/translations/en.json index 1476c87e51f..83741bd3195 100644 --- a/homeassistant/components/upnp/translations/en.json +++ b/homeassistant/components/upnp/translations/en.json @@ -5,8 +5,9 @@ "incomplete_discovery": "Incomplete discovery", "no_devices_found": "No devices found on the network" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { + "init": {}, "ssdp_confirm": { "description": "Do you want to set up this UPnP/IGD device?" }, diff --git a/homeassistant/components/wilight/strings.json b/homeassistant/components/wilight/strings.json index 710543a5a53..e267a8e5327 100644 --- a/homeassistant/components/wilight/strings.json +++ b/homeassistant/components/wilight/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "title": "WiLight", diff --git a/homeassistant/components/wilight/translations/en.json b/homeassistant/components/wilight/translations/en.json index 14724af4ae7..3d3a83a0270 100644 --- a/homeassistant/components/wilight/translations/en.json +++ b/homeassistant/components/wilight/translations/en.json @@ -5,7 +5,7 @@ "not_supported_device": "This WiLight is currently not supported", "not_wilight_device": "This Device is not WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Do you want to set up WiLight {name}?\n\n It supports: {components}", diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index db18bb0f334..81b0bbb79b5 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "profile": { "title": "User Profile.", diff --git a/homeassistant/components/withings/translations/en.json b/homeassistant/components/withings/translations/en.json index 45d1a642da3..e8acc8c3440 100644 --- a/homeassistant/components/withings/translations/en.json +++ b/homeassistant/components/withings/translations/en.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Account is already configured" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Pick Authentication Method" diff --git a/homeassistant/components/wled/strings.json b/homeassistant/components/wled/strings.json index f60a4bf3563..c42a6cdffb1 100644 --- a/homeassistant/components/wled/strings.json +++ b/homeassistant/components/wled/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "description": "Set up your WLED to integrate with Home Assistant.", diff --git a/homeassistant/components/wled/translations/en.json b/homeassistant/components/wled/translations/en.json index 073db955f93..8ebf6f4d91b 100644 --- a/homeassistant/components/wled/translations/en.json +++ b/homeassistant/components/wled/translations/en.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json index a2c8a226c95..b1675992174 100644 --- a/homeassistant/components/xiaomi_aqara/strings.json +++ b/homeassistant/components/xiaomi_aqara/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "user": { "title": "Xiaomi Aqara Gateway", diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json index d51687a0790..53776111d37 100644 --- a/homeassistant/components/xiaomi_aqara/translations/en.json +++ b/homeassistant/components/xiaomi_aqara/translations/en.json @@ -12,7 +12,7 @@ "invalid_key": "Invalid gateway key", "invalid_mac": "Invalid Mac Address" }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index e3d9376bc31..571df98eef1 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -8,7 +8,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown_device": "The device model is not known, not able to setup the device using config flow." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index 3d893ade2f0..bb1485d9fde 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -6,36 +6,18 @@ }, "error": { "cannot_connect": "Failed to connect", - "no_device_selected": "No device selected, please select one device.", "unknown_device": "The device model is not known, not able to setup the device using config flow." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { "host": "IP Address", "model": "Device model (Optional)", - "name": "Name of the device", "token": "API Token" }, "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "IP Address", - "name": "Name of the Gateway", - "token": "API Token" - }, - "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Connect to a Xiaomi Gateway" - }, - "description": "Select to which device you want to connect.", - "title": "Xiaomi Miio" } } } diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 2c2a93aa4d6..9ca6a9821b3 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "user": { "title": "ZHA", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 2a6aa34d886..83dc56c75fc 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { From 973f59e42305fba73ec3da5986eb3639afa373c9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 May 2021 09:21:57 +0200 Subject: [PATCH 314/852] Refactor history component (#50287) * Refactor history component * Update tests * Address review comments * Correct deprecated functions --- homeassistant/components/filter/manifest.json | 2 +- homeassistant/components/filter/sensor.py | 2 +- homeassistant/components/history/__init__.py | 523 ++---------------- .../components/history_stats/manifest.json | 2 +- .../components/history_stats/sensor.py | 2 +- homeassistant/components/recorder/__init__.py | 3 +- homeassistant/components/recorder/history.py | 403 ++++++++++++++ homeassistant/components/recorder/models.py | 113 ++++ tests/components/filter/test_sensor.py | 12 +- tests/components/history/test_init.py | 171 +----- tests/components/history_stats/test_sensor.py | 13 +- tests/components/recorder/test_history.py | 432 +++++++++++++++ 12 files changed, 1008 insertions(+), 670 deletions(-) create mode 100644 homeassistant/components/recorder/history.py create mode 100644 tests/components/recorder/test_history.py diff --git a/homeassistant/components/filter/manifest.json b/homeassistant/components/filter/manifest.json index d8ca603c5a9..248a62bcfa4 100644 --- a/homeassistant/components/filter/manifest.json +++ b/homeassistant/components/filter/manifest.json @@ -2,7 +2,7 @@ "domain": "filter", "name": "Filter", "documentation": "https://www.home-assistant.io/integrations/filter", - "dependencies": ["history"], + "dependencies": ["recorder"], "codeowners": ["@dgomes"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 2f1705f5f4d..e303dc1cf96 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -11,9 +11,9 @@ import statistics import voluptuous as vol -from homeassistant.components import history from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN +from homeassistant.components.recorder import history from homeassistant.components.sensor import ( DEVICE_CLASSES as SENSOR_DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN, diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 35be51a99d9..ac089bbb3b3 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,28 +1,20 @@ """Provide pre-made queries on top of the recorder component.""" from __future__ import annotations -from collections import defaultdict from collections.abc import Iterable from datetime import datetime as dt, timedelta -from itertools import groupby -import json import logging import time from typing import cast from aiohttp import web -from sqlalchemy import and_, bindparam, func, not_, or_ -from sqlalchemy.ext import baked +from sqlalchemy import not_, or_ import voluptuous as vol -from homeassistant.components import recorder from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder.models import ( - States, - process_timestamp, - process_timestamp_to_utc_isoformat, -) -from homeassistant.components.recorder.util import execute, session_scope +from homeassistant.components.recorder import history +from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( CONF_DOMAINS, CONF_ENTITIES, @@ -30,8 +22,9 @@ from homeassistant.const import ( CONF_INCLUDE, HTTP_BAD_REQUEST, ) -from homeassistant.core import Context, HomeAssistant, State, split_entity_id +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.deprecation import deprecated_function from homeassistant.helpers.entityfilter import ( CONF_ENTITY_GLOBS, INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -45,9 +38,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "history" CONF_ORDER = "use_include_order" -STATE_KEY = "state" -LAST_CHANGED_KEY = "last_changed" - GLOB_TO_SQL_CHARS = { 42: "%", # * 46: "_", # . @@ -62,375 +52,41 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SIGNIFICANT_DOMAINS = ( - "climate", - "device_tracker", - "humidifier", - "thermostat", - "water_heater", -) -IGNORE_DOMAINS = ("zone", "scene") -NEED_ATTRIBUTE_DOMAINS = { - "climate", - "humidifier", - "input_datetime", - "thermostat", - "water_heater", -} - -QUERY_STATES = [ - States.domain, - States.entity_id, - States.state, - States.attributes, - States.last_changed, - States.last_updated, -] - -HISTORY_BAKERY = "history_bakery" - +@deprecated_function("homeassistant.components.recorder.history.get_significant_states") def get_significant_states(hass, *args, **kwargs): """Wrap _get_significant_states with a sql session.""" - with session_scope(hass=hass) as session: - return _get_significant_states(hass, session, *args, **kwargs) - - -def _get_significant_states( - hass, - session, - start_time, - end_time=None, - entity_ids=None, - filters=None, - include_start_time_state=True, - significant_changes_only=True, - minimal_response=False, -): - """ - Return states changes during UTC period start_time - end_time. - - Significant states are all states where there is a state change, - as well as all states from certain domains (for instance - thermostat so that we get current temperature in our graphs). - """ - timer_start = time.perf_counter() - - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) - - if significant_changes_only: - baked_query += lambda q: q.filter( - ( - States.domain.in_(SIGNIFICANT_DOMAINS) - | (States.last_changed == States.last_updated) - ) - & (States.last_updated > bindparam("start_time")) - ) - else: - baked_query += lambda q: q.filter(States.last_updated > bindparam("start_time")) - - if entity_ids is not None: - baked_query += lambda q: q.filter( - States.entity_id.in_(bindparam("entity_ids", expanding=True)) - ) - else: - baked_query += lambda q: q.filter(~States.domain.in_(IGNORE_DOMAINS)) - if filters: - filters.bake(baked_query) - - if end_time is not None: - baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) - - baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) - - states = execute( - baked_query(session).params( - start_time=start_time, end_time=end_time, entity_ids=entity_ids - ) - ) - - if _LOGGER.isEnabledFor(logging.DEBUG): - elapsed = time.perf_counter() - timer_start - _LOGGER.debug("get_significant_states took %fs", elapsed) - - return _sorted_states_to_json( - hass, - session, - states, - start_time, - entity_ids, - filters, - include_start_time_state, - minimal_response, - ) + return history.get_significant_states(hass, *args, **kwargs) +@deprecated_function( + "homeassistant.components.recorder.history.state_changes_during_period" +) def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): """Return states changes during UTC period start_time - end_time.""" - with session_scope(hass=hass) as session: - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) - - baked_query += lambda q: q.filter( - (States.last_changed == States.last_updated) - & (States.last_updated > bindparam("start_time")) - ) - - if end_time is not None: - baked_query += lambda q: q.filter( - States.last_updated < bindparam("end_time") - ) - - if entity_id is not None: - baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) - entity_id = entity_id.lower() - - baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) - - states = execute( - baked_query(session).params( - start_time=start_time, end_time=end_time, entity_id=entity_id - ) - ) - - entity_ids = [entity_id] if entity_id is not None else None - - return _sorted_states_to_json(hass, session, states, start_time, entity_ids) + return history.state_changes_during_period( + hass, start_time, end_time=None, entity_id=None + ) +@deprecated_function("homeassistant.components.recorder.history.get_last_state_changes") def get_last_state_changes(hass, number_of_states, entity_id): """Return the last number_of_states.""" - start_time = dt_util.utcnow() - - with session_scope(hass=hass) as session: - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) - baked_query += lambda q: q.filter(States.last_changed == States.last_updated) - - if entity_id is not None: - baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) - entity_id = entity_id.lower() - - baked_query += lambda q: q.order_by( - States.entity_id, States.last_updated.desc() - ) - - baked_query += lambda q: q.limit(bindparam("number_of_states")) - - states = execute( - baked_query(session).params( - number_of_states=number_of_states, entity_id=entity_id - ) - ) - - entity_ids = [entity_id] if entity_id is not None else None - - return _sorted_states_to_json( - hass, - session, - reversed(states), - start_time, - entity_ids, - include_start_time_state=False, - ) + return history.get_last_state_changes(hass, number_of_states, entity_id) +@deprecated_function("homeassistant.components.recorder.history.get_states") def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): """Return the states at a specific point in time.""" - if run is None: - run = recorder.run_information_from_instance(hass, utc_point_in_time) - - # History did not run before utc_point_in_time - if run is None: - return [] - - with session_scope(hass=hass) as session: - return _get_states_with_session( - hass, session, utc_point_in_time, entity_ids, run, filters - ) - - -def _get_states_with_session( - hass, session, utc_point_in_time, entity_ids=None, run=None, filters=None -): - """Return the states at a specific point in time.""" - if entity_ids and len(entity_ids) == 1: - return _get_single_entity_states_with_session( - hass, session, utc_point_in_time, entity_ids[0] - ) - - if run is None: - run = recorder.run_information_with_session(session, utc_point_in_time) - - # History did not run before utc_point_in_time - if run is None: - return [] - - # We have more than one entity to look at (most commonly we want - # all entities,) so we need to do a search on all states since the - # last recorder run started. - query = session.query(*QUERY_STATES) - - most_recent_states_by_date = session.query( - States.entity_id.label("max_entity_id"), - func.max(States.last_updated).label("max_last_updated"), - ).filter( - (States.last_updated >= run.start) & (States.last_updated < utc_point_in_time) + return history.get_states( + hass, utc_point_in_time, entity_ids=None, run=None, filters=None ) - if entity_ids: - most_recent_states_by_date.filter(States.entity_id.in_(entity_ids)) - - most_recent_states_by_date = most_recent_states_by_date.group_by(States.entity_id) - - most_recent_states_by_date = most_recent_states_by_date.subquery() - - most_recent_state_ids = session.query( - func.max(States.state_id).label("max_state_id") - ).join( - most_recent_states_by_date, - and_( - States.entity_id == most_recent_states_by_date.c.max_entity_id, - States.last_updated == most_recent_states_by_date.c.max_last_updated, - ), - ) - - most_recent_state_ids = most_recent_state_ids.group_by(States.entity_id) - - most_recent_state_ids = most_recent_state_ids.subquery() - - query = query.join( - most_recent_state_ids, - States.state_id == most_recent_state_ids.c.max_state_id, - ) - - if entity_ids is not None: - query = query.filter(States.entity_id.in_(entity_ids)) - else: - query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) - if filters: - query = filters.apply(query) - - return [LazyState(row) for row in execute(query)] - - -def _get_single_entity_states_with_session(hass, session, utc_point_in_time, entity_id): - # Use an entirely different (and extremely fast) query if we only - # have a single entity id - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) - baked_query += lambda q: q.filter( - States.last_updated < bindparam("utc_point_in_time"), - States.entity_id == bindparam("entity_id"), - ) - baked_query += lambda q: q.order_by(States.last_updated.desc()) - baked_query += lambda q: q.limit(1) - - query = baked_query(session).params( - utc_point_in_time=utc_point_in_time, entity_id=entity_id - ) - - return [LazyState(row) for row in execute(query)] - - -def _sorted_states_to_json( - hass, - session, - states, - start_time, - entity_ids, - filters=None, - include_start_time_state=True, - minimal_response=False, -): - """Convert SQL results into JSON friendly data structure. - - This takes our state list and turns it into a JSON friendly data - structure {'entity_id': [list of states], 'entity_id2': [list of states]} - - States must be sorted by entity_id and last_updated - - We also need to go back and create a synthetic zero data point for - each list of states, otherwise our graphs won't start on the Y - axis correctly. - """ - result = defaultdict(list) - # Set all entity IDs to empty lists in result set to maintain the order - if entity_ids is not None: - for ent_id in entity_ids: - result[ent_id] = [] - - # Get the states at the start time - timer_start = time.perf_counter() - if include_start_time_state: - run = recorder.run_information_from_instance(hass, start_time) - for state in _get_states_with_session( - hass, session, start_time, entity_ids, run=run, filters=filters - ): - state.last_changed = start_time - state.last_updated = start_time - result[state.entity_id].append(state) - - if _LOGGER.isEnabledFor(logging.DEBUG): - elapsed = time.perf_counter() - timer_start - _LOGGER.debug("getting %d first datapoints took %fs", len(result), elapsed) - - # Called in a tight loop so cache the function - # here - _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat - - # Append all changes to it - for ent_id, group in groupby(states, lambda state: state.entity_id): - domain = split_entity_id(ent_id)[0] - ent_results = result[ent_id] - if not minimal_response or domain in NEED_ATTRIBUTE_DOMAINS: - ent_results.extend(LazyState(db_state) for db_state in group) - - # With minimal response we only provide a native - # State for the first and last response. All the states - # in-between only provide the "state" and the - # "last_changed". - if not ent_results: - ent_results.append(LazyState(next(group))) - - prev_state = ent_results[-1] - initial_state_count = len(ent_results) - - for db_state in group: - # With minimal response we do not care about attribute - # changes so we can filter out duplicate states - if db_state.state == prev_state.state: - continue - - ent_results.append( - { - STATE_KEY: db_state.state, - LAST_CHANGED_KEY: _process_timestamp_to_utc_isoformat( - db_state.last_changed - ), - } - ) - prev_state = db_state - - if prev_state and len(ent_results) != initial_state_count: - # There was at least one state change - # replace the last minimal state with - # a full state - ent_results[-1] = LazyState(prev_state) - - # Filter out the empty lists if some states had 0 results. - return {key: val for key, val in result.items() if val} - +@deprecated_function("homeassistant.components.recorder.history.get_state") def get_state(hass, utc_point_in_time, entity_id, run=None): """Return a state at a specific point in time.""" - states = get_states(hass, utc_point_in_time, (entity_id,), run) - return states[0] if states else None + return history.get_state(hass, utc_point_in_time, entity_id, run=None) async def async_setup(hass, config): @@ -439,8 +95,6 @@ async def async_setup(hass, config): filters = sqlalchemy_filter_from_include_exclude_conf(conf) - hass.data[HISTORY_BAKERY] = baked.bakery() - use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) @@ -542,16 +196,18 @@ class HistoryPeriodView(HomeAssistantView): timer_start = time.perf_counter() with session_scope(hass=hass) as session: - result = _get_significant_states( - hass, - session, - start_time, - end_time, - entity_ids, - self.filters, - include_start_time_state, - significant_changes_only, - minimal_response, + result = ( + history._get_significant_states( # pylint: disable=protected-access + hass, + session, + start_time, + end_time, + entity_ids, + self.filters, + include_start_time_state, + significant_changes_only, + minimal_response, + ) ) result = list(result.values()) @@ -683,116 +339,3 @@ def _entities_may_have_state_changes_after( return True return False - - -class LazyState(State): - """A lazy version of core State.""" - - __slots__ = [ - "_row", - "entity_id", - "state", - "_attributes", - "_last_changed", - "_last_updated", - "_context", - ] - - def __init__(self, row): # pylint: disable=super-init-not-called - """Init the lazy state.""" - self._row = row - self.entity_id = self._row.entity_id - self.state = self._row.state or "" - self._attributes = None - self._last_changed = None - self._last_updated = None - self._context = None - - @property # type: ignore - def attributes(self): - """State attributes.""" - if not self._attributes: - try: - self._attributes = json.loads(self._row.attributes) - except ValueError: - # When json.loads fails - _LOGGER.exception("Error converting row to state: %s", self._row) - self._attributes = {} - return self._attributes - - @attributes.setter - def attributes(self, value): - """Set attributes.""" - self._attributes = value - - @property # type: ignore - def context(self): - """State context.""" - if not self._context: - self._context = Context(id=None) - return self._context - - @context.setter - def context(self, value): - """Set context.""" - self._context = value - - @property # type: ignore - def last_changed(self): - """Last changed datetime.""" - if not self._last_changed: - self._last_changed = process_timestamp(self._row.last_changed) - return self._last_changed - - @last_changed.setter - def last_changed(self, value): - """Set last changed datetime.""" - self._last_changed = value - - @property # type: ignore - def last_updated(self): - """Last updated datetime.""" - if not self._last_updated: - self._last_updated = process_timestamp(self._row.last_updated) - return self._last_updated - - @last_updated.setter - def last_updated(self, value): - """Set last updated datetime.""" - self._last_updated = value - - def as_dict(self): - """Return a dict representation of the LazyState. - - Async friendly. - - To be used for JSON serialization. - """ - if self._last_changed: - last_changed_isoformat = self._last_changed.isoformat() - else: - last_changed_isoformat = process_timestamp_to_utc_isoformat( - self._row.last_changed - ) - if self._last_updated: - last_updated_isoformat = self._last_updated.isoformat() - else: - last_updated_isoformat = process_timestamp_to_utc_isoformat( - self._row.last_updated - ) - return { - "entity_id": self.entity_id, - "state": self.state, - "attributes": self._attributes or self.attributes, - "last_changed": last_changed_isoformat, - "last_updated": last_updated_isoformat, - } - - def __eq__(self, other): - """Return the comparison.""" - return ( - other.__class__ in [self.__class__, State] - and self.entity_id == other.entity_id - and self.state == other.state - and self.attributes == other.attributes - ) diff --git a/homeassistant/components/history_stats/manifest.json b/homeassistant/components/history_stats/manifest.json index 1f6e8822e64..0836a7f6c9f 100644 --- a/homeassistant/components/history_stats/manifest.json +++ b/homeassistant/components/history_stats/manifest.json @@ -2,7 +2,7 @@ "domain": "history_stats", "name": "History Stats", "documentation": "https://www.home-assistant.io/integrations/history_stats", - "dependencies": ["history"], + "dependencies": ["recorder"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling" diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 54ff8bf8252..69f42da5e36 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -5,7 +5,7 @@ import math import voluptuous as vol -from homeassistant.components import history +from homeassistant.components.recorder import history from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_ENTITY_ID, diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index a783dabdbed..4b7709555d0 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -40,7 +40,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util -from . import migration, purge +from . import history, migration, purge from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX from .models import Base, Events, RecorderRuns, States from .pool import RecorderPool @@ -220,6 +220,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: instance.async_initialize() instance.start() _async_register_services(hass, instance) + history.async_setup(hass) return await instance.async_db_ready diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py new file mode 100644 index 00000000000..63938b6774a --- /dev/null +++ b/homeassistant/components/recorder/history.py @@ -0,0 +1,403 @@ +"""Provide pre-made queries on top of the recorder component.""" +from __future__ import annotations + +from collections import defaultdict +from itertools import groupby +import logging +import time + +from sqlalchemy import and_, bindparam, func +from sqlalchemy.ext import baked + +from homeassistant.components import recorder +from homeassistant.components.recorder.models import ( + States, + process_timestamp_to_utc_isoformat, +) +from homeassistant.components.recorder.util import execute, session_scope +from homeassistant.core import split_entity_id +import homeassistant.util.dt as dt_util + +from .models import LazyState + +# mypy: allow-untyped-defs, no-check-untyped-defs + +_LOGGER = logging.getLogger(__name__) + +STATE_KEY = "state" +LAST_CHANGED_KEY = "last_changed" + +SIGNIFICANT_DOMAINS = ( + "climate", + "device_tracker", + "humidifier", + "thermostat", + "water_heater", +) +IGNORE_DOMAINS = ("zone", "scene") +NEED_ATTRIBUTE_DOMAINS = { + "climate", + "humidifier", + "input_datetime", + "thermostat", + "water_heater", +} + +QUERY_STATES = [ + States.domain, + States.entity_id, + States.state, + States.attributes, + States.last_changed, + States.last_updated, +] + +HISTORY_BAKERY = "history_bakery" + + +def async_setup(hass): + """Set up the history hooks.""" + hass.data[HISTORY_BAKERY] = baked.bakery() + + +def get_significant_states(hass, *args, **kwargs): + """Wrap _get_significant_states with a sql session.""" + with session_scope(hass=hass) as session: + return _get_significant_states(hass, session, *args, **kwargs) + + +def _get_significant_states( + hass, + session, + start_time, + end_time=None, + entity_ids=None, + filters=None, + include_start_time_state=True, + significant_changes_only=True, + minimal_response=False, +): + """ + Return states changes during UTC period start_time - end_time. + + Significant states are all states where there is a state change, + as well as all states from certain domains (for instance + thermostat so that we get current temperature in our graphs). + """ + timer_start = time.perf_counter() + + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + + if significant_changes_only: + baked_query += lambda q: q.filter( + ( + States.domain.in_(SIGNIFICANT_DOMAINS) + | (States.last_changed == States.last_updated) + ) + & (States.last_updated > bindparam("start_time")) + ) + else: + baked_query += lambda q: q.filter(States.last_updated > bindparam("start_time")) + + if entity_ids is not None: + baked_query += lambda q: q.filter( + States.entity_id.in_(bindparam("entity_ids", expanding=True)) + ) + else: + baked_query += lambda q: q.filter(~States.domain.in_(IGNORE_DOMAINS)) + if filters: + filters.bake(baked_query) + + if end_time is not None: + baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) + + baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) + + states = execute( + baked_query(session).params( + start_time=start_time, end_time=end_time, entity_ids=entity_ids + ) + ) + + if _LOGGER.isEnabledFor(logging.DEBUG): + elapsed = time.perf_counter() - timer_start + _LOGGER.debug("get_significant_states took %fs", elapsed) + + return _sorted_states_to_dict( + hass, + session, + states, + start_time, + entity_ids, + filters, + include_start_time_state, + minimal_response, + ) + + +def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): + """Return states changes during UTC period start_time - end_time.""" + with session_scope(hass=hass) as session: + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + + baked_query += lambda q: q.filter( + (States.last_changed == States.last_updated) + & (States.last_updated > bindparam("start_time")) + ) + + if end_time is not None: + baked_query += lambda q: q.filter( + States.last_updated < bindparam("end_time") + ) + + if entity_id is not None: + baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) + entity_id = entity_id.lower() + + baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) + + states = execute( + baked_query(session).params( + start_time=start_time, end_time=end_time, entity_id=entity_id + ) + ) + + entity_ids = [entity_id] if entity_id is not None else None + + return _sorted_states_to_dict(hass, session, states, start_time, entity_ids) + + +def get_last_state_changes(hass, number_of_states, entity_id): + """Return the last number_of_states.""" + start_time = dt_util.utcnow() + + with session_scope(hass=hass) as session: + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + baked_query += lambda q: q.filter(States.last_changed == States.last_updated) + + if entity_id is not None: + baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) + entity_id = entity_id.lower() + + baked_query += lambda q: q.order_by( + States.entity_id, States.last_updated.desc() + ) + + baked_query += lambda q: q.limit(bindparam("number_of_states")) + + states = execute( + baked_query(session).params( + number_of_states=number_of_states, entity_id=entity_id + ) + ) + + entity_ids = [entity_id] if entity_id is not None else None + + return _sorted_states_to_dict( + hass, + session, + reversed(states), + start_time, + entity_ids, + include_start_time_state=False, + ) + + +def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): + """Return the states at a specific point in time.""" + if run is None: + run = recorder.run_information_from_instance(hass, utc_point_in_time) + + # History did not run before utc_point_in_time + if run is None: + return [] + + with session_scope(hass=hass) as session: + return _get_states_with_session( + hass, session, utc_point_in_time, entity_ids, run, filters + ) + + +def _get_states_with_session( + hass, session, utc_point_in_time, entity_ids=None, run=None, filters=None +): + """Return the states at a specific point in time.""" + if entity_ids and len(entity_ids) == 1: + return _get_single_entity_states_with_session( + hass, session, utc_point_in_time, entity_ids[0] + ) + + if run is None: + run = recorder.run_information_with_session(session, utc_point_in_time) + + # History did not run before utc_point_in_time + if run is None: + return [] + + # We have more than one entity to look at (most commonly we want + # all entities,) so we need to do a search on all states since the + # last recorder run started. + query = session.query(*QUERY_STATES) + + most_recent_states_by_date = session.query( + States.entity_id.label("max_entity_id"), + func.max(States.last_updated).label("max_last_updated"), + ).filter( + (States.last_updated >= run.start) & (States.last_updated < utc_point_in_time) + ) + + if entity_ids: + most_recent_states_by_date.filter(States.entity_id.in_(entity_ids)) + + most_recent_states_by_date = most_recent_states_by_date.group_by(States.entity_id) + + most_recent_states_by_date = most_recent_states_by_date.subquery() + + most_recent_state_ids = session.query( + func.max(States.state_id).label("max_state_id") + ).join( + most_recent_states_by_date, + and_( + States.entity_id == most_recent_states_by_date.c.max_entity_id, + States.last_updated == most_recent_states_by_date.c.max_last_updated, + ), + ) + + most_recent_state_ids = most_recent_state_ids.group_by(States.entity_id) + + most_recent_state_ids = most_recent_state_ids.subquery() + + query = query.join( + most_recent_state_ids, + States.state_id == most_recent_state_ids.c.max_state_id, + ) + + if entity_ids is not None: + query = query.filter(States.entity_id.in_(entity_ids)) + else: + query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) + if filters: + query = filters.apply(query) + + return [LazyState(row) for row in execute(query)] + + +def _get_single_entity_states_with_session(hass, session, utc_point_in_time, entity_id): + # Use an entirely different (and extremely fast) query if we only + # have a single entity id + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + baked_query += lambda q: q.filter( + States.last_updated < bindparam("utc_point_in_time"), + States.entity_id == bindparam("entity_id"), + ) + baked_query += lambda q: q.order_by(States.last_updated.desc()) + baked_query += lambda q: q.limit(1) + + query = baked_query(session).params( + utc_point_in_time=utc_point_in_time, entity_id=entity_id + ) + + return [LazyState(row) for row in execute(query)] + + +def _sorted_states_to_dict( + hass, + session, + states, + start_time, + entity_ids, + filters=None, + include_start_time_state=True, + minimal_response=False, +): + """Convert SQL results into JSON friendly data structure. + + This takes our state list and turns it into a JSON friendly data + structure {'entity_id': [list of states], 'entity_id2': [list of states]} + + States must be sorted by entity_id and last_updated + + We also need to go back and create a synthetic zero data point for + each list of states, otherwise our graphs won't start on the Y + axis correctly. + """ + result = defaultdict(list) + # Set all entity IDs to empty lists in result set to maintain the order + if entity_ids is not None: + for ent_id in entity_ids: + result[ent_id] = [] + + # Get the states at the start time + timer_start = time.perf_counter() + if include_start_time_state: + run = recorder.run_information_from_instance(hass, start_time) + for state in _get_states_with_session( + hass, session, start_time, entity_ids, run=run, filters=filters + ): + state.last_changed = start_time + state.last_updated = start_time + result[state.entity_id].append(state) + + if _LOGGER.isEnabledFor(logging.DEBUG): + elapsed = time.perf_counter() - timer_start + _LOGGER.debug("getting %d first datapoints took %fs", len(result), elapsed) + + # Called in a tight loop so cache the function + # here + _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat + + # Append all changes to it + for ent_id, group in groupby(states, lambda state: state.entity_id): + domain = split_entity_id(ent_id)[0] + ent_results = result[ent_id] + if not minimal_response or domain in NEED_ATTRIBUTE_DOMAINS: + ent_results.extend(LazyState(db_state) for db_state in group) + + # With minimal response we only provide a native + # State for the first and last response. All the states + # in-between only provide the "state" and the + # "last_changed". + if not ent_results: + ent_results.append(LazyState(next(group))) + + prev_state = ent_results[-1] + initial_state_count = len(ent_results) + + for db_state in group: + # With minimal response we do not care about attribute + # changes so we can filter out duplicate states + if db_state.state == prev_state.state: + continue + + ent_results.append( + { + STATE_KEY: db_state.state, + LAST_CHANGED_KEY: _process_timestamp_to_utc_isoformat( + db_state.last_changed + ), + } + ) + prev_state = db_state + + if prev_state and len(ent_results) != initial_state_count: + # There was at least one state change + # replace the last minimal state with + # a full state + ent_results[-1] = LazyState(prev_state) + + # Filter out the empty lists if some states had 0 results. + return {key: val for key, val in result.items() if val} + + +def get_state(hass, utc_point_in_time, entity_id, run=None): + """Return a state at a specific point in time.""" + states = get_states(hass, utc_point_in_time, (entity_id,), run) + return states[0] if states else None diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 3459da309ee..6f414a437c9 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -286,3 +286,116 @@ def process_timestamp_to_utc_isoformat(ts): if ts.tzinfo is None: return f"{ts.isoformat()}{DB_TIMEZONE}" return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "entity_id", + "state", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + ] + + def __init__(self, row): # pylint: disable=super-init-not-called + """Init the lazy state.""" + self._row = row + self.entity_id = self._row.entity_id + self.state = self._row.state or "" + self._attributes = None + self._last_changed = None + self._last_updated = None + self._context = None + + @property # type: ignore + def attributes(self): + """State attributes.""" + if not self._attributes: + try: + self._attributes = json.loads(self._row.attributes) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self._row) + self._attributes = {} + return self._attributes + + @attributes.setter + def attributes(self, value): + """Set attributes.""" + self._attributes = value + + @property # type: ignore + def context(self): + """State context.""" + if not self._context: + self._context = Context(id=None) + return self._context + + @context.setter + def context(self, value): + """Set context.""" + self._context = value + + @property # type: ignore + def last_changed(self): + """Last changed datetime.""" + if not self._last_changed: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value): + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore + def last_updated(self): + """Last updated datetime.""" + if not self._last_updated: + self._last_updated = process_timestamp(self._row.last_updated) + return self._last_updated + + @last_updated.setter + def last_updated(self, value): + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self): + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed: + last_changed_isoformat = self._last_changed.isoformat() + else: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if self._last_updated: + last_updated_isoformat = self._last_updated.isoformat() + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other): + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index b8cdaf3c88a..60fae0fc5be 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -81,7 +81,6 @@ async def test_chain(hass, values): async def test_chain_history(hass, values, missing=False): """Test if filter chaining works.""" config = { - "history": {}, "sensor": { "platform": "filter", "name": "test", @@ -94,7 +93,6 @@ async def test_chain_history(hass, values, missing=False): }, } await async_init_recorder_component(hass) - assert_setup_component(1, "history") t_0 = dt_util.utcnow() - timedelta(minutes=1) t_1 = dt_util.utcnow() - timedelta(minutes=2) @@ -114,10 +112,10 @@ async def test_chain_history(hass, values, missing=False): } with patch( - "homeassistant.components.history.state_changes_during_period", + "homeassistant.components.recorder.history.state_changes_during_period", return_value=fake_states, ), patch( - "homeassistant.components.history.get_last_state_changes", + "homeassistant.components.recorder.history.get_last_state_changes", return_value=fake_states, ): with assert_setup_component(1, "sensor"): @@ -208,7 +206,6 @@ async def test_chain_history_missing(hass, values): async def test_history_time(hass): """Test loading from history based on a time window.""" config = { - "history": {}, "sensor": { "platform": "filter", "name": "test", @@ -217,7 +214,6 @@ async def test_history_time(hass): }, } await async_init_recorder_component(hass) - assert_setup_component(1, "history") t_0 = dt_util.utcnow() - timedelta(minutes=1) t_1 = dt_util.utcnow() - timedelta(minutes=2) @@ -231,10 +227,10 @@ async def test_history_time(hass): ] } with patch( - "homeassistant.components.history.state_changes_during_period", + "homeassistant.components.recorder.history.state_changes_during_period", return_value=fake_states, ), patch( - "homeassistant.components.history.get_last_state_changes", + "homeassistant.components.recorder.history.get_last_state_changes", return_value=fake_states, ): with assert_setup_component(1, "sensor"): diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 497f296437f..bf6f392b649 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1,6 +1,5 @@ """The tests the History component.""" # pylint: disable=protected-access,invalid-name -from copy import copy from datetime import timedelta import json from unittest.mock import patch, sentinel @@ -8,13 +7,14 @@ from unittest.mock import patch, sentinel import pytest from homeassistant.components import history, recorder +from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.recorder.models import process_timestamp import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import init_recorder_component, mock_state_change_event +from tests.common import init_recorder_component from tests.components.recorder.common import trigger_db_commit, wait_recording_done @@ -25,151 +25,6 @@ def test_setup(): pass -def test_get_states(hass_history): - """Test getting states at a specific point in time.""" - hass = hass_history - states = [] - - now = dt_util.utcnow() - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now): - for i in range(5): - state = ha.State( - f"test.point_in_time_{i % 5}", - f"State {i}", - {"attribute_test": i}, - ) - - mock_state_change_event(hass, state) - - states.append(state) - - wait_recording_done(hass) - - future = now + timedelta(seconds=1) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=future): - for i in range(5): - state = ha.State( - f"test.point_in_time_{i % 5}", - f"State {i}", - {"attribute_test": i}, - ) - - mock_state_change_event(hass, state) - - wait_recording_done(hass) - - # Get states returns everything before POINT - for state1, state2 in zip( - states, - sorted(history.get_states(hass, future), key=lambda state: state.entity_id), - ): - assert state1 == state2 - - # Test get_state here because we have a DB setup - assert states[0] == history.get_state(hass, future, states[0].entity_id) - - time_before_recorder_ran = now - timedelta(days=1000) - assert history.get_states(hass, time_before_recorder_ran) == [] - - assert history.get_state(hass, time_before_recorder_ran, "demo.id") is None - - -def test_state_changes_during_period(hass_history): - """Test state change during period.""" - hass = hass_history - entity_id = "media_player.test" - - def set_state(state): - """Set the state.""" - hass.states.set(entity_id, state) - wait_recording_done(hass) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - point = start + timedelta(seconds=1) - end = point + timedelta(seconds=1) - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): - set_state("idle") - set_state("YouTube") - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): - states = [ - set_state("idle"), - set_state("Netflix"), - set_state("Plex"), - set_state("YouTube"), - ] - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=end): - set_state("Netflix") - set_state("Plex") - - hist = history.state_changes_during_period(hass, start, end, entity_id) - - assert states == hist[entity_id] - - -def test_get_last_state_changes(hass_history): - """Test number of state changes.""" - hass = hass_history - entity_id = "sensor.test" - - def set_state(state): - """Set the state.""" - hass.states.set(entity_id, state) - wait_recording_done(hass) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=2) - point = start + timedelta(minutes=1) - point2 = point + timedelta(minutes=1) - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): - set_state("1") - - states = [] - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): - states.append(set_state("2")) - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point2): - states.append(set_state("3")) - - hist = history.get_last_state_changes(hass, 2, entity_id) - - assert states == hist[entity_id] - - -def test_ensure_state_can_be_copied(hass_history): - """Ensure a state can pass though copy(). - - The filter integration uses copy() on states - from history. - """ - hass = hass_history - entity_id = "sensor.test" - - def set_state(state): - """Set the state.""" - hass.states.set(entity_id, state) - wait_recording_done(hass) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=2) - point = start + timedelta(minutes=1) - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): - set_state("1") - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): - set_state("2") - - hist = history.get_last_state_changes(hass, 2, entity_id) - - assert copy(hist[entity_id][0]) == hist[entity_id][0] - assert copy(hist[entity_id][1]) == hist[entity_id][1] - - def test_get_significant_states(hass_history): """Test that only significant states are returned. @@ -179,7 +34,7 @@ def test_get_significant_states(hass_history): """ hass = hass_history zero, four, states = record_states(hass) - hist = history.get_significant_states(hass, zero, four, filters=history.Filters()) + hist = get_significant_states(hass, zero, four, filters=history.Filters()) assert states == hist @@ -195,7 +50,7 @@ def test_get_significant_states_minimal_response(hass_history): """ hass = hass_history zero, four, states = record_states(hass) - hist = history.get_significant_states( + hist = get_significant_states( hass, zero, four, filters=history.Filters(), minimal_response=True ) @@ -236,7 +91,7 @@ def test_get_significant_states_with_initial(hass_history): if state.last_changed == one: state.last_changed = one_and_half - hist = history.get_significant_states( + hist = get_significant_states( hass, one_and_half, four, @@ -263,7 +118,7 @@ def test_get_significant_states_without_initial(hass_history): ) del states["media_player.test2"] - hist = history.get_significant_states( + hist = get_significant_states( hass, one_and_half, four, @@ -283,7 +138,7 @@ def test_get_significant_states_entity_id(hass_history): del states["thermostat.test2"] del states["script.can_cancel_this_one"] - hist = history.get_significant_states( + hist = get_significant_states( hass, zero, four, ["media_player.test"], filters=history.Filters() ) assert states == hist @@ -298,7 +153,7 @@ def test_get_significant_states_multiple_entity_ids(hass_history): del states["thermostat.test2"] del states["script.can_cancel_this_one"] - hist = history.get_significant_states( + hist = get_significant_states( hass, zero, four, @@ -570,12 +425,12 @@ def test_get_significant_states_are_ordered(hass_history): hass = hass_history zero, four, _states = record_states(hass) entity_ids = ["media_player.test", "media_player.test2"] - hist = history.get_significant_states( + hist = get_significant_states( hass, zero, four, entity_ids, filters=history.Filters() ) assert list(hist.keys()) == entity_ids entity_ids = ["media_player.test2", "media_player.test"] - hist = history.get_significant_states( + hist = get_significant_states( hass, zero, four, entity_ids, filters=history.Filters() ) assert list(hist.keys()) == entity_ids @@ -619,14 +474,14 @@ def test_get_significant_states_only(hass_history): # everything is different states.append(set_state("412", attributes={"attribute": 54.23})) - hist = history.get_significant_states(hass, start, significant_changes_only=True) + hist = get_significant_states(hass, start, significant_changes_only=True) assert len(hist[entity_id]) == 2 assert states[0] not in hist[entity_id] assert states[1] in hist[entity_id] assert states[2] in hist[entity_id] - hist = history.get_significant_states(hass, start, significant_changes_only=False) + hist = get_significant_states(hass, start, significant_changes_only=False) assert len(hist[entity_id]) == 3 assert states == hist[entity_id] @@ -644,7 +499,7 @@ def check_significant_states(hass, zero, four, states, config): filters.included_entities = include.get(history.CONF_ENTITIES, []) filters.included_domains = include.get(history.CONF_DOMAINS, []) - hist = history.get_significant_states(hass, zero, four, filters=filters) + hist = get_significant_states(hass, zero, four, filters=filters) assert states == hist diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 6e25e9e67cf..01ce5bf06b3 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -35,7 +35,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test the history statistics sensor setup.""" self.init_recorder() config = { - "history": {}, "sensor": { "platform": "history_stats", "entity_id": "binary_sensor.test_id", @@ -57,7 +56,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test the history statistics sensor setup for multiple states.""" self.init_recorder() config = { - "history": {}, "sensor": { "platform": "history_stats", "entity_id": "binary_sensor.test_id", @@ -146,7 +144,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test when duration value is not a timedelta.""" self.init_recorder() config = { - "history": {}, "sensor": { "platform": "history_stats", "entity_id": "binary_sensor.test_id", @@ -187,7 +184,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test config when not enough arguments provided.""" self.init_recorder() config = { - "history": {}, "sensor": { "platform": "history_stats", "entity_id": "binary_sensor.test_id", @@ -206,7 +202,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test config when too many arguments provided.""" self.init_recorder() config = { - "history": {}, "sensor": { "platform": "history_stats", "entity_id": "binary_sensor.test_id", @@ -344,9 +339,9 @@ async def test_measure_multiple(hass): ) with patch( - "homeassistant.components.history.state_changes_during_period", + "homeassistant.components.recorder.history.state_changes_during_period", return_value=fake_states, - ), patch("homeassistant.components.history.get_state", return_value=None): + ), patch("homeassistant.components.recorder.history.get_state", return_value=None): for i in range(1, 5): await hass.helpers.entity_component.async_update_entity(f"sensor.sensor{i}") await hass.async_block_till_done() @@ -421,9 +416,9 @@ async def async_test_measure(hass): ) with patch( - "homeassistant.components.history.state_changes_during_period", + "homeassistant.components.recorder.history.state_changes_during_period", return_value=fake_states, - ), patch("homeassistant.components.history.get_state", return_value=None): + ), patch("homeassistant.components.recorder.history.get_state", return_value=None): for i in range(1, 5): await hass.helpers.entity_component.async_update_entity(f"sensor.sensor{i}") await hass.async_block_till_done() diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py new file mode 100644 index 00000000000..b2940f2bb39 --- /dev/null +++ b/tests/components/recorder/test_history.py @@ -0,0 +1,432 @@ +"""The tests the History component.""" +# pylint: disable=protected-access,invalid-name +from copy import copy +from datetime import timedelta +import json +from unittest.mock import patch, sentinel + +from homeassistant.components.recorder import history +from homeassistant.components.recorder.models import process_timestamp +import homeassistant.core as ha +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +from tests.common import mock_state_change_event +from tests.components.recorder.common import wait_recording_done + + +def test_get_states(hass_recorder): + """Test getting states at a specific point in time.""" + hass = hass_recorder() + states = [] + + now = dt_util.utcnow() + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now): + for i in range(5): + state = ha.State( + f"test.point_in_time_{i % 5}", + f"State {i}", + {"attribute_test": i}, + ) + + mock_state_change_event(hass, state) + + states.append(state) + + wait_recording_done(hass) + + future = now + timedelta(seconds=1) + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=future): + for i in range(5): + state = ha.State( + f"test.point_in_time_{i % 5}", + f"State {i}", + {"attribute_test": i}, + ) + + mock_state_change_event(hass, state) + + wait_recording_done(hass) + + # Get states returns everything before POINT + for state1, state2 in zip( + states, + sorted(history.get_states(hass, future), key=lambda state: state.entity_id), + ): + assert state1 == state2 + + # Test get_state here because we have a DB setup + assert states[0] == history.get_state(hass, future, states[0].entity_id) + + time_before_recorder_ran = now - timedelta(days=1000) + assert history.get_states(hass, time_before_recorder_ran) == [] + + assert history.get_state(hass, time_before_recorder_ran, "demo.id") is None + + +def test_state_changes_during_period(hass_recorder): + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + set_state("idle") + set_state("YouTube") + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): + states = [ + set_state("idle"), + set_state("Netflix"), + set_state("Plex"), + set_state("YouTube"), + ] + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=end): + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period(hass, start, end, entity_id) + + assert states == hist[entity_id] + + +def test_get_last_state_changes(hass_recorder): + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + set_state("1") + + states = [] + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): + states.append(set_state("2")) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point2): + states.append(set_state("3")) + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert states == hist[entity_id] + + +def test_ensure_state_can_be_copied(hass_recorder): + """Ensure a state can pass though copy(). + + The filter integration uses copy() on states + from history. + """ + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + set_state("1") + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): + set_state("2") + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert copy(hist[entity_id][0]) == hist[entity_id][0] + assert copy(hist[entity_id][1]) == hist[entity_id][1] + + +def test_get_significant_states(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert states == hist + + +def test_get_significant_states_minimal_response(hass_recorder): + """Test that only significant states are returned. + + When minimal responses is set only the first and + last states return a complete state. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four, minimal_response=True) + + # The second media_player.test state is reduced + # down to last_changed and state when minimal_response + # is set. We use JSONEncoder to make sure that are + # pre-encoded last_changed is always the same as what + # will happen with encoding a native state + input_state = states["media_player.test"][1] + orig_last_changed = json.dumps( + process_timestamp(input_state.last_changed), + cls=JSONEncoder, + ).replace('"', "") + orig_state = input_state.state + states["media_player.test"][1] = { + "last_changed": orig_last_changed, + "state": orig_state, + } + + assert states == hist + + +def test_get_significant_states_with_initial(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + if entity_id == "media_player.test": + states[entity_id] = states[entity_id][1:] + for state in states[entity_id]: + if state.last_changed == one: + state.last_changed = one_and_half + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=True, + ) + assert states == hist + + +def test_get_significant_states_without_initial(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + states[entity_id] = list( + filter(lambda s: s.last_changed != one, states[entity_id]) + ) + del states["media_player.test2"] + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=False, + ) + assert states == hist + + +def test_get_significant_states_entity_id(hass_recorder): + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + hist = history.get_significant_states(hass, zero, four, ["media_player.test"]) + assert states == hist + + +def test_get_significant_states_multiple_entity_ids(hass_recorder): + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + hist = history.get_significant_states( + hass, + zero, + four, + ["media_player.test", "thermostat.test"], + ) + assert states == hist + + +def test_get_significant_states_are_ordered(hass_recorder): + """Test order of results from get_significant_states. + + When entity ids are given, the results should be returned with the data + in the same order. + """ + hass = hass_recorder() + zero, four, _states = record_states(hass) + entity_ids = ["media_player.test", "media_player.test2"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + entity_ids = ["media_player.test2", "media_player.test"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + + +def test_get_significant_states_only(hass_recorder): + """Test significant states when significant_states_only is set.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=4) + points = [] + for i in range(1, 4): + points.append(start + timedelta(minutes=i)) + + states = [] + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + set_state("123", attributes={"attribute": 10.64}) + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=points[0] + ): + # Attributes are different, state not + states.append(set_state("123", attributes={"attribute": 21.42})) + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=points[1] + ): + # state is different, attributes not + states.append(set_state("32", attributes={"attribute": 21.42})) + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=points[2] + ): + # everything is different + states.append(set_state("412", attributes={"attribute": 54.23})) + + hist = history.get_significant_states(hass, start, significant_changes_only=True) + + assert len(hist[entity_id]) == 2 + assert states[0] not in hist[entity_id] + assert states[1] in hist[entity_id] + assert states[2] in hist[entity_id] + + hist = history.get_significant_states(hass, start, significant_changes_only=False) + + assert len(hist[entity_id]) == 3 + assert states == hist[entity_id] + + +def record_states(hass): + """Record some test states. + + We inject a bunch of state updates from media player, zone and + thermostat. + """ + mp = "media_player.test" + mp2 = "media_player.test2" + mp3 = "media_player.test3" + therm = "thermostat.test" + therm2 = "thermostat.test2" + zone = "zone.home" + script_c = "script.can_cancel_this_one" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(seconds=1) + two = one + timedelta(seconds=1) + three = two + timedelta(seconds=1) + four = three + timedelta(seconds=1) + + states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp2].append( + set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp3].append( + set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[therm].append( + set_state(therm, 20, attributes={"current_temperature": 19.5}) + ) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + # This state will be skipped only different in time + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) + # This state will be skipped because domain is excluded + set_state(zone, "zoning") + states[script_c].append( + set_state(script_c, "off", attributes={"can_cancel": True}) + ) + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 19.8}) + ) + states[therm2].append( + set_state(therm2, 20, attributes={"current_temperature": 19}) + ) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + states[mp].append( + set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) + ) + states[mp3].append( + set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + ) + # Attributes changed even though state is the same + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 20}) + ) + + return zero, four, states From 56d1e0a99dd23fbd5e77a1448d9c61f0e65a07fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 11 May 2021 11:46:51 +0200 Subject: [PATCH 315/852] Migrate wheels builder to GitHub actions (#50445) --- .github/workflows/wheels.yml | 161 +++++++++++++++++++++++++++++++++++ azure-pipelines-wheels.yml | 101 ---------------------- 2 files changed, 161 insertions(+), 101 deletions(-) create mode 100644 .github/workflows/wheels.yml delete mode 100644 azure-pipelines-wheels.yml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 00000000000..360025b4d7e --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,161 @@ +name: Build wheels + +# yamllint disable-line rule:truthy +on: + workflow_dispatch: + schedule: + - cron: "0 4 * * *" + push: + branches: + - dev + - rc + paths: + - "requirements.txt" + - "requirements_all.txt" + +jobs: + init: + name: Initialize wheels builder + runs-on: ubuntu-latest + outputs: + architectures: ${{ steps.info.outputs.architectures }} + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Get information + id: info + uses: home-assistant/actions/helpers/info@master + + - name: Create requirements_diff file + run: curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/dev/requirements.txt + + - name: Write env-file + run: | + ( + echo "GRPC_BUILD_WITH_BORING_SSL_ASM=false" + echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true" + echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true" + ) > .env_file + + - name: Upload env_file + uses: actions/upload-artifact@v2 + with: + name: env_file + path: ./env_file + + - name: Upload requirements_diff + uses: actions/upload-artifact@v2 + with: + name: requirements_diff + path: ./requirements_diff.txt + + core: + name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for core + needs: init + runs-on: ubuntu-latest + strategy: + matrix: + arch: ${{ fromJson(needs.init.outputs.architectures) }} + tag: + - "3.8-alpine3.12" + - "3.9-alpine3.13" + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Download env_file + uses: actions/download-artifact@v2 + with: + name: env_file + + - name: Download requirements_diff + uses: actions/download-artifact@v2 + with: + name: requirements_diff + + - name: Build wheels + uses: home-assistant/wheels@master + with: + tag: ${{ matrix.tag }} + arch: ${{ matrix.arch }} + wheels-host: ${{ secrets.WHEELS_HOST }} + wheels-key: ${{ secrets.WHEELS_KEY }} + wheels-user: wheels + env-file: true + apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev" + pip: "Cython;numpy" + skip-binary: aiohttp + constraints: "homeassistant/package_constraints.txt" + requirements-diff: 'requirements_diff.txt' + requirements: "requirements.txt" + + integrations: + name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for integrations + needs: init + runs-on: ubuntu-latest + strategy: + matrix: + arch: ${{ fromJson(needs.init.outputs.architectures) }} + tag: + - "3.8-alpine3.12" + - "3.9-alpine3.13" + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Download env_file + uses: actions/download-artifact@v2 + with: + name: env_file + + - name: Download requirements_diff + uses: actions/download-artifact@v2 + with: + name: requirements_diff + + - name: Uncomment packages + run: | + requirement_files="requirements_all.txt requirements_diff.txt" + for requirement_file in ${requirement_files}; do + sed -i "s|# pybluez|pybluez|g" ${requirement_file} + sed -i "s|# bluepy|bluepy|g" ${requirement_file} + sed -i "s|# beacontools|beacontools|g" ${requirement_file} + sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file} + sed -i "s|# raspihats|raspihats|g" ${requirement_file} + sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file} + sed -i "s|# blinkt|blinkt|g" ${requirement_file} + sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} + sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} + sed -i "s|# evdev|evdev|g" ${requirement_file} + sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file} + sed -i "s|# i2csense|i2csense|g" ${requirement_file} + sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} + sed -i "s|# pycups|pycups|g" ${requirement_file} + 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|# bme680|bme680|g" ${requirement_file} + sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} + done + + - name: Build wheels + uses: home-assistant/wheels@master + with: + tag: ${{ matrix.tag }} + arch: ${{ matrix.arch }} + wheels-host: ${{ secrets.WHEELS_HOST }} + wheels-key: ${{ secrets.WHEELS_KEY }} + wheels-user: wheels + env-file: true + apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev" + pip: "Cython;numpy;scikit-build" + skip-binary: aiohttp + constraints: "homeassistant/package_constraints.txt" + requirements-diff: 'requirements_diff.txt' + requirements: "requirements_all.txt" diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml deleted file mode 100644 index 760030ec3cc..00000000000 --- a/azure-pipelines-wheels.yml +++ /dev/null @@ -1,101 +0,0 @@ -# https://dev.azure.com/home-assistant - -trigger: - branches: - include: - - dev - - rc - paths: - include: - - requirements_all.txt -pr: none -schedules: -- cron: '0 */4 * * *' - displayName: 'daily builds' - branches: - include: - - dev -variables: - - name: versionWheels - value: '1.13.0-3.8-alpine3.12' -resources: - repositories: - - repository: azure - type: github - name: 'home-assistant/ci-azure' - endpoint: 'home-assistant' - -jobs: -- template: templates/azp-job-wheels.yaml@azure - parameters: - builderVersion: '$(versionWheels)' - builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev' - builderPip: 'Cython;numpy' - skipBinary: 'aiohttp' - wheelsRequirement: 'requirements.txt' - wheelsRequirementDiff: 'requirements_diff.txt' - wheelsConstraint: 'homeassistant/package_constraints.txt' - jobName: 'Wheels_Core' - preBuild: - - script: | - if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then - exit 0 - else - curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt - fi - displayName: 'Prepare requirements files for Home Assistant Core wheels' -- template: templates/azp-job-wheels.yaml@azure - parameters: - builderVersion: '$(versionWheels)' - builderApk: 'build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' - builderPip: 'Cython;numpy;scikit-build' - builderEnvFile: true - skipBinary: 'aiohttp' - wheelsRequirement: 'requirements_wheels.txt' - wheelsRequirementDiff: 'requirements_diff.txt' - wheelsConstraint: 'homeassistant/package_constraints.txt' - jobName: 'Wheels_Integrations' - preBuild: - - script: | - cp requirements_all.txt requirements_wheels.txt - if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then - touch requirements_diff.txt - else - curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements_all.txt - fi - - requirement_files="requirements_wheels.txt requirements_diff.txt" - for requirement_file in ${requirement_files}; do - sed -i "s|# pybluez|pybluez|g" ${requirement_file} - sed -i "s|# bluepy|bluepy|g" ${requirement_file} - sed -i "s|# beacontools|beacontools|g" ${requirement_file} - sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file} - sed -i "s|# raspihats|raspihats|g" ${requirement_file} - sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file} - sed -i "s|# blinkt|blinkt|g" ${requirement_file} - sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} - sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} - sed -i "s|# evdev|evdev|g" ${requirement_file} - sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file} - sed -i "s|# i2csense|i2csense|g" ${requirement_file} - sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} - sed -i "s|# pycups|pycups|g" ${requirement_file} - 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|# bme680|bme680|g" ${requirement_file} - sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} - done - - # Write env for build settings - ( - echo "GRPC_BUILD_WITH_BORING_SSL_ASM=false" - echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true" - echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true" - ) > .env_file - displayName: 'Prepare requirements files for Home Assistant wheels' From d6dcf95235b4c6957e45757b78881fc8149b635e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 11 May 2021 11:54:02 +0200 Subject: [PATCH 316/852] Fix .env_file name (#50447) --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 360025b4d7e..b3dbf04609e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -42,7 +42,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: env_file - path: ./env_file + path: ./.env_file - name: Upload requirements_diff uses: actions/upload-artifact@v2 From ef67a2659e26811c356f6f53e09fea6b275c67ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 11 May 2021 12:15:57 +0200 Subject: [PATCH 317/852] Add ignore_diff to workflow_dispatch trigger (#50449) --- .github/workflows/wheels.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b3dbf04609e..72af301be1c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -3,6 +3,11 @@ name: Build wheels # yamllint disable-line rule:truthy on: workflow_dispatch: + inputs: + ignore_diff: + description: "Ignore checking requirements_diff.txt" + default: "false" + required: true schedule: - cron: "0 4 * * *" push: @@ -28,7 +33,14 @@ jobs: uses: home-assistant/actions/helpers/info@master - name: Create requirements_diff file - run: curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/dev/requirements.txt + run: | + curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/dev/requirements.txt + + if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then + if [[ ${{ github.event.inputs.ignore_diff }} == "true" ]]; then + echo "" > requirements_diff.txt + fi + fi - name: Write env-file run: | From 50a88eb6c0dd026f187d340e70e769a2bac88e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 11 May 2021 12:29:13 +0200 Subject: [PATCH 318/852] Use empty requirements_diff for schedule and workflow_dispatch (#50450) --- .github/workflows/wheels.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 72af301be1c..2dc8eb1d29b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -3,11 +3,6 @@ name: Build wheels # yamllint disable-line rule:truthy on: workflow_dispatch: - inputs: - ignore_diff: - description: "Ignore checking requirements_diff.txt" - default: "false" - required: true schedule: - cron: "0 4 * * *" push: @@ -34,12 +29,10 @@ jobs: - name: Create requirements_diff file run: | - curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/dev/requirements.txt - - if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then - if [[ ${{ github.event.inputs.ignore_diff }} == "true" ]]; then - echo "" > requirements_diff.txt - fi + if [[ ${{ github.event_name }} ~= (schedule|workflow_dispatch) ]]; then + touch requirements_diff.txt + else + curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt fi - name: Write env-file From bc9f0caf4ac1fd7b67d694cedb02876fe221c8e6 Mon Sep 17 00:00:00 2001 From: Pawel Date: Tue, 11 May 2021 12:45:03 +0200 Subject: [PATCH 319/852] Add configuration.yaml deprecation warning to Epson (#50403) * add deprecation warning * Break long string Co-authored-by: Martin Hjelmare --- homeassistant/components/epson/media_player.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 9910826cc3d..92a43330d69 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -87,6 +87,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Epson projector.""" + _LOGGER.warning( + "Loading Espon projector via platform setup is deprecated; " + "Please remove it from your configuration" + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config From 236f138ab73e24b03ebc0fbdad553f5f19a211af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 11 May 2021 13:23:56 +0200 Subject: [PATCH 320/852] Fix compare syntax (#50451) --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2dc8eb1d29b..16818a37cb2 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -29,7 +29,7 @@ jobs: - name: Create requirements_diff file run: | - if [[ ${{ github.event_name }} ~= (schedule|workflow_dispatch) ]]; then + if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then touch requirements_diff.txt else curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt From 1538271555d36e10795b22c2f5162995c5c86cbe Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Tue, 11 May 2021 13:29:14 +0100 Subject: [PATCH 321/852] Don't generate mypy.ini if errors are found (#50456) --- script/hassfest/mypy_config.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 54e5b5b13a7..9b03210e9eb 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -308,7 +308,9 @@ def generate_and_validate(config: Config) -> str: "mypy_config", f"Only components should be added: {module}" ) if module in ignored_modules_set: - config.add_error("mypy_config", f"Module '{module}' is in ignored list") + config.add_error( + "mypy_config", f"Module '{module}' is in ignored list in mypy_config.py" + ) # Validate that all modules exist. all_modules = strict_modules + IGNORED_MODULES @@ -326,6 +328,10 @@ def generate_and_validate(config: Config) -> str: if not module_path.is_file(): config.add_error("mypy_config", f"Module '{module} doesn't exist") + # Don't generate mypy.ini if there're errors found because it will likely crash. + if config.errors: + return "" + mypy_config = configparser.ConfigParser() general_section = "mypy" @@ -369,6 +375,9 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: config_path = config.root / "mypy.ini" config.cache["mypy_config"] = content = generate_and_validate(config) + if config.errors: + return + with open(str(config_path)) as fp: if fp.read().strip() != content: config.add_error( From 48b5ef0bac6a8e9abc617ef62bb95f282548bb2d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 11 May 2021 15:53:36 +0200 Subject: [PATCH 322/852] Clean twentemilieu config flow tests (#50460) --- .../twentemilieu/test_config_flow.py | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index 27fd86d0868..93ee7116f76 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -2,13 +2,13 @@ import aiohttp from homeassistant import data_entry_flow -from homeassistant.components.twentemilieu import config_flow from homeassistant.components.twentemilieu.const import ( CONF_HOUSE_LETTER, CONF_HOUSE_NUMBER, CONF_POST_CODE, DOMAIN, ) +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_ID, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant @@ -16,7 +16,6 @@ from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker FIXTURE_USER_INPUT = { - CONF_ID: "12345", CONF_POST_CODE: "1234AB", CONF_HOUSE_NUMBER: "1", CONF_HOUSE_LETTER: "A", @@ -25,9 +24,9 @@ FIXTURE_USER_INPUT = { async def test_show_set_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" - flow = config_flow.TwenteMilieuFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -41,9 +40,9 @@ async def test_connection_error( "https://twentemilieuapi.ximmio.com/api/FetchAdress", exc=aiohttp.ClientError ) - flow = config_flow.TwenteMilieuFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -60,9 +59,9 @@ async def test_invalid_address( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.TwenteMilieuFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -73,9 +72,9 @@ async def test_address_already_set_up( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort if address has already been set up.""" - MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT, title="12345").add_to_hass( - hass - ) + MockConfigEntry( + domain=DOMAIN, data={**FIXTURE_USER_INPUT, CONF_ID: "12345"}, title="12345" + ).add_to_hass(hass) aioclient_mock.post( "https://twentemilieuapi.ximmio.com/api/FetchAdress", @@ -83,9 +82,9 @@ async def test_address_already_set_up( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.TwenteMilieuFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -101,13 +100,18 @@ async def test_full_flow_implementation( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.TwenteMilieuFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "12345" assert result["data"][CONF_POST_CODE] == FIXTURE_USER_INPUT[CONF_POST_CODE] From ca65cdd450a5509297947822476c321579a5d17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 11 May 2021 16:14:32 +0200 Subject: [PATCH 323/852] pyTibber revert (#50462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index ee2b8404405..01a20011bef 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.16.3"], + "requirements": ["pyTibber==0.16.2"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 04531f6f0e5..1001a9640b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1250,7 +1250,7 @@ pyRFXtrx==0.26.1 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.16.3 +pyTibber==0.16.2 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7f63ab3692..af92bd17488 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -687,7 +687,7 @@ pyMetno==0.8.3 pyRFXtrx==0.26.1 # homeassistant.components.tibber -pyTibber==0.16.3 +pyTibber==0.16.2 # homeassistant.components.nextbus py_nextbusnext==0.1.4 From 4e24640ff7805a280bcfb33746fae49c222091e7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 11 May 2021 16:17:00 +0200 Subject: [PATCH 324/852] Remove pytest-mock dependency (#50400) --- tests/components/fritz/test_config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 14830249da9..a795f2073dd 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -54,11 +54,11 @@ MOCK_SSDP_DATA = { @pytest.fixture() -def fc_class_mock(mocker): +def fc_class_mock(): """Fixture that sets up a mocked FritzConnection class.""" - result = mocker.patch("fritzconnection.FritzConnection", autospec=True) - result.return_value = FritzConnectionMock() - yield result + with patch("fritzconnection.FritzConnection", autospec=True) as result: + result.return_value = FritzConnectionMock() + yield result async def test_user(hass: HomeAssistant, fc_class_mock): From f71eb4d34d5a5afaa0ca83c00bd0a63b0f868b59 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 11 May 2021 16:19:07 +0200 Subject: [PATCH 325/852] Clean somfy config flow tests (#50461) --- .coveragerc | 7 ++- script/hassfest/coverage.py | 1 - tests/components/somfy/test_config_flow.py | 62 +++++++++------------- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/.coveragerc b/.coveragerc index 02cd35bcd60..81f2d17c671 100644 --- a/.coveragerc +++ b/.coveragerc @@ -934,7 +934,12 @@ omit = homeassistant/components/soma/__init__.py homeassistant/components/soma/cover.py homeassistant/components/soma/sensor.py - homeassistant/components/somfy/* + homeassistant/components/somfy/__init__.py + homeassistant/components/somfy/api.py + homeassistant/components/somfy/climate.py + homeassistant/components/somfy/cover.py + homeassistant/components/somfy/sensor.py + homeassistant/components/somfy/switch.py homeassistant/components/somfy_mylink/__init__.py homeassistant/components/somfy_mylink/cover.py homeassistant/components/sonos/* diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 06e38902060..1a4b1fbf8ba 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -51,7 +51,6 @@ ALLOWED_IGNORE_VIOLATIONS = { ("sense", "config_flow.py"), ("sms", "config_flow.py"), ("solarlog", "config_flow.py"), - ("somfy", "config_flow.py"), ("sonos", "config_flow.py"), ("speedtestdotnet", "config_flow.py"), ("spider", "config_flow.py"), diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index 60200824c00..4e969358b2a 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -2,10 +2,8 @@ import asyncio from unittest.mock import patch -import pytest - from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.somfy import DOMAIN, config_flow +from homeassistant.components.somfy import DOMAIN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow @@ -15,39 +13,22 @@ CLIENT_ID_VALUE = "1234" CLIENT_SECRET_VALUE = "5678" -@pytest.fixture() -async def mock_impl(hass): - """Mock implementation.""" - await setup.async_setup_component(hass, "http", {}) - - impl = config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, - CLIENT_ID_VALUE, - CLIENT_SECRET_VALUE, - "https://accounts.somfy.com/oauth/oauth/v2/auth", - "https://accounts.somfy.com/oauth/oauth/v2/token", - ) - config_flow.SomfyFlowHandler.async_register_implementation(hass, impl) - return impl - - async def test_abort_if_no_configuration(hass): """Check flow abort when no configuration.""" - flow = config_flow.SomfyFlowHandler() - flow.hass = hass - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "missing_configuration" async def test_abort_if_existing_entry(hass): """Check flow abort when an entry already exist.""" - flow = config_flow.SomfyFlowHandler() - flow.hass = hass MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" @@ -63,8 +44,7 @@ async def test_full_flow( DOMAIN: { CONF_CLIENT_ID: CLIENT_ID_VALUE, CONF_CLIENT_SECRET: CLIENT_SECRET_VALUE, - }, - "http": {"base_url": "https://example.com"}, + } }, ) @@ -123,17 +103,27 @@ async def test_full_flow( assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED -async def test_abort_if_authorization_timeout( - hass, mock_impl, current_request_with_host -): +async def test_abort_if_authorization_timeout(hass, current_request_with_host): """Check Somfy authorization timeout.""" - flow = config_flow.SomfyFlowHandler() - flow.hass = hass + assert await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_CLIENT_ID: CLIENT_ID_VALUE, + CONF_CLIENT_SECRET: CLIENT_SECRET_VALUE, + } + }, + ) - with patch.object( - mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + with patch( + "homeassistant.components.somfy.config_entry_oauth2_flow." + "LocalOAuth2Implementation.async_generate_authorize_url", + side_effect=asyncio.TimeoutError, ): - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "authorize_url_timeout" From 7c4893cbb19d7386c0c4d1a6375c2d4fc7b62b68 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 11 May 2021 16:23:59 +0200 Subject: [PATCH 326/852] Fix event action return value typing (#50353) Co-authored-by: Ruslan Sayfutdinov --- homeassistant/helpers/event.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b8a8db8f03d..cdcacb8871b 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -139,7 +139,7 @@ def threaded_listener_factory( def async_track_state_change( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[str, State, State], None], + action: Callable[[str, State, State], Awaitable[None] | None], from_state: None | str | Iterable[str] = None, to_state: None | str | Iterable[str] = None, ) -> CALLBACK_TYPE: @@ -683,7 +683,7 @@ def async_track_state_change_filtered( def async_track_template( hass: HomeAssistant, template: Template, - action: Callable[[str, State | None, State | None], None], + action: Callable[[str, State | None, State | None], Awaitable[None] | None], variables: TemplateVarsType | None = None, ) -> Callable[[], None]: """Add a listener that fires when a a template evaluates to 'true'. @@ -1072,7 +1072,7 @@ def async_track_template_result( def async_track_same_state( hass: HomeAssistant, period: timedelta, - action: Callable[..., None], + action: Callable[..., Awaitable[None] | None], async_check_same_func: Callable[[str, State | None, State | None], bool], entity_ids: str | Iterable[str] = MATCH_ALL, ) -> CALLBACK_TYPE: @@ -1141,7 +1141,7 @@ track_same_state = threaded_listener_factory(async_track_same_state) @bind_hass def async_track_point_in_time( hass: HomeAssistant, - action: HassJob | Callable[..., None], + action: HassJob | Callable[..., Awaitable[None] | None], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in time.""" @@ -1162,7 +1162,7 @@ track_point_in_time = threaded_listener_factory(async_track_point_in_time) @bind_hass def async_track_point_in_utc_time( hass: HomeAssistant, - action: HassJob | Callable[..., None], + action: HassJob | Callable[..., Awaitable[None] | None], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in UTC time.""" @@ -1213,7 +1213,9 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim @callback @bind_hass def async_call_later( - hass: HomeAssistant, delay: float, action: HassJob | Callable[..., None] + hass: HomeAssistant, + delay: float, + action: HassJob | Callable[..., Awaitable[None] | None], ) -> CALLBACK_TYPE: """Add a listener that is called in .""" return async_track_point_in_utc_time( @@ -1228,7 +1230,7 @@ call_later = threaded_listener_factory(async_call_later) @bind_hass def async_track_time_interval( hass: HomeAssistant, - action: Callable[..., None | Awaitable], + action: Callable[..., Awaitable[None] | None], interval: timedelta, ) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" @@ -1360,7 +1362,7 @@ time_tracker_utcnow = dt_util.utcnow @bind_hass def async_track_utc_time_change( hass: HomeAssistant, - action: Callable[..., None], + action: Callable[..., Awaitable[None] | None], hour: Any | None = None, minute: Any | None = None, second: Any | None = None, From f5541a468eb0cfeebdd3c582822fe8dabfcbfa05 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 11 May 2021 16:57:24 +0200 Subject: [PATCH 327/852] Improve type annotations for GIOS integration (#50454) --- .strict-typing | 1 + homeassistant/components/gios/__init__.py | 45 ++++++-- homeassistant/components/gios/air_quality.py | 107 ++++++++++-------- homeassistant/components/gios/config_flow.py | 10 +- homeassistant/components/gios/const.py | 47 ++++---- .../components/gios/system_health.py | 8 +- mypy.ini | 14 ++- script/hassfest/mypy_config.py | 1 - tests/components/gios/__init__.py | 2 +- tests/components/gios/test_air_quality.py | 27 ++++- tests/components/gios/test_config_flow.py | 2 +- tests/components/gios/test_init.py | 48 +++++++- 12 files changed, 218 insertions(+), 94 deletions(-) diff --git a/.strict-typing b/.strict-typing index f6ed1ba63af..97e3a467359 100644 --- a/.strict-typing +++ b/.strict-typing @@ -15,6 +15,7 @@ homeassistant.components.device_automation.* homeassistant.components.elgato.* homeassistant.components.frontend.* homeassistant.components.geo_location.* +homeassistant.components.gios.* homeassistant.components.group.* homeassistant.components.history.* homeassistant.components.http.* diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 9c4b76d8009..ab956fe9da7 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -1,11 +1,18 @@ """The GIOS component.""" -import logging +from __future__ import annotations +import logging +from typing import Any, Dict, cast + +from aiohttp import ClientSession from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout from gios import ApiError, Gios, InvalidSensorsData, NoStationError +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import API_TIMEOUT, CONF_STATION_ID, DOMAIN, SCAN_INTERVAL @@ -15,10 +22,22 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["air_quality"] -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up GIOS as config entry.""" - station_id = entry.data[CONF_STATION_ID] - _LOGGER.debug("Using station_id: %s", station_id) + station_id: int = entry.data[CONF_STATION_ID] + _LOGGER.debug("Using station_id: %d", station_id) + + # We used to use int as config_entry unique_id, convert this to str. + if isinstance(entry.unique_id, int): # type: ignore[unreachable] + hass.config_entries.async_update_entry(entry, unique_id=str(station_id)) # type: ignore[unreachable] + + # We used to use int in device_entry identifiers, convert this to str. + device_registry = await async_get_registry(hass) + old_ids = (DOMAIN, station_id) + device_entry = device_registry.async_get_device({old_ids}) # type: ignore[arg-type] + if device_entry and entry.entry_id in device_entry.config_entries: + new_ids = (DOMAIN, str(station_id)) + device_registry.async_update_device(device_entry.id, new_identifiers={new_ids}) websession = async_get_clientsession(hass) @@ -33,26 +52,32 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - hass.data[DOMAIN].pop(entry.entry_id) - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok class GiosDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold GIOS data.""" - def __init__(self, hass, session, station_id): + def __init__( + self, hass: HomeAssistant, session: ClientSession, station_id: int + ) -> None: """Class to manage fetching GIOS data API.""" self.gios = Gios(station_id, session) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) - async def _async_update_data(self): + async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" try: with timeout(API_TIMEOUT): - return await self.gios.async_update() + return cast(Dict[str, Any], await self.gios.async_update()) except ( ApiError, NoStationError, diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py index 9e4df19e7ad..e74cec8e151 100644 --- a/homeassistant/components/gios/air_quality.py +++ b/homeassistant/components/gios/air_quality.py @@ -1,8 +1,18 @@ """Support for the GIOS service.""" +from __future__ import annotations + +from typing import Any, Optional, cast + from homeassistant.components.air_quality import AirQualityEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import GiosDataUpdateCoordinator from .const import ( API_AQI, API_CO, @@ -23,111 +33,107 @@ from .const import ( PARALLEL_UPDATES = 1 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Add a GIOS entities from a config_entry.""" - name = config_entry.data[CONF_NAME] + name = entry.data[CONF_NAME] - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([GiosAirQuality(coordinator, name)], False) + # We used to use int as entity unique_id, convert this to str. + entity_registry = await async_get_registry(hass) + old_entity_id = entity_registry.async_get_entity_id( + "air_quality", DOMAIN, coordinator.gios.station_id + ) + if old_entity_id is not None: + entity_registry.async_update_entity( + old_entity_id, new_unique_id=str(coordinator.gios.station_id) + ) - -def round_state(func): - """Round state.""" - - def _decorator(self): - res = func(self) - if isinstance(res, float): - return round(res) - return res - - return _decorator + async_add_entities([GiosAirQuality(coordinator, name)]) class GiosAirQuality(CoordinatorEntity, AirQualityEntity): """Define an GIOS sensor.""" - def __init__(self, coordinator, name): + coordinator: GiosDataUpdateCoordinator + + def __init__(self, coordinator: GiosDataUpdateCoordinator, name: str) -> None: """Initialize.""" super().__init__(coordinator) self._name = name - self._attrs = {} + self._attrs: dict[str, Any] = {} @property - def name(self): + def name(self) -> str: """Return the name.""" return self._name @property - def icon(self): + def icon(self) -> str: """Return the icon.""" - if self.air_quality_index in ICONS_MAP: + if self.air_quality_index is not None and self.air_quality_index in ICONS_MAP: return ICONS_MAP[self.air_quality_index] return "mdi:blur" @property - def air_quality_index(self): + def air_quality_index(self) -> str | None: """Return the air quality index.""" - return self._get_sensor_value(API_AQI) + return cast(Optional[str], self.coordinator.data.get(API_AQI, {}).get("value")) @property - @round_state - def particulate_matter_2_5(self): + def particulate_matter_2_5(self) -> float | None: """Return the particulate matter 2.5 level.""" - return self._get_sensor_value(API_PM25) + return round_state(self._get_sensor_value(API_PM25)) @property - @round_state - def particulate_matter_10(self): + def particulate_matter_10(self) -> float | None: """Return the particulate matter 10 level.""" - return self._get_sensor_value(API_PM10) + return round_state(self._get_sensor_value(API_PM10)) @property - @round_state - def ozone(self): + def ozone(self) -> float | None: """Return the O3 (ozone) level.""" - return self._get_sensor_value(API_O3) + return round_state(self._get_sensor_value(API_O3)) @property - @round_state - def carbon_monoxide(self): + def carbon_monoxide(self) -> float | None: """Return the CO (carbon monoxide) level.""" - return self._get_sensor_value(API_CO) + return round_state(self._get_sensor_value(API_CO)) @property - @round_state - def sulphur_dioxide(self): + def sulphur_dioxide(self) -> float | None: """Return the SO2 (sulphur dioxide) level.""" - return self._get_sensor_value(API_SO2) + return round_state(self._get_sensor_value(API_SO2)) @property - @round_state - def nitrogen_dioxide(self): + def nitrogen_dioxide(self) -> float | None: """Return the NO2 (nitrogen dioxide) level.""" - return self._get_sensor_value(API_NO2) + return round_state(self._get_sensor_value(API_NO2)) @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return ATTRIBUTION @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" - return self.coordinator.gios.station_id + return str(self.coordinator.gios.station_id) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" return { - "identifiers": {(DOMAIN, self.coordinator.gios.station_id)}, + "identifiers": {(DOMAIN, str(self.coordinator.gios.station_id))}, "name": DEFAULT_NAME, "manufacturer": MANUFACTURER, "entry_type": "service", } @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" # Different measuring stations have different sets of sensors. We don't know # what data we will get. @@ -139,8 +145,13 @@ class GiosAirQuality(CoordinatorEntity, AirQualityEntity): self._attrs[ATTR_STATION] = self.coordinator.gios.station_name return self._attrs - def _get_sensor_value(self, sensor): + def _get_sensor_value(self, sensor: str) -> float | None: """Return value of specified sensor.""" if sensor in self.coordinator.data: - return self.coordinator.data[sensor]["value"] + return cast(float, self.coordinator.data[sensor]["value"]) return None + + +def round_state(state: float | None) -> float | None: + """Round state.""" + return round(state) if state is not None else None diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index b351fafc0c1..161dc1b0add 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -1,5 +1,8 @@ """Adds config flow for GIOS.""" +from __future__ import annotations + import asyncio +from typing import Any from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout @@ -8,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import API_TIMEOUT, CONF_STATION_ID, DEFAULT_NAME, DOMAIN @@ -25,14 +29,16 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: try: await self.async_set_unique_id( - user_input[CONF_STATION_ID], raise_on_progress=False + str(user_input[CONF_STATION_ID]), raise_on_progress=False ) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 4d3d7e139ce..d16225d90a7 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -1,5 +1,8 @@ """Constants for GIOS integration.""" +from __future__ import annotations + from datetime import timedelta +from typing import Final from homeassistant.components.air_quality import ( ATTR_CO, @@ -10,33 +13,33 @@ from homeassistant.components.air_quality import ( ATTR_SO2, ) -ATTRIBUTION = "Data provided by GIOŚ" +ATTRIBUTION: Final = "Data provided by GIOŚ" -ATTR_STATION = "station" -CONF_STATION_ID = "station_id" -DEFAULT_NAME = "GIOŚ" +ATTR_STATION: Final = "station" +CONF_STATION_ID: Final = "station_id" +DEFAULT_NAME: Final = "GIOŚ" # Term of service GIOŚ allow downloading data no more than twice an hour. -SCAN_INTERVAL = timedelta(minutes=30) -DOMAIN = "gios" -MANUFACTURER = "Główny Inspektorat Ochrony Środowiska" +SCAN_INTERVAL: Final = timedelta(minutes=30) +DOMAIN: Final = "gios" +MANUFACTURER: Final = "Główny Inspektorat Ochrony Środowiska" -API_AQI = "aqi" -API_CO = "co" -API_NO2 = "no2" -API_O3 = "o3" -API_PM10 = "pm10" -API_PM25 = "pm2.5" -API_SO2 = "so2" +API_AQI: Final = "aqi" +API_CO: Final = "co" +API_NO2: Final = "no2" +API_O3: Final = "o3" +API_PM10: Final = "pm10" +API_PM25: Final = "pm2.5" +API_SO2: Final = "so2" -API_TIMEOUT = 30 +API_TIMEOUT: Final = 30 -AQI_GOOD = "dobry" -AQI_MODERATE = "umiarkowany" -AQI_POOR = "dostateczny" -AQI_VERY_GOOD = "bardzo dobry" -AQI_VERY_POOR = "zły" +AQI_GOOD: Final = "dobry" +AQI_MODERATE: Final = "umiarkowany" +AQI_POOR: Final = "dostateczny" +AQI_VERY_GOOD: Final = "bardzo dobry" +AQI_VERY_POOR: Final = "zły" -ICONS_MAP = { +ICONS_MAP: Final[dict[str, str]] = { AQI_VERY_GOOD: "mdi:emoticon-excited", AQI_GOOD: "mdi:emoticon-happy", AQI_MODERATE: "mdi:emoticon-neutral", @@ -44,7 +47,7 @@ ICONS_MAP = { AQI_VERY_POOR: "mdi:emoticon-dead", } -SENSOR_MAP = { +SENSOR_MAP: Final[dict[str, str]] = { API_CO: ATTR_CO, API_NO2: ATTR_NO2, API_O3: ATTR_OZONE, diff --git a/homeassistant/components/gios/system_health.py b/homeassistant/components/gios/system_health.py index 391a8c1affe..589dc428bcb 100644 --- a/homeassistant/components/gios/system_health.py +++ b/homeassistant/components/gios/system_health.py @@ -1,8 +1,12 @@ """Provide info to system health.""" +from __future__ import annotations + +from typing import Any, Final + from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback -API_ENDPOINT = "http://api.gios.gov.pl/" +API_ENDPOINT: Final = "http://api.gios.gov.pl/" @callback @@ -13,7 +17,7 @@ def async_register( register.async_register_info(system_health_info) -async def system_health_info(hass): +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" return { "can_reach_server": system_health.async_check_can_reach_url(hass, API_ENDPOINT) diff --git a/mypy.ini b/mypy.ini index 97507f0ec84..c62a0f18edf 100644 --- a/mypy.ini +++ b/mypy.ini @@ -176,6 +176,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.gios.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.group.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -760,9 +771,6 @@ ignore_errors = true [mypy-homeassistant.components.geniushub.*] ignore_errors = true -[mypy-homeassistant.components.gios.*] -ignore_errors = true - [mypy-homeassistant.components.glances.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 9b03210e9eb..c1dfa085e7f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -73,7 +73,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.fritzbox.*", "homeassistant.components.garmin_connect.*", "homeassistant.components.geniushub.*", - "homeassistant.components.gios.*", "homeassistant.components.glances.*", "homeassistant.components.gogogate2.*", "homeassistant.components.google_assistant.*", diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 729d0d50f61..537d6265125 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -17,7 +17,7 @@ async def init_integration(hass, incomplete_data=False) -> MockConfigEntry: entry = MockConfigEntry( domain=DOMAIN, title="Home", - unique_id=123, + unique_id="123", data={"station_id": 123, "name": "Home"}, ) diff --git a/tests/components/gios/test_air_quality.py b/tests/components/gios/test_air_quality.py index 873e1e089a3..b7ce8d1f97a 100644 --- a/tests/components/gios/test_air_quality.py +++ b/tests/components/gios/test_air_quality.py @@ -13,9 +13,10 @@ from homeassistant.components.air_quality import ( ATTR_PM_2_5, ATTR_PM_10, ATTR_SO2, + DOMAIN as AIR_QUALITY_DOMAIN, ) from homeassistant.components.gios.air_quality import ATTRIBUTION -from homeassistant.components.gios.const import AQI_GOOD +from homeassistant.components.gios.const import AQI_GOOD, DOMAIN from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ICON, @@ -55,7 +56,7 @@ async def test_air_quality(hass): entry = registry.async_get("air_quality.home") assert entry - assert entry.unique_id == 123 + assert entry.unique_id == "123" async def test_air_quality_with_incomplete_data(hass): @@ -83,7 +84,7 @@ async def test_air_quality_with_incomplete_data(hass): entry = registry.async_get("air_quality.home") assert entry - assert entry.unique_id == 123 + assert entry.unique_id == "123" async def test_availability(hass): @@ -122,3 +123,23 @@ async def test_availability(hass): assert state assert state.state != STATE_UNAVAILABLE assert state.state == "4" + + +async def test_migrate_unique_id(hass): + """Test migrate unique_id of the air_quality entity.""" + registry = er.async_get(hass) + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + AIR_QUALITY_DOMAIN, + DOMAIN, + 123, + suggested_object_id="home", + disabled_by=None, + ) + + await init_integration(hass) + + entry = registry.async_get("air_quality.home") + assert entry + assert entry.unique_id == "123" diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 830b3a198a5..6b1f829c4d8 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -102,4 +102,4 @@ async def test_create_entry(hass): assert result["title"] == CONFIG[CONF_STATION_ID] assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] - assert flow.context["unique_id"] == CONFIG[CONF_STATION_ID] + assert flow.context["unique_id"] == "123" diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py index 344afe4e047..85834571c86 100644 --- a/tests/components/gios/test_init.py +++ b/tests/components/gios/test_init.py @@ -1,4 +1,5 @@ """Test init of GIOS integration.""" +import json from unittest.mock import patch from homeassistant.components.gios.const import DOMAIN @@ -9,7 +10,9 @@ from homeassistant.config_entries import ( ) from homeassistant.const import STATE_UNAVAILABLE -from tests.common import MockConfigEntry +from . import STATIONS + +from tests.common import MockConfigEntry, load_fixture, mock_device_registry from tests.components.gios import init_integration @@ -53,3 +56,46 @@ async def test_unload_entry(hass): assert entry.state == ENTRY_STATE_NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_migrate_device_and_config_entry(hass): + """Test device_info identifiers and config entry migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id=123, + data={ + "station_id": 123, + "name": "Home", + }, + ) + + indexes = json.loads(load_fixture("gios/indexes.json")) + station = json.loads(load_fixture("gios/station.json")) + sensors = json.loads(load_fixture("gios/sensors.json")) + + with patch( + "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS + ), patch( + "homeassistant.components.gios.Gios._get_station", + return_value=station, + ), patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=sensors, + ), patch( + "homeassistant.components.gios.Gios._get_indexes", return_value=indexes + ): + config_entry.add_to_hass(hass) + + device_reg = mock_device_registry(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, 123)} + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + migrated_device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "123")} + ) + assert device_entry.id == migrated_device_entry.id From efa5c595598a94cc534cd917b39875e81f99f4b6 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 May 2021 11:18:20 -0400 Subject: [PATCH 328/852] Replace hand-rolled binary search with bisect_left (#50410) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `bisect` module exposes a `bisect_left` function which does basically what the bulk of `_lower_bound` does. From my tests, it is slightly faster (~5%) in the probably common ideal case where `arr` is short. In the worst case scenario, `bisect.bisect_left` is *much* faster. ``` >>> arr = list(range(60)) >>> cmp = 59 >>> %timeit _lower_bound(arr, cmp) 736 ns ± 6.24 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) >>> %timeit bisect_lower_bound(arr, cmp) 290 ns ± 7.77 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) ``` I doubt this is a huge bottleneck or anything, but I think it's a bit more readable, and it's more efficient, so it seems like it's mostly a win. This commit *will* add a new unconditional import for `bisect` when importing `util.dt`, and `bisect` is not currently imported for any of the standard library modules. It is possible to make this conditional by placing `import bisect` in the _lower_bound function, or in the function it's nested in. --- homeassistant/util/dt.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index c818c955370..28aebc5db47 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -1,6 +1,7 @@ """Helper methods to handle the time in Home Assistant.""" from __future__ import annotations +import bisect from contextlib import suppress import datetime as dt import re @@ -265,15 +266,7 @@ def find_next_time_expression_time( Return None if no such value exists. """ - left = 0 - right = len(arr) - while left < right: - mid = (left + right) // 2 - if arr[mid] < cmp: - left = mid + 1 - else: - right = mid - + left = bisect.bisect_left(arr, cmp) if left == len(arr): return None return arr[left] From d6c99a3db911c3e5ed7d98b5edf06373e62ffebd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 May 2021 17:28:17 +0200 Subject: [PATCH 329/852] Enable strict type checks for onewire (#50422) --- .strict-typing | 1 + homeassistant/components/onewire/__init__.py | 4 +- .../components/onewire/binary_sensor.py | 30 +++++--- .../components/onewire/config_flow.py | 37 +++++++--- homeassistant/components/onewire/const.py | 4 +- homeassistant/components/onewire/model.py | 21 ++++++ .../components/onewire/onewire_entities.py | 43 +++++------ .../components/onewire/onewirehub.py | 20 ++++-- homeassistant/components/onewire/sensor.py | 72 ++++++++++++++----- homeassistant/components/onewire/switch.py | 35 ++++++--- mypy.ini | 14 +++- script/hassfest/mypy_config.py | 1 - tests/components/onewire/const.py | 4 +- 13 files changed, 202 insertions(+), 84 deletions(-) create mode 100644 homeassistant/components/onewire/model.py diff --git a/.strict-typing b/.strict-typing index 97e3a467359..77e833bca7a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -31,6 +31,7 @@ homeassistant.components.media_player.* homeassistant.components.nam.* homeassistant.components.notify.* homeassistant.components.number.* +homeassistant.components.onewire.* homeassistant.components.persistent_notification.* homeassistant.components.proximity.* homeassistant.components.recorder.purge diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index fbcc5a5fe04..a27e1a49ab1 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -13,7 +13,7 @@ from .onewirehub import CannotConnect, OneWireHub _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up a 1-Wire proxy for a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 4e25ba431c3..9671a787c41 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -1,14 +1,21 @@ """Support for 1-Wire binary sensors.""" +from __future__ import annotations + import os from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_TYPE_OWSERVER, DOMAIN, SENSOR_TYPE_SENSED -from .onewire_entities import OneWireProxyEntity +from .model import DeviceComponentDescription +from .onewire_entities import OneWireBaseEntity, OneWireProxyEntity from .onewirehub import OneWireHub -DEVICE_BINARY_SENSORS = { +DEVICE_BINARY_SENSORS: dict[str, list[DeviceComponentDescription]] = { # Family : { path, sensor_type } "12": [ { @@ -77,7 +84,11 @@ DEVICE_BINARY_SENSORS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up 1-Wire platform.""" # Only OWServer implementation works with binary sensors if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: @@ -87,9 +98,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -def get_entities(onewirehub: OneWireHub): +def get_entities(onewirehub: OneWireHub) -> list[OneWireBaseEntity]: """Get a list of entities.""" - entities = [] + if not onewirehub.devices: + return [] + + entities: list[OneWireBaseEntity] = [] for device in onewirehub.devices: family = device["family"] @@ -98,7 +112,7 @@ def get_entities(onewirehub: OneWireHub): if family not in DEVICE_BINARY_SENSORS: continue - device_info = { + device_info: DeviceInfo = { "identifiers": {(DOMAIN, device_id)}, "manufacturer": "Maxim Integrated", "model": device_type, @@ -126,6 +140,6 @@ class OneWireProxyBinarySensor(OneWireProxyEntity, BinarySensorEntity): """Implementation of a 1-Wire binary sensor.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if sensor is on.""" - return self._state + return bool(self._state) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 4ae3b1468db..856eb95aa57 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -1,9 +1,14 @@ """Config flow for 1-Wire component.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_MOUNT_DIR, @@ -32,7 +37,9 @@ DATA_SCHEMA_MOUNTDIR = vol.Schema( ) -async def validate_input_owserver(hass: HomeAssistant, data): +async def validate_input_owserver( + hass: HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA_OWSERVER with values provided by the user. @@ -49,7 +56,9 @@ async def validate_input_owserver(hass: HomeAssistant, data): return {"title": host} -def is_duplicate_owserver_entry(hass: HomeAssistant, user_input): +def is_duplicate_owserver_entry( + hass: HomeAssistant, user_input: dict[str, Any] +) -> bool: """Check existing entries for matching host and port.""" for config_entry in hass.config_entries.async_entries(DOMAIN): if ( @@ -61,7 +70,9 @@ def is_duplicate_owserver_entry(hass: HomeAssistant, user_input): return False -async def validate_input_mount_dir(hass: HomeAssistant, data): +async def validate_input_mount_dir( + hass: HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA_MOUNTDIR with values provided by the user. @@ -82,16 +93,18 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize 1-Wire config flow.""" - self.onewire_config = {} + self.onewire_config: dict[str, Any] = {} - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle 1-Wire config flow start. Let user manually input configuration. """ - errors = {} + errors: dict[str, str] = {} if user_input is not None: self.onewire_config.update(user_input) if CONF_TYPE_OWSERVER == user_input[CONF_TYPE]: @@ -105,7 +118,9 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_owserver(self, user_input=None): + async def async_step_owserver( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle OWServer configuration.""" errors = {} if user_input: @@ -130,7 +145,9 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_mount_dir(self, user_input=None): + async def async_step_mount_dir( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle SysBus configuration.""" errors = {} if user_input: @@ -157,7 +174,7 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, platform_config): + async def async_step_import(self, platform_config: dict[str, Any]) -> FlowResult: """Handle import configuration from YAML.""" # OWServer if platform_config[CONF_TYPE] == CONF_TYPE_OWSERVER: diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 9dc67dbd9b8..d2c712c26c5 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -1,4 +1,6 @@ """Constants for 1-Wire component.""" +from __future__ import annotations + from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -44,7 +46,7 @@ SENSOR_TYPE_WETNESS = "wetness" SWITCH_TYPE_LATCH = "latch" SWITCH_TYPE_PIO = "pio" -SENSOR_TYPES = { +SENSOR_TYPES: dict[str, list[str | None]] = { # SensorType: [ Unit, DeviceClass ] SENSOR_TYPE_TEMPERATURE: [TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE], SENSOR_TYPE_HUMIDITY: [PERCENTAGE, DEVICE_CLASS_HUMIDITY], diff --git a/homeassistant/components/onewire/model.py b/homeassistant/components/onewire/model.py new file mode 100644 index 00000000000..8dc841f16ba --- /dev/null +++ b/homeassistant/components/onewire/model.py @@ -0,0 +1,21 @@ +"""Type definitions for 1-Wire integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class DeviceComponentDescription(TypedDict, total=False): + """Device component description class.""" + + path: str + name: str + type: str + default_disabled: bool + + +class OWServerDeviceDescription(TypedDict): + """OWServer device description class.""" + + path: str + family: str + type: str diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index 3927d2626ac..0182f86f6de 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -7,6 +7,7 @@ from typing import Any from pyownet import protocol from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.typing import StateType from .const import ( SENSOR_TYPE_COUNT, @@ -15,6 +16,7 @@ from .const import ( SWITCH_TYPE_LATCH, SWITCH_TYPE_PIO, ) +from .model import DeviceComponentDescription _LOGGER = logging.getLogger(__name__) @@ -24,13 +26,13 @@ class OneWireBaseEntity(Entity): def __init__( self, - name, - device_file, + name: str, + device_file: str, entity_type: str, - entity_name: str = None, - device_info: DeviceInfo | None = None, - default_disabled: bool = False, - unique_id: str = None, + entity_name: str, + device_info: DeviceInfo, + default_disabled: bool, + unique_id: str, ): """Initialize the entity.""" self._name = f"{name} {entity_name or entity_type.capitalize()}" @@ -39,10 +41,10 @@ class OneWireBaseEntity(Entity): self._device_class = SENSOR_TYPES[entity_type][1] self._unit_of_measurement = SENSOR_TYPES[entity_type][0] self._device_info = device_info - self._state = None - self._value_raw = None + self._state: StateType = None + self._value_raw: float | None = None self._default_disabled = default_disabled - self._unique_id = unique_id or device_file + self._unique_id = unique_id @property def name(self) -> str | None: @@ -84,7 +86,7 @@ class OneWireProxyEntity(OneWireBaseEntity): device_name: str, device_info: DeviceInfo, entity_path: str, - entity_specs: dict[str, Any], + entity_specs: DeviceComponentDescription, owproxy: protocol._Proxy, ): """Initialize the sensor.""" @@ -99,31 +101,30 @@ class OneWireProxyEntity(OneWireBaseEntity): ) self._owproxy = owproxy - def _read_value_ownet(self): + def _read_value_ownet(self) -> str: """Read a value from the owserver.""" - return self._owproxy.read(self._device_file).decode().lstrip() + read_bytes: bytes = self._owproxy.read(self._device_file) + return read_bytes.decode().lstrip() - def _write_value_ownet(self, value: bytes): + def _write_value_ownet(self, value: bytes) -> None: """Write a value to the owserver.""" - return self._owproxy.write(self._device_file, value) + self._owproxy.write(self._device_file, value) - def update(self): + def update(self) -> None: """Get the latest data from the device.""" - value = None try: self._value_raw = float(self._read_value_ownet()) except protocol.Error as exc: _LOGGER.error("Owserver failure in read(), got: %s", exc) + self._state = None else: if self._entity_type == SENSOR_TYPE_COUNT: - value = int(self._value_raw) + self._state = int(self._value_raw) elif self._entity_type in [ SENSOR_TYPE_SENSED, SWITCH_TYPE_LATCH, SWITCH_TYPE_PIO, ]: - value = int(self._value_raw) == 1 + self._state = int(self._value_raw) == 1 else: - value = round(self._value_raw, 1) - - self._state = value + self._state = round(self._value_raw, 1) diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 5f9e3bfff77..68ee0af85aa 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -1,4 +1,6 @@ """Hub for communication with 1-Wire server or mount_dir.""" +from __future__ import annotations + import os from pi1wire import Pi1Wire @@ -10,6 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from .const import CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS +from .model import OWServerDeviceDescription DEVICE_COUPLERS = { # Family : [branches] @@ -23,10 +26,10 @@ class OneWireHub: def __init__(self, hass: HomeAssistant): """Initialize.""" self.hass = hass - self.type: str = None - self.pi1proxy: Pi1Wire = None - self.owproxy: protocol._Proxy = None - self.devices = None + self.type: str | None = None + self.pi1proxy: Pi1Wire | None = None + self.owproxy: protocol._Proxy | None = None + self.devices: list | None = None async def connect(self, host: str, port: int) -> None: """Connect to the owserver host.""" @@ -54,10 +57,11 @@ class OneWireHub: await self.connect(host, port) await self.discover_devices() - async def discover_devices(self): + async def discover_devices(self) -> None: """Discover all devices.""" if self.devices is None: if self.type == CONF_TYPE_SYSBUS: + assert self.pi1proxy self.devices = await self.hass.async_add_executor_job( self.pi1proxy.find_all_sensors ) @@ -65,11 +69,13 @@ class OneWireHub: self.devices = await self.hass.async_add_executor_job( self._discover_devices_owserver ) - return self.devices - def _discover_devices_owserver(self, path="/"): + def _discover_devices_owserver( + self, path: str = "/" + ) -> list[OWServerDeviceDescription]: """Discover all owserver devices.""" devices = [] + assert self.owproxy for device_path in self.owproxy.dir(path): device_family = self.owproxy.read(f"{device_path}family").decode() device_type = self.owproxy.read(f"{device_path}type").decode() diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 02de1ed463e..ba202ff24f2 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -4,15 +4,20 @@ from __future__ import annotations import asyncio import logging import os +from types import MappingProxyType +from typing import Any -from pi1wire import InvalidCRCException, UnsupportResponseException +from pi1wire import InvalidCRCException, OneWireInterface, UnsupportResponseException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import StateType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import DiscoveryInfoType, StateType from .const import ( CONF_MOUNT_DIR, @@ -32,12 +37,13 @@ from .const import ( SENSOR_TYPE_VOLTAGE, SENSOR_TYPE_WETNESS, ) +from .model import DeviceComponentDescription from .onewire_entities import OneWireBaseEntity, OneWireProxyEntity from .onewirehub import OneWireHub _LOGGER = logging.getLogger(__name__) -DEVICE_SENSORS = { +DEVICE_SENSORS: dict[str, list[DeviceComponentDescription]] = { # Family : { SensorType: owfs path } "10": [ {"path": "temperature", "name": "Temperature", "type": SENSOR_TYPE_TEMPERATURE} @@ -145,7 +151,7 @@ DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"] # These can only be read by OWFS. Currently this driver only supports them # via owserver (network protocol) -HOBBYBOARD_EF = { +HOBBYBOARD_EF: dict[str, list[DeviceComponentDescription]] = { "HobbyBoards_EF": [ { "path": "humidity/humidity_corrected", @@ -189,7 +195,7 @@ HOBBYBOARD_EF = { # 7E sensors are special sensors by Embedded Data Systems -EDS_SENSORS = { +EDS_SENSORS: dict[str, list[DeviceComponentDescription]] = { "EDS0068": [ { "path": "EDS0068/temperature", @@ -225,7 +231,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_sensor_types(device_sub_type): +def get_sensor_types(device_sub_type: str) -> dict[str, Any]: """Return the proper info array for the device type.""" if "HobbyBoard" in device_sub_type: return HOBBYBOARD_EF @@ -234,7 +240,12 @@ def get_sensor_types(device_sub_type): return DEVICE_SENSORS -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: dict[str, Any], + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Old way of setting up 1-Wire platform.""" _LOGGER.warning( "Loading 1-Wire via platform setup is deprecated. " @@ -253,7 +264,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up 1-Wire platform.""" onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job( @@ -262,9 +277,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -def get_entities(onewirehub: OneWireHub, config): +def get_entities( + onewirehub: OneWireHub, config: MappingProxyType[str, Any] +) -> list[OneWireBaseEntity]: """Get a list of entities.""" - entities = [] + if not onewirehub.devices: + return [] + + entities: list[OneWireBaseEntity] = [] device_names = {} if CONF_NAMES in config and isinstance(config[CONF_NAMES], dict): device_names = config[CONF_NAMES] @@ -272,6 +292,7 @@ def get_entities(onewirehub: OneWireHub, config): conf_type = config[CONF_TYPE] # We have an owserver on a remote(or local) host/port if conf_type == CONF_TYPE_OWSERVER: + assert onewirehub.owproxy for device in onewirehub.devices: family = device["family"] device_type = device["type"] @@ -292,7 +313,7 @@ def get_entities(onewirehub: OneWireHub, config): device_id, ) continue - device_info = { + device_info: DeviceInfo = { "identifiers": {(DOMAIN, device_id)}, "manufacturer": "Maxim Integrated", "model": device_type, @@ -384,9 +405,23 @@ class OneWireProxySensor(OneWireProxyEntity, OneWireSensor): class OneWireDirectSensor(OneWireSensor): """Implementation of a 1-Wire sensor directly connected to RPI GPIO.""" - def __init__(self, name, device_file, device_info, owsensor): + def __init__( + self, + name: str, + device_file: str, + device_info: DeviceInfo, + owsensor: OneWireInterface, + ) -> None: """Initialize the sensor.""" - super().__init__(name, device_file, "temperature", "Temperature", device_info) + super().__init__( + name, + device_file, + "temperature", + "Temperature", + device_info, + False, + device_file, + ) self._owsensor = owsensor @property @@ -394,7 +429,7 @@ class OneWireDirectSensor(OneWireSensor): """Return the state of the entity.""" return self._state - async def get_temperature(self): + async def get_temperature(self) -> float: """Get the latest data from the device.""" attempts = 1 while True: @@ -414,16 +449,15 @@ class OneWireDirectSensor(OneWireSensor): if attempts > 10: raise - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from the device.""" - value = None try: self._value_raw = await self.get_temperature() - value = round(float(self._value_raw), 1) + self._state = round(self._value_raw, 1) except ( FileNotFoundError, InvalidCRCException, UnsupportResponseException, ) as ex: _LOGGER.warning("Cannot read from sensor %s: %s", self._device_file, ex) - self._state = value + self._state = None diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index 1753800fbf0..ed7b0df73a1 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -1,15 +1,23 @@ """Support for 1-Wire environment switches.""" +from __future__ import annotations + import logging import os +from typing import Any from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_TYPE_OWSERVER, DOMAIN, SWITCH_TYPE_LATCH, SWITCH_TYPE_PIO -from .onewire_entities import OneWireProxyEntity +from .model import DeviceComponentDescription +from .onewire_entities import OneWireBaseEntity, OneWireProxyEntity from .onewirehub import OneWireHub -DEVICE_SWITCHES = { +DEVICE_SWITCHES: dict[str, list[DeviceComponentDescription]] = { # Family : { owfs path } "12": [ { @@ -140,7 +148,11 @@ DEVICE_SWITCHES = { LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up 1-Wire platform.""" # Only OWServer implementation works with switches if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: @@ -150,9 +162,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -def get_entities(onewirehub: OneWireHub): +def get_entities(onewirehub: OneWireHub) -> list[OneWireBaseEntity]: """Get a list of entities.""" - entities = [] + if not onewirehub.devices: + return [] + + entities: list[OneWireBaseEntity] = [] for device in onewirehub.devices: family = device["family"] @@ -162,7 +177,7 @@ def get_entities(onewirehub: OneWireHub): if family not in DEVICE_SWITCHES: continue - device_info = { + device_info: DeviceInfo = { "identifiers": {(DOMAIN, device_id)}, "manufacturer": "Maxim Integrated", "model": device_type, @@ -190,14 +205,14 @@ class OneWireProxySwitch(OneWireProxyEntity, SwitchEntity): """Implementation of a 1-Wire switch.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if sensor is on.""" - return self._state + return bool(self._state) - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self._write_value_ownet(b"1") - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self._write_value_ownet(b"0") diff --git a/mypy.ini b/mypy.ini index c62a0f18edf..92c40569fac 100644 --- a/mypy.ini +++ b/mypy.ini @@ -352,6 +352,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.onewire.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.persistent_notification.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1008,9 +1019,6 @@ ignore_errors = true [mypy-homeassistant.components.ondilo_ico.*] ignore_errors = true -[mypy-homeassistant.components.onewire.*] -ignore_errors = true - [mypy-homeassistant.components.onvif.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index c1dfa085e7f..f7ec869b654 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -152,7 +152,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.omnilogic.*", "homeassistant.components.onboarding.*", "homeassistant.components.ondilo_ico.*", - "homeassistant.components.onewire.*", "homeassistant.components.onvif.*", "homeassistant.components.ovo_energy.*", "homeassistant.components.ozw.*", diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index ccae8e695ce..a58528ab55f 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -884,7 +884,7 @@ MOCK_SYSBUS_DEVICES = { { "entity_id": "sensor.42_111111111112_temperature", "unique_id": "/sys/bus/w1/devices/42-111111111112/w1_slave", - "injected_value": [UnsupportResponseException] * 9 + ["27.993"], + "injected_value": [UnsupportResponseException] * 9 + [27.993], "result": "28.0", "unit": TEMP_CELSIUS, "class": DEVICE_CLASS_TEMPERATURE, @@ -902,7 +902,7 @@ MOCK_SYSBUS_DEVICES = { { "entity_id": "sensor.42_111111111113_temperature", "unique_id": "/sys/bus/w1/devices/42-111111111113/w1_slave", - "injected_value": [UnsupportResponseException] * 10 + ["27.993"], + "injected_value": [UnsupportResponseException] * 10 + [27.993], "result": "unknown", "unit": TEMP_CELSIUS, "class": DEVICE_CLASS_TEMPERATURE, From e616583badc9b7cd7dd9f04be05117f38b5af38d Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 11 May 2021 17:41:27 +0200 Subject: [PATCH 330/852] Improve types for Fritz (#50327) Co-authored-by: Ruslan Sayfutdinov Co-authored-by: Maciej Bieniek --- homeassistant/components/fritz/sensor.py | 71 +++++++++++++----------- mypy.ini | 3 - script/hassfest/mypy_config.py | 1 - 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 21c121ea295..19c2859b7f0 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -3,13 +3,16 @@ from __future__ import annotations import datetime import logging +from typing import Callable, TypedDict from fritzconnection.core.exceptions import FritzConnectionException +from fritzconnection.lib.fritzstatus import FritzStatus from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from .common import FritzBoxBaseEntity, FritzBoxTools @@ -18,7 +21,7 @@ from .const import DOMAIN, UPTIME_DEVIATION _LOGGER = logging.getLogger(__name__) -def _retrieve_uptime_state(status, last_value): +def _retrieve_uptime_state(status: FritzStatus, last_value: str) -> str: """Return uptime from device.""" delta_uptime = utcnow() - datetime.timedelta(seconds=status.uptime) @@ -34,30 +37,38 @@ def _retrieve_uptime_state(status, last_value): return last_value -def _retrieve_external_ip_state(status, last_value): +def _retrieve_external_ip_state(status: FritzStatus, last_value: str) -> str: """Return external ip from device.""" - return status.external_ip + return status.external_ip # type: ignore[no-any-return] -SENSOR_NAME = 0 -SENSOR_DEVICE_CLASS = 1 -SENSOR_ICON = 2 -SENSOR_STATE_PROVIDER = 3 +class SensorData(TypedDict): + """Sensor data class.""" + + name: str + device_class: str | None + icon: str | None + state_provider: Callable + -# sensor_type: [name, device_class, icon, state_provider] SENSOR_DATA = { - "external_ip": [ - "External IP", - None, - "mdi:earth", - _retrieve_external_ip_state, - ], - "uptime": ["Uptime", DEVICE_CLASS_TIMESTAMP, None, _retrieve_uptime_state], + "external_ip": SensorData( + name="External IP", + device_class=None, + icon="mdi:earth", + state_provider=_retrieve_external_ip_state, + ), + "uptime": SensorData( + name="Uptime", + device_class=DEVICE_CLASS_TIMESTAMP, + icon=None, + state_provider=_retrieve_uptime_state, + ), } async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up entry.""" _LOGGER.debug("Setting up FRITZ!Box sensors") @@ -81,36 +92,36 @@ class FritzBoxSensor(FritzBoxBaseEntity, BinarySensorEntity): self, fritzbox_tools: FritzBoxTools, device_friendlyname: str, sensor_type: str ) -> None: """Init FRITZ!Box connectivity class.""" - self._sensor_data = SENSOR_DATA[sensor_type] + self._sensor_data: SensorData = SENSOR_DATA[sensor_type] self._unique_id = f"{fritzbox_tools.unique_id}-{sensor_type}" - self._name = f"{device_friendlyname} {self._sensor_data[SENSOR_NAME]}" + self._name = f"{device_friendlyname} {self._sensor_data['name']}" self._is_available = True self._last_value: str | None = None self._state: str | None = None super().__init__(fritzbox_tools, device_friendlyname) @property - def _state_provider(self): + def _state_provider(self) -> Callable: """Return the state provider for the binary sensor.""" - return self._sensor_data[SENSOR_STATE_PROVIDER] + return self._sensor_data["state_provider"] @property - def name(self): + def name(self) -> str: """Return name.""" return self._name @property def device_class(self) -> str | None: """Return device class.""" - return self._sensor_data[SENSOR_DEVICE_CLASS] + return self._sensor_data["device_class"] @property - def icon(self): + def icon(self) -> str | None: """Return icon.""" - return self._sensor_data[SENSOR_ICON] + return self._sensor_data["icon"] @property - def unique_id(self): + def unique_id(self) -> str: """Return unique id.""" return self._unique_id @@ -129,13 +140,11 @@ class FritzBoxSensor(FritzBoxBaseEntity, BinarySensorEntity): _LOGGER.debug("Updating FRITZ!Box sensors") try: - status = self._fritzbox_tools.fritzstatus + status: FritzStatus = self._fritzbox_tools.fritzstatus self._is_available = True - - self._state = self._last_value = self._state_provider( - status, self._last_value - ) - except FritzConnectionException: _LOGGER.error("Error getting the state from the FRITZ!Box", exc_info=True) self._is_available = False + return + + self._state = self._last_value = self._state_provider(status, self._last_value) diff --git a/mypy.ini b/mypy.ini index 92c40569fac..d501c482dd6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -770,9 +770,6 @@ ignore_errors = true [mypy-homeassistant.components.freebox.*] ignore_errors = true -[mypy-homeassistant.components.fritz.*] -ignore_errors = true - [mypy-homeassistant.components.fritzbox.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f7ec869b654..0bc97412bc0 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -69,7 +69,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.fortios.*", "homeassistant.components.foscam.*", "homeassistant.components.freebox.*", - "homeassistant.components.fritz.*", "homeassistant.components.fritzbox.*", "homeassistant.components.garmin_connect.*", "homeassistant.components.geniushub.*", From 909a20b36d4df6724c955c2ae28cb82fe6d50c2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 11:03:36 -0500 Subject: [PATCH 331/852] Use async zeroconf registration functions (#50168) --- homeassistant/components/zeroconf/__init__.py | 40 +++++++++++-------- homeassistant/components/zeroconf/models.py | 18 +++++++++ .../components/homekit_controller/conftest.py | 2 +- tests/conftest.py | 2 +- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 58d8ad21094..bf717141f11 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Iterable from contextlib import suppress import fnmatch -from functools import partial import ipaddress from ipaddress import ip_address import logging @@ -33,11 +32,10 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url -from homeassistant.helpers.singleton import singleton -from homeassistant.loader import async_get_homekit, async_get_zeroconf +from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass from homeassistant.util.network import is_loopback -from .models import HaServiceBrowser, HaZeroconf +from .models import HaAsyncZeroconf, HaServiceBrowser, HaZeroconf from .usage import install_multiple_zeroconf_catcher _LOGGER = logging.getLogger(__name__) @@ -92,16 +90,26 @@ class HaServiceInfo(TypedDict): properties: dict[str, Any] -@singleton(DOMAIN) +@bind_hass async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: + """Zeroconf instance to be shared with other integrations that use it.""" + return cast(HaZeroconf, (await _async_get_instance(hass)).zeroconf) + + +@bind_hass +async def async_get_async_instance(hass: HomeAssistant) -> HaAsyncZeroconf: """Zeroconf instance to be shared with other integrations that use it.""" return await _async_get_instance(hass) -async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaZeroconf: +async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZeroconf: + if DOMAIN in hass.data: + return cast(HaAsyncZeroconf, hass.data[DOMAIN]) + logging.getLogger("zeroconf").setLevel(logging.NOTSET) - zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs)) + aio_zc = HaAsyncZeroconf(**zcargs) + zeroconf = cast(HaZeroconf, aio_zc.zeroconf) install_multiple_zeroconf_catcher(zeroconf) @@ -110,8 +118,9 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaZeroconf: zeroconf.ha_close() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf) + hass.data[DOMAIN] = aio_zc - return zeroconf + return aio_zc def _get_ip_route(dst_ip: str) -> Any: @@ -171,7 +180,8 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: if not zc_config.get(CONF_IPV6, DEFAULT_IPV6): zc_args["ip_version"] = IPVersion.V4Only - zeroconf = hass.data[DOMAIN] = await _async_get_instance(hass, **zc_args) + aio_zc = await _async_get_instance(hass, **zc_args) + zeroconf = aio_zc.zeroconf async def _async_zeroconf_hass_start(_event: Event) -> None: """Expose Home Assistant on zeroconf when it starts. @@ -179,9 +189,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: Wait till started or otherwise HTTP is not up and running. """ uuid = await hass.helpers.instance_id.async_get() - await hass.async_add_executor_job( - _register_hass_zc_service, hass, zeroconf, uuid - ) + await _async_register_hass_zc_service(hass, aio_zc, uuid) async def _async_zeroconf_hass_started(_event: Event) -> None: """Start the service browser.""" @@ -196,8 +204,8 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return True -def _register_hass_zc_service( - hass: HomeAssistant, zeroconf: HaZeroconf, uuid: str +async def _async_register_hass_zc_service( + hass: HomeAssistant, aio_zc: HaAsyncZeroconf, uuid: str ) -> None: # Get instance UUID valid_location_name = _truncate_location_name_to_valid(hass.config.location_name) @@ -244,7 +252,7 @@ def _register_hass_zc_service( _LOGGER.info("Starting Zeroconf broadcast") try: - zeroconf.register_service(info) + await aio_zc.async_register_service(info) except NonUniqueNameException: _LOGGER.error( "Home Assistant instance with identical name present in the local network" @@ -252,7 +260,7 @@ def _register_hass_zc_service( async def _async_start_zeroconf_browser( - hass: HomeAssistant, zeroconf: HaZeroconf + hass: HomeAssistant, zeroconf: Zeroconf ) -> None: """Start the zeroconf browser.""" diff --git a/homeassistant/components/zeroconf/models.py b/homeassistant/components/zeroconf/models.py index 02a6fc7cdaa..c09e6428f2a 100644 --- a/homeassistant/components/zeroconf/models.py +++ b/homeassistant/components/zeroconf/models.py @@ -1,6 +1,10 @@ """Models for Zeroconf.""" +import asyncio +from typing import Any + from zeroconf import DNSPointer, DNSRecord, ServiceBrowser, Zeroconf +from zeroconf.asyncio import AsyncZeroconf class HaZeroconf(Zeroconf): @@ -12,6 +16,20 @@ class HaZeroconf(Zeroconf): ha_close = Zeroconf.close +class HaAsyncZeroconf(AsyncZeroconf): + """Home Assistant version of AsyncZeroconf.""" + + def __init__( # pylint: disable=super-init-not-called + self, *args: Any, **kwargs: Any + ) -> None: + """Wrap AsyncZeroconf.""" + self.zeroconf = HaZeroconf(*args, **kwargs) + self.loop = asyncio.get_running_loop() + + async def async_close(self) -> None: + """Fake method to avoid integrations closing it.""" + + class HaServiceBrowser(ServiceBrowser): """ServiceBrowser that only consumes DNSPointer records.""" diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 3cde3912709..266fa177fb2 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -14,7 +14,7 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 @pytest.fixture(autouse=True) def mock_zeroconf(): """Mock zeroconf.""" - with mock.patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: + with mock.patch("homeassistant.components.zeroconf.models.HaZeroconf") as mock_zc: yield mock_zc.return_value diff --git a/tests/conftest.py b/tests/conftest.py index 3fc2dc748cb..2a453a8dad1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -478,7 +478,7 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): @pytest.fixture def mock_zeroconf(): """Mock zeroconf.""" - with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: + with patch("homeassistant.components.zeroconf.models.HaZeroconf") as mock_zc: yield mock_zc.return_value From 0fdc50408ac65ad3e76c5b56e96a8ccd4fbff1d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 12:08:13 -0500 Subject: [PATCH 332/852] Remove unused ignore in fritz (#50469) --- homeassistant/components/fritz/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 19c2859b7f0..5c01552582f 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -39,7 +39,7 @@ def _retrieve_uptime_state(status: FritzStatus, last_value: str) -> str: def _retrieve_external_ip_state(status: FritzStatus, last_value: str) -> str: """Return external ip from device.""" - return status.external_ip # type: ignore[no-any-return] + return status.external_ip class SensorData(TypedDict): From d6a202bd7430624025a42a0c609a47f44c50ad3c Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 11 May 2021 12:36:40 -0500 Subject: [PATCH 333/852] Move core Sonos functionality out of entities (#50277) --- homeassistant/components/sonos/__init__.py | 1 - .../components/sonos/binary_sensor.py | 4 +- homeassistant/components/sonos/const.py | 7 +- homeassistant/components/sonos/entity.py | 14 +- homeassistant/components/sonos/helpers.py | 32 + .../components/sonos/media_player.py | 877 +++--------------- homeassistant/components/sonos/sensor.py | 4 +- homeassistant/components/sonos/speaker.py | 613 +++++++++++- tests/components/sonos/test_media_player.py | 20 +- 9 files changed, 733 insertions(+), 839 deletions(-) create mode 100644 homeassistant/components/sonos/helpers.py diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index dbbeecdcdb3..cdc1169f9f7 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -66,7 +66,6 @@ class SonosData: def __init__(self) -> None: """Initialize the data.""" self.discovered: dict[str, SonosSpeaker] = {} - self.media_player_entities = {} self.topology_condition = asyncio.Condition() self.discovery_thread = None self.hosts_heartbeat = None diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 558ee1ee25d..9fd81a1f006 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import SONOS_CREATE_BATTERY -from .entity import SonosSensorEntity +from .entity import SonosEntity from .speaker import SonosSpeaker _LOGGER = logging.getLogger(__name__) @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class SonosPowerEntity(SonosSensorEntity, BinarySensorEntity): +class SonosPowerEntity(SonosEntity, BinarySensorEntity): """Representation of a Sonos power entity.""" @property diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 133bf773991..6cecf5169d1 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -128,18 +128,17 @@ PLAYABLE_MEDIA_TYPES = [ MEDIA_TYPE_TRACK, ] -SONOS_CONTENT_UPDATE = "sonos_content_update" SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_ENTITY_UPDATE = "sonos_entity_update" SONOS_GROUP_UPDATE = "sonos_group_update" -SONOS_MEDIA_UPDATE = "sonos_media_update" -SONOS_PLAYER_RECONNECTED = "sonos_player_reconnected" SONOS_STATE_UPDATED = "sonos_state_updated" -SONOS_VOLUME_UPDATE = "sonos_properties_update" SONOS_SEEN = "sonos_seen" +SOURCE_LINEIN = "Line-in" +SOURCE_TV = "TV" + BATTERY_SCAN_INTERVAL = datetime.timedelta(minutes=15) SCAN_INTERVAL = datetime.timedelta(seconds=10) DISCOVERY_INTERVAL = datetime.timedelta(seconds=60) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index f7319e483d3..146725f90e2 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -48,6 +48,9 @@ class SonosEntity(Entity): self.async_write_ha_state, ) ) + async_dispatcher_send( + self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain + ) @property def soco(self) -> SoCo: @@ -76,14 +79,3 @@ class SonosEntity(Entity): def should_poll(self) -> bool: """Return that we should not be polled (we handle that internally).""" return False - - -class SonosSensorEntity(SonosEntity): - """Representation of a Sonos sensor entity.""" - - async def async_added_to_hass(self) -> None: - """Handle common setup when added to hass.""" - await super().async_added_to_hass() - async_dispatcher_send( - self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain - ) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py new file mode 100644 index 00000000000..6f22d8ab417 --- /dev/null +++ b/homeassistant/components/sonos/helpers.py @@ -0,0 +1,32 @@ +"""Helper methods for common tasks.""" +from __future__ import annotations + +import functools as ft +import logging +from typing import Any, Callable + +from pysonos.exceptions import SoCoException, SoCoUPnPException + +_LOGGER = logging.getLogger(__name__) + + +def soco_error(errorcodes: list[str] | None = None) -> Callable: + """Filter out specified UPnP errors from logs and avoid exceptions.""" + + def decorator(funct: Callable) -> Callable: + """Decorate functions.""" + + @ft.wraps(funct) + def wrapper(*args: Any, **kwargs: Any) -> Any: + """Wrap for all soco UPnP exception.""" + try: + return funct(*args, **kwargs) + except SoCoUPnPException as err: + if not errorcodes or err.error_code not in errorcodes: + _LOGGER.error("Error on %s with %s", funct.__name__, err) + except SoCoException as err: + _LOGGER.error("Error on %s with %s", funct.__name__, err) + + return wrapper + + return decorator diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index c3971852ac6..2150cd3a464 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1,30 +1,19 @@ """Support to interface with Sonos players.""" from __future__ import annotations -import asyncio -from collections.abc import Coroutine -from contextlib import suppress import datetime -import functools as ft import logging -from typing import Any, Callable +from typing import Any import urllib.parse -import async_timeout from pysonos import alarms from pysonos.core import ( MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, - MUSIC_SRC_TV, PLAY_MODE_BY_MEANING, PLAY_MODES, - SoCo, ) -from pysonos.data_structures import DidlFavorite -from pysonos.events_base import Event as SonosEvent from pysonos.exceptions import SoCoException, SoCoUPnPException -import pysonos.music_library -import pysonos.snapshot import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity @@ -60,30 +49,22 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv, entity_platform, service -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.network import is_internal_request -from homeassistant.util.dt import utcnow from .const import ( - DATA_SONOS, DOMAIN as SONOS_DOMAIN, MEDIA_TYPES_TO_SONOS, PLAYABLE_MEDIA_TYPES, - SONOS_CONTENT_UPDATE, SONOS_CREATE_MEDIA_PLAYER, - SONOS_ENTITY_CREATED, - SONOS_GROUP_UPDATE, - SONOS_MEDIA_UPDATE, - SONOS_PLAYER_RECONNECTED, - SONOS_VOLUME_UPDATE, + SOURCE_LINEIN, + SOURCE_TV, ) from .entity import SonosEntity +from .helpers import soco_error from .media_browser import build_item_response, get_media, library_payload -from .speaker import SonosSpeaker +from .speaker import SonosMedia, SonosSpeaker _LOGGER = logging.getLogger(__name__) @@ -104,8 +85,7 @@ SUPPORT_SONOS = ( | SUPPORT_VOLUME_SET ) -SOURCE_LINEIN = "Line-in" -SOURCE_TV = "TV" +VOLUME_INCREMENT = 2 REPEAT_TO_SONOS = { REPEAT_MODE_OFF: False, @@ -142,8 +122,6 @@ ATTR_SPEECH_ENHANCE = "speech_enhance" ATTR_QUEUE_POSITION = "queue_position" ATTR_STATUS_LIGHT = "status_light" -UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} - async def async_setup_entry( hass: HomeAssistant, @@ -167,27 +145,29 @@ async def async_setup_entry( if not entities: return + speakers = [] for entity in entities: assert isinstance(entity, SonosMediaPlayerEntity) + speakers.append(entity.speaker) if service_call.service == SERVICE_JOIN: master = platform.entities.get(service_call.data[ATTR_MASTER]) if master: - await SonosMediaPlayerEntity.join_multi(hass, master, entities) # type: ignore[arg-type] + await SonosSpeaker.join_multi(hass, master.speaker, speakers) # type: ignore[arg-type] else: _LOGGER.error( "Invalid master specified for join service: %s", service_call.data[ATTR_MASTER], ) elif service_call.service == SERVICE_UNJOIN: - await SonosMediaPlayerEntity.unjoin_multi(hass, entities) # type: ignore[arg-type] + await SonosSpeaker.unjoin_multi(hass, speakers) # type: ignore[arg-type] elif service_call.service == SERVICE_SNAPSHOT: - await SonosMediaPlayerEntity.snapshot_multi( - hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] + await SonosSpeaker.snapshot_multi( + hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] ) elif service_call.service == SERVICE_RESTORE: - await SonosMediaPlayerEntity.restore_multi( - hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] + await SonosSpeaker.restore_multi( + hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] ) config_entry.async_on_unload( @@ -267,134 +247,13 @@ async def async_setup_entry( ) -def _get_entity_from_soco_uid( - hass: HomeAssistant, uid: str -) -> SonosMediaPlayerEntity | None: - """Return SonosMediaPlayerEntity from SoCo uid.""" - return hass.data[DATA_SONOS].media_player_entities.get(uid) # type: ignore[no-any-return] - - -def soco_error(errorcodes: list[str] | None = None) -> Callable: - """Filter out specified UPnP errors from logs and avoid exceptions.""" - - def decorator(funct: Callable) -> Callable: - """Decorate functions.""" - - @ft.wraps(funct) - def wrapper(*args: Any, **kwargs: Any) -> Any: - """Wrap for all soco UPnP exception.""" - try: - return funct(*args, **kwargs) - except SoCoUPnPException as err: - if not errorcodes or err.error_code not in errorcodes: - _LOGGER.error("Error on %s with %s", funct.__name__, err) - except SoCoException as err: - _LOGGER.error("Error on %s with %s", funct.__name__, err) - - return wrapper - - return decorator - - -def soco_coordinator(funct: Callable) -> Callable: - """Call function on coordinator.""" - - @ft.wraps(funct) - def wrapper(entity: SonosMediaPlayerEntity, *args: Any, **kwargs: Any) -> Any: - """Wrap for call to coordinator.""" - if entity.is_coordinator: - return funct(entity, *args, **kwargs) - return funct(entity.coordinator, *args, **kwargs) - - return wrapper - - -def _timespan_secs(timespan: str | None) -> None | float: - """Parse a time-span into number of seconds.""" - if timespan in UNAVAILABLE_VALUES: - return None - - assert timespan is not None - return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))) - - class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Representation of a Sonos entity.""" - def __init__(self, speaker: SonosSpeaker) -> None: - """Initialize the Sonos entity.""" - super().__init__(speaker) - self._volume_increment = 2 - self._player_volume: int | None = None - self._player_muted: bool | None = None - self._play_mode: str | None = None - self._coordinator: SonosMediaPlayerEntity | None = None - self._sonos_group: list[SonosMediaPlayerEntity] = [self] - self._status: str | None = None - self._uri: str | None = None - self._media_library = pysonos.music_library.MusicLibrary(self.soco) - self._media_duration: float | None = None - self._media_position: float | None = None - self._media_position_updated_at: datetime.datetime | None = None - self._media_image_url: str | None = None - self._media_channel: str | None = None - self._media_artist: str | None = None - self._media_album_name: str | None = None - self._media_title: str | None = None - self._queue_position: int | None = None - self._night_sound: bool | None = None - self._speech_enhance: bool | None = None - self._source_name: str | None = None - self._favorites: list[DidlFavorite] = [] - self._soco_snapshot: pysonos.snapshot.Snapshot | None = None - self._snapshot_group: list[SonosMediaPlayerEntity] | None = None - - async def async_added_to_hass(self) -> None: - """Subscribe sonos events.""" - self.hass.data[DATA_SONOS].media_player_entities[self.unique_id] = self - await self.async_reconnect_player() - await super().async_added_to_hass() - - self.async_on_remove( - async_dispatcher_connect( - self.hass, SONOS_GROUP_UPDATE, self.async_update_groups - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SONOS_CONTENT_UPDATE}-{self.soco.uid}", - self.async_update_content, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SONOS_MEDIA_UPDATE}-{self.soco.uid}", - self.async_update_media, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SONOS_VOLUME_UPDATE}-{self.soco.uid}", - self.async_update_volume, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SONOS_PLAYER_RECONNECTED}-{self.soco.uid}", - self.async_reconnect_player, - ) - ) - - if self.hass.is_running: - async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE) - - async_dispatcher_send( - self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain - ) + @property + def coordinator(self) -> SonosSpeaker: + """Return the current coordinator SonosSpeaker.""" + return self.speaker.coordinator or self.speaker @property def unique_id(self) -> str: @@ -411,59 +270,21 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): return self.speaker.zone_name # type: ignore[no-any-return] @property # type: ignore[misc] - @soco_coordinator def state(self) -> str: """Return the state of the entity.""" - if self._status in ( + if self.media.playback_status in ( "PAUSED_PLAYBACK", "STOPPED", ): # Sonos can consider itself "paused" but without having media loaded # (happens if playing Spotify and via Spotify app you pick another device to play on) - if self.media_title is None: + if self.media.title is None: return STATE_IDLE return STATE_PAUSED - if self._status in ("PLAYING", "TRANSITIONING"): + if self.media.playback_status in ("PLAYING", "TRANSITIONING"): return STATE_PLAYING return STATE_IDLE - @property - def is_coordinator(self) -> bool: - """Return true if player is a coordinator.""" - return self._coordinator is None - - @property - def coordinator(self) -> SoCo: - """Return coordinator of this player.""" - return self._coordinator - - def _clear_media_position(self) -> None: - """Clear the media_position.""" - self._media_position = None - self._media_position_updated_at = None - - def _set_favorites(self) -> None: - """Set available favorites.""" - self._favorites = [] - for fav in self.soco.music_library.get_sonos_favorites(): - try: - # Exclude non-playable favorites with no linked resources - if fav.reference.resources: - self._favorites.append(fav) - except SoCoException as ex: - # Skip unknown types - _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) - - async def async_reconnect_player(self) -> None: - """Set basic information when player is reconnected.""" - await self.hass.async_add_executor_job(self._reconnect_player) - - def _reconnect_player(self) -> None: - """Set basic information when player is reconnected.""" - self._play_mode = self.soco.play_mode - self.update_volume() - self._set_favorites() - async def async_update(self, now: datetime.datetime | None = None) -> None: """Retrieve latest state.""" await self.hass.async_add_executor_job(self._update, now) @@ -472,310 +293,44 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Retrieve latest state.""" _LOGGER.debug("Polling speaker %s", self.speaker.zone_name) try: - self.update_groups() - self.update_volume() - if self.is_coordinator: - self.update_media() + self.speaker.update_groups() + self.speaker.update_volume() + if self.speaker.is_coordinator: + self.speaker.update_media() except SoCoException: pass - @callback - def async_update_media(self, event: SonosEvent | None = None) -> None: - """Update information about currently playing media.""" - self.hass.async_add_executor_job(self.update_media, event) - - def update_media(self, event: SonosEvent | None = None) -> None: - """Update information about currently playing media.""" - variables = event and event.variables - - if variables and "transport_state" in variables: - # If the transport has an error then transport_state will - # not be set - new_status = variables["transport_state"] - else: - transport_info = self.soco.get_current_transport_info() - new_status = transport_info["current_transport_state"] - - # Ignore transitions, we should get the target state soon - if new_status == "TRANSITIONING": - return - - self._play_mode = ( - variables["current_play_mode"] if variables else self.soco.play_mode - ) - self._uri = None - self._media_duration = None - self._media_image_url = None - self._media_channel = None - self._media_artist = None - self._media_album_name = None - self._media_title = None - self._queue_position = None - self._source_name = None - - update_position = new_status != self._status - self._status = new_status - - if variables: - track_uri = variables["current_track_uri"] - music_source = self.soco.music_source_from_uri(track_uri) - else: - # This causes a network round-trip so we avoid it when possible - music_source = self.soco.music_source - - if music_source == MUSIC_SRC_TV: - self.update_media_linein(SOURCE_TV) - elif music_source == MUSIC_SRC_LINE_IN: - self.update_media_linein(SOURCE_LINEIN) - else: - track_info = self.soco.get_current_track_info() - if not track_info["uri"]: - self._clear_media_position() - else: - self._uri = track_info["uri"] - self._media_artist = track_info.get("artist") - self._media_album_name = track_info.get("album") - self._media_title = track_info.get("title") - - if music_source == MUSIC_SRC_RADIO: - self.update_media_radio(variables) - else: - self.update_media_music(update_position, track_info) - - self.schedule_update_ha_state() - - # Also update slaves - entities = self.hass.data[DATA_SONOS].media_player_entities.values() - for entity in entities: - coordinator = entity.coordinator - if coordinator and coordinator.unique_id == self.unique_id: - entity.schedule_update_ha_state() - - def update_media_linein(self, source: str) -> None: - """Update state when playing from line-in/tv.""" - self._clear_media_position() - - self._media_title = source - self._source_name = source - - def update_media_radio(self, variables: dict) -> None: - """Update state when streaming radio.""" - self._clear_media_position() - - try: - album_art_uri = variables["current_track_meta_data"].album_art_uri - self._media_image_url = self._media_library.build_album_art_full_uri( - album_art_uri - ) - except (TypeError, KeyError, AttributeError): - pass - - # Non-playing radios will not have a current title. Radios without tagging - # can have part of the radio URI as title. In these cases we try to use the - # radio name instead. - try: - uri_meta_data = variables["enqueued_transport_uri_meta_data"] - if isinstance( - uri_meta_data, pysonos.data_structures.DidlAudioBroadcast - ) and ( - self.state != STATE_PLAYING - or self.soco.music_source_from_uri(self._media_title) == MUSIC_SRC_RADIO - or ( - isinstance(self._media_title, str) - and isinstance(self._uri, str) - and self._media_title in self._uri - ) - ): - self._media_title = uri_meta_data.title - except (TypeError, KeyError, AttributeError): - pass - - media_info = self.soco.get_current_media_info() - - self._media_channel = media_info["channel"] - - # Check if currently playing radio station is in favorites - for fav in self._favorites: - if fav.reference.get_uri() == media_info["uri"]: - self._source_name = fav.title - - def update_media_music(self, update_media_position: bool, track_info: dict) -> None: - """Update state when playing music tracks.""" - self._media_duration = _timespan_secs(track_info.get("duration")) - current_position = _timespan_secs(track_info.get("position")) - - # player started reporting position? - if current_position is not None and self._media_position is None: - update_media_position = True - - # position jumped? - if current_position is not None and self._media_position is not None: - if self.state == STATE_PLAYING: - assert self._media_position_updated_at is not None - time_delta = utcnow() - self._media_position_updated_at - time_diff = time_delta.total_seconds() - else: - time_diff = 0 - - calculated_position = self._media_position + time_diff - - if abs(calculated_position - current_position) > 1.5: - update_media_position = True - - if current_position is None: - self._clear_media_position() - elif update_media_position: - self._media_position = current_position - self._media_position_updated_at = utcnow() - - self._media_image_url = track_info.get("album_art") - - playlist_position = int(track_info.get("playlist_position")) # type: ignore - if playlist_position > 0: - self._queue_position = playlist_position - 1 - - @callback - def async_update_volume(self, event: SonosEvent) -> None: - """Update information about currently volume settings.""" - variables = event.variables - - if "volume" in variables: - self._player_volume = int(variables["volume"]["Master"]) - - if "mute" in variables: - self._player_muted = variables["mute"]["Master"] == "1" - - if "night_mode" in variables: - self._night_sound = variables["night_mode"] == "1" - - if "dialog_level" in variables: - self._speech_enhance = variables["dialog_level"] == "1" - - self.async_write_ha_state() - - def update_volume(self) -> None: - """Update information about currently volume settings.""" - self._player_volume = self.soco.volume - self._player_muted = self.soco.mute - self._night_sound = self.soco.night_mode - self._speech_enhance = self.soco.dialog_mode - - def update_groups(self, event: SonosEvent | None = None) -> None: - """Handle callback for topology change event.""" - coro = self.create_update_groups_coro(event) - if coro: - self.hass.add_job(coro) # type: ignore - - @callback - def async_update_groups(self, event: SonosEvent | None = None) -> None: - """Handle callback for topology change event.""" - coro = self.create_update_groups_coro(event) - if coro: - self.hass.async_add_job(coro) # type: ignore - - def create_update_groups_coro( - self, event: SonosEvent | None = None - ) -> Coroutine | None: - """Handle callback for topology change event.""" - - def _get_soco_group() -> list[str]: - """Ask SoCo cache for existing topology.""" - coordinator_uid = self.unique_id - slave_uids = [] - - with suppress(SoCoException): - if self.soco.group and self.soco.group.coordinator: - coordinator_uid = self.soco.group.coordinator.uid - slave_uids = [ - p.uid - for p in self.soco.group.members - if p.uid != coordinator_uid - ] - - return [coordinator_uid] + slave_uids - - async def _async_extract_group(event: SonosEvent) -> list[str]: - """Extract group layout from a topology event.""" - group = event and event.zone_player_uui_ds_in_group - if group: - assert isinstance(group, str) - return group.split(",") - - return await self.hass.async_add_executor_job(_get_soco_group) - - @callback - def _async_regroup(group: list[str]) -> None: - """Rebuild internal group layout.""" - sonos_group = [] - for uid in group: - entity = _get_entity_from_soco_uid(self.hass, uid) - if entity: - sonos_group.append(entity) - - self._coordinator = None - self._sonos_group = sonos_group - self.async_write_ha_state() - - for slave_uid in group[1:]: - slave = _get_entity_from_soco_uid(self.hass, slave_uid) - if slave: - # pylint: disable=protected-access - slave._coordinator = self - slave._sonos_group = sonos_group - slave.async_write_ha_state() - - async def _async_handle_group_event(event: SonosEvent) -> None: - """Get async lock and handle event.""" - - async with self.hass.data[DATA_SONOS].topology_condition: - group = await _async_extract_group(event) - - if self.unique_id == group[0]: - _async_regroup(group) - - self.hass.data[DATA_SONOS].topology_condition.notify_all() - - if event and not hasattr(event, "zone_player_uui_ds_in_group"): - return None - - return _async_handle_group_event(event) - - @callback - def async_update_content(self, event: SonosEvent | None = None) -> None: - """Update information about available content.""" - if event and "favorites_update_id" in event.variables: - self.hass.async_add_job(self._set_favorites) - self.async_write_ha_state() - @property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - return self._player_volume and self._player_volume / 100 + return self.speaker.volume and self.speaker.volume / 100 @property def is_volume_muted(self) -> bool | None: """Return true if volume is muted.""" - return self._player_muted + return self.speaker.muted @property # type: ignore[misc] - @soco_coordinator def shuffle(self) -> str | None: """Shuffling state.""" - shuffle: str = PLAY_MODES[self._play_mode][0] + shuffle: str = PLAY_MODES[self.media.play_mode][0] return shuffle @property # type: ignore[misc] - @soco_coordinator def repeat(self) -> str | None: """Return current repeat mode.""" - sonos_repeat = PLAY_MODES[self._play_mode][1] + sonos_repeat = PLAY_MODES[self.media.play_mode][1] return SONOS_TO_REPEAT[sonos_repeat] + @property + def media(self) -> SonosMedia: + """Return the SonosMedia object from the coordinator speaker.""" + return self.coordinator.media + @property # type: ignore[misc] - @soco_coordinator def media_content_id(self) -> str | None: """Content id of current playing media.""" - return self._uri + return self.media.uri @property def media_content_type(self) -> str: @@ -783,67 +338,51 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): return MEDIA_TYPE_MUSIC @property # type: ignore[misc] - @soco_coordinator def media_duration(self) -> float | None: """Duration of current playing media in seconds.""" - return self._media_duration + return self.media.duration @property # type: ignore[misc] - @soco_coordinator def media_position(self) -> float | None: """Position of current playing media in seconds.""" - return self._media_position + return self.media.position @property # type: ignore[misc] - @soco_coordinator def media_position_updated_at(self) -> datetime.datetime | None: """When was the position of the current playing media valid.""" - return self._media_position_updated_at + return self.media.position_updated_at @property # type: ignore[misc] - @soco_coordinator def media_image_url(self) -> str | None: """Image url of current playing media.""" - return self._media_image_url or None + return self.media.image_url or None @property # type: ignore[misc] - @soco_coordinator def media_channel(self) -> str | None: """Channel currently playing.""" - return self._media_channel or None + return self.media.channel or None @property # type: ignore[misc] - @soco_coordinator def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" - return self._media_artist or None + return self.media.artist or None @property # type: ignore[misc] - @soco_coordinator def media_album_name(self) -> str | None: """Album name of current playing media, music track only.""" - return self._media_album_name or None + return self.media.album_name or None @property # type: ignore[misc] - @soco_coordinator def media_title(self) -> str | None: """Title of current playing media.""" - return self._media_title or None + return self.media.title or None @property # type: ignore[misc] - @soco_coordinator - def queue_position(self) -> int | None: - """If playing local queue return the position in the queue else None.""" - return self._queue_position - - @property # type: ignore[misc] - @soco_coordinator def source(self) -> str | None: """Name of the current input source.""" - return self._source_name or None + return self.media.source_name or None @property # type: ignore[misc] - @soco_coordinator def supported_features(self) -> int: """Flag media player features that are supported.""" return SUPPORT_SONOS @@ -851,12 +390,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): @soco_error() def volume_up(self) -> None: """Volume up media player.""" - self.soco.volume += self._volume_increment + self.soco.volume += VOLUME_INCREMENT @soco_error() def volume_down(self) -> None: """Volume down media player.""" - self.soco.volume -= self._volume_increment + self.soco.volume -= VOLUME_INCREMENT @soco_error() def set_volume_level(self, volume: str) -> None: @@ -864,20 +403,22 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.soco.volume = str(int(volume * 100)) @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator def set_shuffle(self, shuffle: str) -> None: """Enable/Disable shuffle mode.""" sonos_shuffle = shuffle - sonos_repeat = PLAY_MODES[self._play_mode][1] - self.soco.play_mode = PLAY_MODE_BY_MEANING[(sonos_shuffle, sonos_repeat)] + sonos_repeat = PLAY_MODES[self.media.play_mode][1] + self.coordinator.soco.play_mode = PLAY_MODE_BY_MEANING[ + (sonos_shuffle, sonos_repeat) + ] @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator def set_repeat(self, repeat: str) -> None: """Set repeat mode.""" - sonos_shuffle = PLAY_MODES[self._play_mode][0] + sonos_shuffle = PLAY_MODES[self.media.play_mode][0] sonos_repeat = REPEAT_TO_SONOS[repeat] - self.soco.play_mode = PLAY_MODE_BY_MEANING[(sonos_shuffle, sonos_repeat)] + self.coordinator.soco.play_mode = PLAY_MODE_BY_MEANING[ + (sonos_shuffle, sonos_repeat) + ] @soco_error() def mute_volume(self, mute: bool) -> None: @@ -885,35 +426,34 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.soco.mute = mute @soco_error() - @soco_coordinator def select_source(self, source: str) -> None: """Select input source.""" + soco = self.coordinator.soco if source == SOURCE_LINEIN: - self.soco.switch_to_line_in() + soco.switch_to_line_in() elif source == SOURCE_TV: - self.soco.switch_to_tv() + soco.switch_to_tv() else: - fav = [fav for fav in self._favorites if fav.title == source] + fav = [fav for fav in self.coordinator.favorites if fav.title == source] if len(fav) == 1: src = fav.pop() uri = src.reference.get_uri() - if self.soco.music_source_from_uri(uri) in [ + if soco.music_source_from_uri(uri) in [ MUSIC_SRC_RADIO, MUSIC_SRC_LINE_IN, ]: - self.soco.play_uri(uri, title=source) + soco.play_uri(uri, title=source) else: - self.soco.clear_queue() - self.soco.add_to_queue(src.reference) - self.soco.play_from_queue(0) + soco.clear_queue() + soco.add_to_queue(src.reference) + soco.play_from_queue(0) @property # type: ignore[misc] - @soco_coordinator def source_list(self) -> list[str]: """List of available input sources.""" - sources = [fav.title for fav in self._favorites] + sources = [fav.title for fav in self.coordinator.favorites] - model = self.speaker.model_name.upper() + model = self.coordinator.model_name.upper() if "PLAY:5" in model or "CONNECT" in model: sources += [SOURCE_LINEIN] elif "PLAYBAR" in model: @@ -924,49 +464,41 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): return sources @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator def media_play(self) -> None: """Send play command.""" - self.soco.play() + self.coordinator.soco.play() @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator def media_stop(self) -> None: """Send stop command.""" - self.soco.stop() + self.coordinator.soco.stop() @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator def media_pause(self) -> None: """Send pause command.""" - self.soco.pause() + self.coordinator.soco.pause() @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator def media_next_track(self) -> None: """Send next track command.""" - self.soco.next() + self.coordinator.soco.next() @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator def media_previous_track(self) -> None: """Send next track command.""" - self.soco.previous() + self.coordinator.soco.previous() @soco_error(UPNP_ERRORS_TO_IGNORE) - @soco_coordinator def media_seek(self, position: str) -> None: """Send seek command.""" - self.soco.seek(str(datetime.timedelta(seconds=int(position)))) + self.coordinator.soco.seek(str(datetime.timedelta(seconds=int(position)))) @soco_error() - @soco_coordinator def clear_playlist(self) -> None: """Clear players playlist.""" - self.soco.clear_queue() + self.coordinator.soco.clear_queue() @soco_error() - @soco_coordinator def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """ Send the play_media command to the media player. @@ -978,16 +510,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ + soco = self.coordinator.soco if media_id and media_id.startswith(PLEX_URI_SCHEME): media_id = media_id[len(PLEX_URI_SCHEME) :] play_on_sonos(self.hass, media_type, media_id, self.name) # type: ignore[no-untyped-call] elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): if kwargs.get(ATTR_MEDIA_ENQUEUE): try: - if self.soco.is_service_uri(media_id): - self.soco.add_service_uri_to_queue(media_id) + if soco.is_service_uri(media_id): + soco.add_service_uri_to_queue(media_id) else: - self.soco.add_uri_to_queue(media_id) + soco.add_uri_to_queue(media_id) except SoCoUPnPException: _LOGGER.error( 'Error parsing media uri "%s", ' @@ -996,242 +529,47 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_id, ) else: - if self.soco.is_service_uri(media_id): - self.soco.clear_queue() - self.soco.add_service_uri_to_queue(media_id) - self.soco.play_from_queue(0) + if soco.is_service_uri(media_id): + soco.clear_queue() + soco.add_service_uri_to_queue(media_id) + soco.play_from_queue(0) else: - self.soco.play_uri(media_id) + soco.play_uri(media_id) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): - item = get_media(self._media_library, media_id, media_type) # type: ignore[no-untyped-call] - self.soco.play_uri(item.get_uri()) + item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] + soco.play_uri(item.get_uri()) return try: - playlists = self.soco.get_sonos_playlists() + playlists = soco.get_sonos_playlists() playlist = next(p for p in playlists if p.title == media_id) - self.soco.clear_queue() - self.soco.add_to_queue(playlist) - self.soco.play_from_queue(0) + soco.clear_queue() + soco.add_to_queue(playlist) + soco.play_from_queue(0) except StopIteration: _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) elif media_type in PLAYABLE_MEDIA_TYPES: - item = get_media(self._media_library, media_id, media_type) # type: ignore[no-untyped-call] + item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] if not item: _LOGGER.error('Could not find "%s" in the library', media_id) return - self.soco.play_uri(item.get_uri()) + soco.play_uri(item.get_uri()) else: _LOGGER.error('Sonos does not support a media type of "%s"', media_type) @soco_error() - def join( - self, slaves: list[SonosMediaPlayerEntity] - ) -> list[SonosMediaPlayerEntity]: - """Form a group with other players.""" - if self._coordinator: - self.unjoin() - group = [self] - else: - group = self._sonos_group.copy() - - for slave in slaves: - if slave.unique_id != self.unique_id: - slave.soco.join(self.soco) - # pylint: disable=protected-access - slave._coordinator = self - if slave not in group: - group.append(slave) - - return group - - @staticmethod - async def join_multi( - hass: HomeAssistant, - master: SonosMediaPlayerEntity, - entities: list[SonosMediaPlayerEntity], - ) -> None: - """Form a group with other players.""" - async with hass.data[DATA_SONOS].topology_condition: - group: list[SonosMediaPlayerEntity] = await hass.async_add_executor_job( - master.join, entities - ) - await SonosMediaPlayerEntity.wait_for_groups(hass, [group]) - - @soco_error() - def unjoin(self) -> None: - """Unjoin the player from a group.""" - self.soco.unjoin() - self._coordinator = None - - @staticmethod - async def unjoin_multi( - hass: HomeAssistant, entities: list[SonosMediaPlayerEntity] - ) -> None: - """Unjoin several players from their group.""" - - def _unjoin_all(entities: list[SonosMediaPlayerEntity]) -> None: - """Sync helper.""" - # Unjoin slaves first to prevent inheritance of queues - coordinators = [e for e in entities if e.is_coordinator] - slaves = [e for e in entities if not e.is_coordinator] - - for entity in slaves + coordinators: - entity.unjoin() - - async with hass.data[DATA_SONOS].topology_condition: - await hass.async_add_executor_job(_unjoin_all, entities) - await SonosMediaPlayerEntity.wait_for_groups(hass, [[e] for e in entities]) - - @soco_error() - def snapshot(self, with_group: bool) -> None: - """Snapshot the state of a player.""" - self._soco_snapshot = pysonos.snapshot.Snapshot(self.soco) - self._soco_snapshot.snapshot() - if with_group: - self._snapshot_group = self._sonos_group.copy() - else: - self._snapshot_group = None - - @staticmethod - async def snapshot_multi( - hass: HomeAssistant, entities: list[SonosMediaPlayerEntity], with_group: bool - ) -> None: - """Snapshot all the entities and optionally their groups.""" - # pylint: disable=protected-access - - def _snapshot_all(entities: list[SonosMediaPlayerEntity]) -> None: - """Sync helper.""" - for entity in entities: - entity.snapshot(with_group) - - # Find all affected players - entities_set = set(entities) - if with_group: - for entity in list(entities_set): - entities_set.update(entity._sonos_group) - - async with hass.data[DATA_SONOS].topology_condition: - await hass.async_add_executor_job(_snapshot_all, entities_set) - - @soco_error() - def restore(self) -> None: - """Restore a snapshotted state to a player.""" - try: - assert self._soco_snapshot is not None - self._soco_snapshot.restore() - except (TypeError, AssertionError, AttributeError, SoCoException) as ex: - # Can happen if restoring a coordinator onto a current slave - _LOGGER.warning("Error on restore %s: %s", self.entity_id, ex) - - self._soco_snapshot = None - self._snapshot_group = None - - @staticmethod - async def restore_multi( - hass: HomeAssistant, entities: list[SonosMediaPlayerEntity], with_group: bool - ) -> None: - """Restore snapshots for all the entities.""" - # pylint: disable=protected-access - - def _restore_groups( - entities: list[SonosMediaPlayerEntity], with_group: bool - ) -> list[list[SonosMediaPlayerEntity]]: - """Pause all current coordinators and restore groups.""" - for entity in (e for e in entities if e.is_coordinator): - if entity.state == STATE_PLAYING: - entity.media_pause() - - groups = [] - - if with_group: - # Unjoin slaves first to prevent inheritance of queues - for entity in [e for e in entities if not e.is_coordinator]: - if entity._snapshot_group != entity._sonos_group: - entity.unjoin() - - # Bring back the original group topology - for entity in (e for e in entities if e._snapshot_group): - assert entity._snapshot_group is not None - if entity._snapshot_group[0] == entity: - entity.join(entity._snapshot_group) - groups.append(entity._snapshot_group.copy()) - - return groups - - def _restore_players(entities: list[SonosMediaPlayerEntity]) -> None: - """Restore state of all players.""" - for entity in (e for e in entities if not e.is_coordinator): - entity.restore() - - for entity in (e for e in entities if e.is_coordinator): - entity.restore() - - # Find all affected players - entities_set = {e for e in entities if e._soco_snapshot} - if with_group: - for entity in [e for e in entities_set if e._snapshot_group]: - assert entity._snapshot_group is not None - entities_set.update(entity._snapshot_group) - - async with hass.data[DATA_SONOS].topology_condition: - groups = await hass.async_add_executor_job( - _restore_groups, entities_set, with_group - ) - - await SonosMediaPlayerEntity.wait_for_groups(hass, groups) - - await hass.async_add_executor_job(_restore_players, entities_set) - - @staticmethod - async def wait_for_groups( - hass: HomeAssistant, groups: list[list[SonosMediaPlayerEntity]] - ) -> None: - """Wait until all groups are present, or timeout.""" - # pylint: disable=protected-access - - def _test_groups(groups: list[list[SonosMediaPlayerEntity]]) -> bool: - """Return whether all groups exist now.""" - for group in groups: - coordinator = group[0] - - # Test that coordinator is coordinating - current_group = coordinator._sonos_group - if coordinator != current_group[0]: - return False - - # Test that slaves match - if set(group[1:]) != set(current_group[1:]): - return False - - return True - - try: - with async_timeout.timeout(5): - while not _test_groups(groups): - await hass.data[DATA_SONOS].topology_condition.wait() - except asyncio.TimeoutError: - _LOGGER.warning("Timeout waiting for target groups %s", groups) - - for entity in hass.data[DATA_SONOS].media_player_entities.values(): - entity.soco._zgs_cache.clear() - - @soco_error() - @soco_coordinator def set_sleep_timer(self, sleep_time: int) -> None: """Set the timer on the player.""" - self.soco.set_sleep_timer(sleep_time) + self.coordinator.soco.set_sleep_timer(sleep_time) @soco_error() - @soco_coordinator def clear_sleep_timer(self) -> None: """Clear the timer on the player.""" - self.soco.set_sleep_timer(None) + self.coordinator.soco.set_sleep_timer(None) @soco_error() - @soco_coordinator def set_alarm( self, alarm_id: int, @@ -1242,7 +580,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) -> None: """Set the alarm clock on the player.""" alarm = None - for one_alarm in alarms.get_alarms(self.soco): + for one_alarm in alarms.get_alarms(self.coordinator.soco): # pylint: disable=protected-access if one_alarm._alarm_id == str(alarm_id): alarm = one_alarm @@ -1267,10 +605,10 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): status_light: bool | None = None, ) -> None: """Modify playback options.""" - if night_sound is not None and self._night_sound is not None: + if night_sound is not None and self.speaker.night_mode is not None: self.soco.night_mode = night_sound - if speech_enhance is not None and self._speech_enhance is not None: + if speech_enhance is not None and self.speaker.dialog_mode is not None: self.soco.dialog_mode = speech_enhance if status_light is not None: @@ -1282,26 +620,25 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.soco.play_from_queue(queue_position) @soco_error() - @soco_coordinator def remove_from_queue(self, queue_position: int = 0) -> None: """Remove item from the queue.""" - self.soco.remove_from_queue(queue_position) + self.coordinator.soco.remove_from_queue(queue_position) @property def extra_state_attributes(self) -> dict[str, Any]: """Return entity specific state attributes.""" attributes: dict[str, Any] = { - ATTR_SONOS_GROUP: [e.entity_id for e in self._sonos_group] + ATTR_SONOS_GROUP: self.speaker.sonos_group_entities } - if self._night_sound is not None: - attributes[ATTR_NIGHT_SOUND] = self._night_sound + if self.speaker.night_mode is not None: + attributes[ATTR_NIGHT_SOUND] = self.speaker.night_mode - if self._speech_enhance is not None: - attributes[ATTR_SPEECH_ENHANCE] = self._speech_enhance + if self.speaker.dialog_mode is not None: + attributes[ATTR_SPEECH_ENHANCE] = self.speaker.dialog_mode - if self.queue_position is not None: - attributes[ATTR_QUEUE_POSITION] = self.queue_position + if self.media.queue_position is not None: + attributes[ATTR_QUEUE_POSITION] = self.media.queue_position return attributes @@ -1318,7 +655,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ): item = await self.hass.async_add_executor_job( get_media, - self._media_library, + self.media.library, media_content_id, MEDIA_TYPES_TO_SONOS[media_content_type], ) @@ -1342,7 +679,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) -> str | None: if is_internal: item = get_media( # type: ignore[no-untyped-call] - self._media_library, + self.media.library, media_content_id, media_content_type, ) @@ -1356,7 +693,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if media_content_type in [None, "library"]: return await self.hass.async_add_executor_job( - library_payload, self._media_library, _get_thumbnail_url + library_payload, self.media.library, _get_thumbnail_url ) payload = { @@ -1364,7 +701,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): "idstring": media_content_id, } response = await self.hass.async_add_executor_job( - build_item_response, self._media_library, payload, _get_thumbnail_url + build_item_response, self.media.library, payload, _get_thumbnail_url ) if response is None: raise BrowseError( diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 38d6d679219..fcb856e1c06 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -9,7 +9,7 @@ from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import SONOS_CREATE_BATTERY -from .entity import SonosSensorEntity +from .entity import SonosEntity from .speaker import SonosSpeaker _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class SonosBatteryEntity(SonosSensorEntity, SensorEntity): +class SonosBatteryEntity(SonosEntity, SensorEntity): """Representation of a Sonos Battery entity.""" @property diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 03cce67e4d8..7c44245bf5d 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -1,20 +1,28 @@ """Base class for common speaker tasks.""" from __future__ import annotations -from asyncio import gather +import asyncio +from collections.abc import Coroutine import contextlib import datetime from functools import partial import logging from typing import Any, Callable -from pysonos.core import SoCo +import async_timeout +from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo +from pysonos.data_structures import DidlAudioBroadcast, DidlFavorite from pysonos.events_base import Event as SonosEvent, SubscriptionBase from pysonos.exceptions import SoCoException +from pysonos.music_library import MusicLibrary +from pysonos.snapshot import Snapshot from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import STATE_PLAYING from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as ent_reg from homeassistant.helpers.dispatcher import ( async_dispatcher_send, dispatcher_connect, @@ -24,26 +32,29 @@ from homeassistant.util import dt as dt_util from .const import ( BATTERY_SCAN_INTERVAL, + DATA_SONOS, + DOMAIN, PLATFORMS, SCAN_INTERVAL, SEEN_EXPIRE_TIME, - SONOS_CONTENT_UPDATE, SONOS_CREATE_BATTERY, SONOS_CREATE_MEDIA_PLAYER, SONOS_ENTITY_CREATED, SONOS_ENTITY_UPDATE, SONOS_GROUP_UPDATE, - SONOS_MEDIA_UPDATE, - SONOS_PLAYER_RECONNECTED, SONOS_SEEN, SONOS_STATE_UPDATED, - SONOS_VOLUME_UPDATE, + SOURCE_LINEIN, + SOURCE_TV, ) +from .helpers import soco_error EVENT_CHARGING = { "CHARGING": True, "NOT_CHARGING": False, } +UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} + _LOGGER = logging.getLogger(__name__) @@ -58,6 +69,55 @@ def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None: return soco.get_battery_info() +def _timespan_secs(timespan: str | None) -> None | float: + """Parse a time-span into number of seconds.""" + if timespan in UNAVAILABLE_VALUES: + return None + + assert timespan is not None + return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))) + + +class SonosMedia: + """Representation of the current Sonos media.""" + + def __init__(self, soco: SoCo) -> None: + """Initialize a SonosMedia.""" + self.library = MusicLibrary(soco) + self.play_mode: str | None = None + self.playback_status: str | None = None + + self.album_name: str | None = None + self.artist: str | None = None + self.channel: str | None = None + self.duration: float | None = None + self.image_url: str | None = None + self.queue_position: int | None = None + self.source_name: str | None = None + self.title: str | None = None + self.uri: str | None = None + + self.position: float | None = None + self.position_updated_at: datetime.datetime | None = None + + def clear(self) -> None: + """Clear basic media info.""" + self.album_name = None + self.artist = None + self.channel = None + self.duration = None + self.image_url = None + self.queue_position = None + self.source_name = None + self.title = None + self.uri = None + + def clear_position(self) -> None: + """Clear the position attributes.""" + self.position = None + self.position_updated_at = None + + class SonosSpeaker: """Representation of a Sonos speaker.""" @@ -65,16 +125,19 @@ class SonosSpeaker: self, hass: HomeAssistant, soco: SoCo, speaker_info: dict[str, Any] ) -> None: """Initialize a SonosSpeaker.""" + self.hass: HomeAssistant = hass + self.soco: SoCo = soco + self.media = SonosMedia(soco) + self._is_ready: bool = False self._subscriptions: list[SubscriptionBase] = [] self._poll_timer: Callable | None = None self._seen_timer: Callable | None = None - self._seen_dispatcher: Callable | None = None - self._entity_creation_dispatcher: Callable | None = None self._platforms_ready: set[str] = set() - self.hass: HomeAssistant = hass - self.soco: SoCo = soco + self._entity_creation_dispatcher: Callable | None = None + self._group_dispatcher: Callable | None = None + self._seen_dispatcher: Callable | None = None self.mac_address = speaker_info["mac_address"] self.model_name = speaker_info["model_name"] @@ -85,13 +148,33 @@ class SonosSpeaker: self._last_battery_event: datetime.datetime | None = None self._battery_poll_timer: Callable | None = None + self.volume: int | None = None + self.muted: bool | None = None + self.night_mode: bool | None = None + self.dialog_mode: bool | None = None + + self.coordinator: SonosSpeaker | None = None + self.sonos_group: list[SonosSpeaker] = [self] + self.sonos_group_entities: list[str] = [] + self.soco_snapshot: Snapshot | None = None + self.snapshot_group: list[SonosSpeaker] | None = None + + self.favorites: list[DidlFavorite] = [] + def setup(self) -> None: """Run initial setup of the speaker.""" + self.set_basic_info() + self._entity_creation_dispatcher = dispatcher_connect( self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.async_handle_new_entity, ) + self._group_dispatcher = dispatcher_connect( + self.hass, + SONOS_GROUP_UPDATE, + self.async_update_groups, + ) self._seen_dispatcher = dispatcher_connect( self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen ) @@ -115,11 +198,21 @@ class SonosSpeaker: await self.async_subscribe() self._is_ready = True + def write_entity_states(self) -> None: + """Write states for associated SonosEntity instances.""" + dispatcher_send(self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}") + @callback def async_write_entity_states(self) -> None: """Write states for associated SonosEntity instances.""" async_dispatcher_send(self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}") + def set_basic_info(self) -> None: + """Set basic information when speaker is reconnected.""" + self.media.play_mode = self.soco.play_mode + self.update_volume() + self.set_favorites() + @property def available(self) -> bool: """Return whether this speaker is available.""" @@ -129,7 +222,7 @@ class SonosSpeaker: """Initiate event subscriptions.""" _LOGGER.debug("Creating subscriptions for %s", self.zone_name) try: - self.async_dispatch_player_reconnected() + await self.hass.async_add_executor_job(self.set_basic_info) if self._subscriptions: raise RuntimeError( @@ -137,12 +230,10 @@ class SonosSpeaker: f"when existing subscriptions exist: {self._subscriptions}" ) - await gather( - self._subscribe(self.soco.avTransport, self.async_dispatch_media), - self._subscribe(self.soco.renderingControl, self.async_dispatch_volume), - self._subscribe( - self.soco.contentDirectory, self.async_dispatch_content - ), + await asyncio.gather( + self._subscribe(self.soco.avTransport, self.async_update_media), + self._subscribe(self.soco.renderingControl, self.async_update_volume), + self._subscribe(self.soco.contentDirectory, self.async_update_content), self._subscribe( self.soco.zoneGroupTopology, self.async_dispatch_groups ), @@ -163,25 +254,6 @@ class SonosSpeaker: subscription.callback = sub_callback self._subscriptions.append(subscription) - @callback - def async_dispatch_media(self, event: SonosEvent | None = None) -> None: - """Update currently playing media from event.""" - async_dispatcher_send(self.hass, f"{SONOS_MEDIA_UPDATE}-{self.soco.uid}", event) - - @callback - def async_dispatch_content(self, event: SonosEvent | None = None) -> None: - """Update available content from event.""" - async_dispatcher_send( - self.hass, f"{SONOS_CONTENT_UPDATE}-{self.soco.uid}", event - ) - - @callback - def async_dispatch_volume(self, event: SonosEvent | None = None) -> None: - """Update volume from event.""" - async_dispatcher_send( - self.hass, f"{SONOS_VOLUME_UPDATE}-{self.soco.uid}", event - ) - @callback def async_dispatch_properties(self, event: SonosEvent | None = None) -> None: """Update properties from event.""" @@ -197,12 +269,7 @@ class SonosSpeaker: self._poll_timer() self._poll_timer = None - async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE, event) - - @callback - def async_dispatch_player_reconnected(self) -> None: - """Signal that player has been reconnected.""" - async_dispatcher_send(self.hass, f"{SONOS_PLAYER_RECONNECTED}-{self.soco.uid}") + self.async_update_groups(event) async def async_seen(self, soco: SoCo | None = None) -> None: """Record that this speaker was seen right now.""" @@ -280,6 +347,11 @@ class SonosSpeaker: ): self.battery_info = battery_info + @property + def is_coordinator(self) -> bool: + """Return true if player is a coordinator.""" + return self.coordinator is None + @property def power_source(self) -> str: """Return the name of the current power source. @@ -309,3 +381,460 @@ class SonosSpeaker: ): self.battery_info = battery_info self.async_write_entity_states() + + def update_groups(self, event: SonosEvent | None = None) -> None: + """Handle callback for topology change event.""" + coro = self.create_update_groups_coro(event) + if coro: + self.hass.add_job(coro) # type: ignore + + @callback + def async_update_groups(self, event: SonosEvent | None = None) -> None: + """Handle callback for topology change event.""" + coro = self.create_update_groups_coro(event) + if coro: + self.hass.async_add_job(coro) # type: ignore + + def create_update_groups_coro( + self, event: SonosEvent | None = None + ) -> Coroutine | None: + """Handle callback for topology change event.""" + + def _get_soco_group() -> list[str]: + """Ask SoCo cache for existing topology.""" + coordinator_uid = self.soco.uid + slave_uids = [] + + with contextlib.suppress(SoCoException): + if self.soco.group and self.soco.group.coordinator: + coordinator_uid = self.soco.group.coordinator.uid + slave_uids = [ + p.uid + for p in self.soco.group.members + if p.uid != coordinator_uid + ] + + return [coordinator_uid] + slave_uids + + async def _async_extract_group(event: SonosEvent) -> list[str]: + """Extract group layout from a topology event.""" + group = event and event.zone_player_uui_ds_in_group + if group: + assert isinstance(group, str) + return group.split(",") + + return await self.hass.async_add_executor_job(_get_soco_group) + + @callback + def _async_regroup(group: list[str]) -> None: + """Rebuild internal group layout.""" + entity_registry = ent_reg.async_get(self.hass) + sonos_group = [] + sonos_group_entities = [] + + for uid in group: + speaker = self.hass.data[DATA_SONOS].discovered.get(uid) + if speaker: + sonos_group.append(speaker) + entity_id = entity_registry.async_get_entity_id( + MP_DOMAIN, DOMAIN, uid + ) + sonos_group_entities.append(entity_id) + + self.coordinator = None + self.sonos_group = sonos_group + self.sonos_group_entities = sonos_group_entities + self.async_write_entity_states() + + for slave_uid in group[1:]: + slave = self.hass.data[DATA_SONOS].discovered.get(slave_uid) + if slave: + slave.coordinator = self + slave.sonos_group = sonos_group + slave.sonos_group_entities = sonos_group_entities + slave.async_write_entity_states() + + async def _async_handle_group_event(event: SonosEvent) -> None: + """Get async lock and handle event.""" + + async with self.hass.data[DATA_SONOS].topology_condition: + group = await _async_extract_group(event) + + if self.soco.uid == group[0]: + _async_regroup(group) + + self.hass.data[DATA_SONOS].topology_condition.notify_all() + + if event and not hasattr(event, "zone_player_uui_ds_in_group"): + return None + + return _async_handle_group_event(event) + + @soco_error() + def join(self, slaves: list[SonosSpeaker]) -> list[SonosSpeaker]: + """Form a group with other players.""" + if self.coordinator: + self.unjoin() + group = [self] + else: + group = self.sonos_group.copy() + + for slave in slaves: + if slave.soco.uid != self.soco.uid: + slave.soco.join(self.soco) + slave.coordinator = self + if slave not in group: + group.append(slave) + + return group + + @staticmethod + async def join_multi( + hass: HomeAssistant, + master: SonosSpeaker, + speakers: list[SonosSpeaker], + ) -> None: + """Form a group with other players.""" + async with hass.data[DATA_SONOS].topology_condition: + group: list[SonosSpeaker] = await hass.async_add_executor_job( + master.join, speakers + ) + await SonosSpeaker.wait_for_groups(hass, [group]) + + @soco_error() + def unjoin(self) -> None: + """Unjoin the player from a group.""" + self.soco.unjoin() + self.coordinator = None + + @staticmethod + async def unjoin_multi(hass: HomeAssistant, speakers: list[SonosSpeaker]) -> None: + """Unjoin several players from their group.""" + + def _unjoin_all(speakers: list[SonosSpeaker]) -> None: + """Sync helper.""" + # Unjoin slaves first to prevent inheritance of queues + coordinators = [s for s in speakers if s.is_coordinator] + slaves = [s for s in speakers if not s.is_coordinator] + + for speaker in slaves + coordinators: + speaker.unjoin() + + async with hass.data[DATA_SONOS].topology_condition: + await hass.async_add_executor_job(_unjoin_all, speakers) + await SonosSpeaker.wait_for_groups(hass, [[s] for s in speakers]) + + @soco_error() + def snapshot(self, with_group: bool) -> None: + """Snapshot the state of a player.""" + self.soco_snapshot = Snapshot(self.soco) + self.soco_snapshot.snapshot() + if with_group: + self.snapshot_group = self.sonos_group.copy() + else: + self.snapshot_group = None + + @staticmethod + async def snapshot_multi( + hass: HomeAssistant, speakers: list[SonosSpeaker], with_group: bool + ) -> None: + """Snapshot all the speakers and optionally their groups.""" + + def _snapshot_all(speakers: list[SonosSpeaker]) -> None: + """Sync helper.""" + for speaker in speakers: + speaker.snapshot(with_group) + + # Find all affected players + speakers_set = set(speakers) + if with_group: + for speaker in list(speakers_set): + speakers_set.update(speaker.sonos_group) + + async with hass.data[DATA_SONOS].topology_condition: + await hass.async_add_executor_job(_snapshot_all, speakers_set) + + @soco_error() + def restore(self) -> None: + """Restore a snapshotted state to a player.""" + try: + assert self.soco_snapshot is not None + self.soco_snapshot.restore() + except (TypeError, AssertionError, AttributeError, SoCoException) as ex: + # Can happen if restoring a coordinator onto a current slave + _LOGGER.warning("Error on restore %s: %s", self.zone_name, ex) + + self.soco_snapshot = None + self.snapshot_group = None + + @staticmethod + async def restore_multi( + hass: HomeAssistant, speakers: list[SonosSpeaker], with_group: bool + ) -> None: + """Restore snapshots for all the speakers.""" + + def _restore_groups( + speakers: list[SonosSpeaker], with_group: bool + ) -> list[list[SonosSpeaker]]: + """Pause all current coordinators and restore groups.""" + for speaker in (s for s in speakers if s.is_coordinator): + if speaker.media.playback_status == STATE_PLAYING: + hass.async_create_task(speaker.soco.pause()) + + groups = [] + + if with_group: + # Unjoin slaves first to prevent inheritance of queues + for speaker in [s for s in speakers if not s.is_coordinator]: + if speaker.snapshot_group != speaker.sonos_group: + speaker.unjoin() + + # Bring back the original group topology + for speaker in (s for s in speakers if s.snapshot_group): + assert speaker.snapshot_group is not None + if speaker.snapshot_group[0] == speaker: + speaker.join(speaker.snapshot_group) + groups.append(speaker.snapshot_group.copy()) + + return groups + + def _restore_players(speakers: list[SonosSpeaker]) -> None: + """Restore state of all players.""" + for speaker in (s for s in speakers if not s.is_coordinator): + speaker.restore() + + for speaker in (s for s in speakers if s.is_coordinator): + speaker.restore() + + # Find all affected players + speakers_set = {s for s in speakers if s.soco_snapshot} + if with_group: + for speaker in [s for s in speakers_set if s.snapshot_group]: + assert speaker.snapshot_group is not None + speakers_set.update(speaker.snapshot_group) + + async with hass.data[DATA_SONOS].topology_condition: + groups = await hass.async_add_executor_job( + _restore_groups, speakers_set, with_group + ) + await SonosSpeaker.wait_for_groups(hass, groups) + await hass.async_add_executor_job(_restore_players, speakers_set) + + @staticmethod + async def wait_for_groups( + hass: HomeAssistant, groups: list[list[SonosSpeaker]] + ) -> None: + """Wait until all groups are present, or timeout.""" + + def _test_groups(groups: list[list[SonosSpeaker]]) -> bool: + """Return whether all groups exist now.""" + for group in groups: + coordinator = group[0] + + # Test that coordinator is coordinating + current_group = coordinator.sonos_group + if coordinator != current_group[0]: + return False + + # Test that slaves match + if set(group[1:]) != set(current_group[1:]): + return False + + return True + + try: + with async_timeout.timeout(5): + while not _test_groups(groups): + await hass.data[DATA_SONOS].topology_condition.wait() + except asyncio.TimeoutError: + _LOGGER.warning("Timeout waiting for target groups %s", groups) + + for speaker in hass.data[DATA_SONOS].discovered.values(): + speaker.soco._zgs_cache.clear() # pylint: disable=protected-access + + def set_favorites(self) -> None: + """Set available favorites.""" + self.favorites = [] + for fav in self.soco.music_library.get_sonos_favorites(): + try: + # Exclude non-playable favorites with no linked resources + if fav.reference.resources: + self.favorites.append(fav) + except SoCoException as ex: + # Skip unknown types + _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) + + @callback + def async_update_content(self, event: SonosEvent | None = None) -> None: + """Update information about available content.""" + if event and "favorites_update_id" in event.variables: + self.hass.async_add_job(self.set_favorites) + self.async_write_entity_states() + + def update_volume(self) -> None: + """Update information about current volume settings.""" + self.volume = self.soco.volume + self.muted = self.soco.mute + self.night_mode = self.soco.night_mode + self.dialog_mode = self.soco.dialog_mode + + @callback + def async_update_volume(self, event: SonosEvent) -> None: + """Update information about currently volume settings.""" + variables = event.variables + + if "volume" in variables: + self.volume = int(variables["volume"]["Master"]) + + if "mute" in variables: + self.muted = variables["mute"]["Master"] == "1" + + if "night_mode" in variables: + self.night_mode = variables["night_mode"] == "1" + + if "dialog_level" in variables: + self.dialog_mode = variables["dialog_level"] == "1" + + self.async_write_entity_states() + + @callback + def async_update_media(self, event: SonosEvent | None = None) -> None: + """Update information about currently playing media.""" + self.hass.async_add_executor_job(self.update_media, event) + + def update_media(self, event: SonosEvent | None = None) -> None: + """Update information about currently playing media.""" + variables = event and event.variables + + if variables and "transport_state" in variables: + # If the transport has an error then transport_state will + # not be set + new_status = variables["transport_state"] + else: + transport_info = self.soco.get_current_transport_info() + new_status = transport_info["current_transport_state"] + + # Ignore transitions, we should get the target state soon + if new_status == "TRANSITIONING": + return + + self.media.clear() + update_position = new_status != self.media.playback_status + self.media.playback_status = new_status + + if variables: + self.media.play_mode = variables["current_play_mode"] + track_uri = variables["current_track_uri"] + music_source = self.soco.music_source_from_uri(track_uri) + else: + # This causes a network round-trip so we avoid it when possible + self.media.play_mode = self.soco.play_mode + music_source = self.soco.music_source + + if music_source == MUSIC_SRC_TV: + self.update_media_linein(SOURCE_TV) + elif music_source == MUSIC_SRC_LINE_IN: + self.update_media_linein(SOURCE_LINEIN) + else: + track_info = self.soco.get_current_track_info() + if not track_info["uri"]: + self.media.clear_position() + else: + self.media.uri = track_info["uri"] + self.media.artist = track_info.get("artist") + self.media.album_name = track_info.get("album") + self.media.title = track_info.get("title") + + if music_source == MUSIC_SRC_RADIO: + self.update_media_radio(variables) + else: + self.update_media_music(update_position, track_info) + + self.write_entity_states() + + # Also update slaves + speakers = self.hass.data[DATA_SONOS].discovered.values() + for speaker in speakers: + if speaker.coordinator == self: + speaker.write_entity_states() + + def update_media_linein(self, source: str) -> None: + """Update state when playing from line-in/tv.""" + self.media.clear_position() + + self.media.title = source + self.media.source_name = source + + def update_media_radio(self, variables: dict) -> None: + """Update state when streaming radio.""" + self.media.clear_position() + + try: + album_art_uri = variables["current_track_meta_data"].album_art_uri + self.media.image_url = self.media.library.build_album_art_full_uri( + album_art_uri + ) + except (TypeError, KeyError, AttributeError): + pass + + # Non-playing radios will not have a current title. Radios without tagging + # can have part of the radio URI as title. In these cases we try to use the + # radio name instead. + try: + uri_meta_data = variables["enqueued_transport_uri_meta_data"] + if isinstance(uri_meta_data, DidlAudioBroadcast) and ( + self.media.playback_status != STATE_PLAYING + or self.soco.music_source_from_uri(self.media.title) == MUSIC_SRC_RADIO + or ( + isinstance(self.media.title, str) + and isinstance(self.media.uri, str) + and self.media.title in self.media.uri + ) + ): + self.media.title = uri_meta_data.title + except (TypeError, KeyError, AttributeError): + pass + + media_info = self.soco.get_current_media_info() + + self.media.channel = media_info["channel"] + + # Check if currently playing radio station is in favorites + for fav in self.favorites: + if fav.reference.get_uri() == media_info["uri"]: + self.media.source_name = fav.title + + def update_media_music(self, update_media_position: bool, track_info: dict) -> None: + """Update state when playing music tracks.""" + self.media.duration = _timespan_secs(track_info.get("duration")) + current_position = _timespan_secs(track_info.get("position")) + + # player started reporting position? + if current_position is not None and self.media.position is None: + update_media_position = True + + # position jumped? + if current_position is not None and self.media.position is not None: + if self.media.playback_status == STATE_PLAYING: + assert self.media.position_updated_at is not None + time_delta = dt_util.utcnow() - self.media.position_updated_at + time_diff = time_delta.total_seconds() + else: + time_diff = 0 + + calculated_position = self.media.position + time_diff + + if abs(calculated_position - current_position) > 1.5: + update_media_position = True + + if current_position is None: + self.media.clear_position() + elif update_media_position: + self.media.position = current_position + self.media.position_updated_at = dt_util.utcnow() + + self.media.image_url = track_info.get("album_art") + + playlist_position = int(track_info.get("playlist_position")) # type: ignore + if playlist_position > 0: + self.media.queue_position = playlist_position - 1 diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index a2eb5d26645..460c9012aeb 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,7 +1,7 @@ """Tests for the Sonos Media Player platform.""" import pytest -from homeassistant.components.sonos import DOMAIN, media_player +from homeassistant.components.sonos import DATA_SONOS, DOMAIN, media_player from homeassistant.const import STATE_IDLE from homeassistant.core import Context from homeassistant.exceptions import Unauthorized @@ -20,18 +20,24 @@ async def test_async_setup_entry_hosts(hass, config_entry, config, soco): """Test static setup.""" await setup_platform(hass, config_entry, config) - entities = list(hass.data[media_player.DATA_SONOS].media_player_entities.values()) - entity = entities[0] - assert entity.soco == soco + speakers = list(hass.data[DATA_SONOS].discovered.values()) + speaker = speakers[0] + assert speaker.soco == soco + + media_player = hass.states.get("media_player.zone_a") + assert media_player.state == STATE_IDLE async def test_async_setup_entry_discover(hass, config_entry, discover): """Test discovery setup.""" await setup_platform(hass, config_entry, {}) - entities = list(hass.data[media_player.DATA_SONOS].media_player_entities.values()) - entity = entities[0] - assert entity.unique_id == "RINCON_test" + speakers = list(hass.data[DATA_SONOS].discovered.values()) + speaker = speakers[0] + assert speaker.soco.uid == "RINCON_test" + + media_player = hass.states.get("media_player.zone_a") + assert media_player.state == STATE_IDLE async def test_services(hass, config_entry, config, hass_read_only_user): From 34c84a6bbb524812af4378bd45dbd98fd42898cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 15:00:12 -0500 Subject: [PATCH 334/852] Reduce boilerplate to abort for matching config entries (#50186) Co-authored-by: Franck Nijhof --- .../components/adguard/config_flow.py | 10 +--- .../components/ambiclimate/config_flow.py | 9 +-- .../components/broadlink/config_flow.py | 6 +- .../components/denonavr/config_flow.py | 4 +- .../components/dunehd/config_flow.py | 3 +- .../components/emulated_roku/config_flow.py | 8 +-- .../components/forked_daapd/config_flow.py | 4 +- .../components/foscam/config_flow.py | 10 +--- .../components/fritzbox/config_flow.py | 5 +- .../components/goalzero/config_flow.py | 9 +-- .../components/gogogate2/config_flow.py | 4 +- .../components/hangouts/config_flow.py | 13 +---- .../components/harmony/config_flow.py | 13 +---- homeassistant/components/hue/config_flow.py | 13 +---- .../hunterdouglas_powerview/config_flow.py | 17 +----- .../components/keenetic_ndms2/config_flow.py | 4 +- .../kostal_plenticore/config_flow.py | 14 +---- .../components/litterrobot/config_flow.py | 4 +- .../components/logi_circle/config_flow.py | 9 +-- .../components/lutron_caseta/config_flow.py | 16 +----- .../components/lutron_caseta/const.py | 1 - .../components/motioneye/config_flow.py | 4 +- .../components/mullvad/config_flow.py | 3 +- .../components/onewire/config_flow.py | 23 +++----- .../components/philips_js/config_flow.py | 4 +- .../components/plugwise/config_flow.py | 4 +- .../components/powerwall/config_flow.py | 14 +---- .../components/progettihwsw/config_flow.py | 16 +----- .../components/rachio/config_flow.py | 9 +-- .../components/rainmachine/config_flow.py | 15 ++--- homeassistant/components/roku/config_flow.py | 12 +--- .../components/roomba/config_flow.py | 17 +----- .../components/somfy_mylink/config_flow.py | 17 +----- .../components/songpal/config_flow.py | 13 +---- .../components/subaru/config_flow.py | 5 +- homeassistant/components/tado/config_flow.py | 9 +-- .../components/tibber/config_flow.py | 3 +- .../components/tradfri/config_flow.py | 4 +- .../components/twentemilieu/config_flow.py | 5 +- homeassistant/components/unifi/config_flow.py | 10 +--- .../components/yeelight/config_flow.py | 12 +--- homeassistant/config_entries.py | 11 ++++ .../ambiclimate/test_config_flow.py | 24 +++++--- .../emulated_roku/test_config_flow.py | 19 ++++--- .../kostal_plenticore/test_config_flow.py | 14 ----- .../logi_circle/test_config_flow.py | 24 ++++---- .../lutron_caseta/test_config_flow.py | 2 +- .../twentemilieu/test_config_flow.py | 7 ++- tests/test_config_entries.py | 57 +++++++++++++++++++ 49 files changed, 183 insertions(+), 350 deletions(-) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index 11d97d98d62..bbb6d34954b 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -65,13 +65,9 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is None: return await self._show_setup_form(user_input) - entries = self._async_current_entries() - for entry in entries: - if ( - entry.data[CONF_HOST] == user_input[CONF_HOST] - and entry.data[CONF_PORT] == user_input[CONF_PORT] - ): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) errors = {} diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index d714a6bc2a6..7ef0c5439aa 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -50,8 +50,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle external yaml configuration.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() config = self.hass.data.get(DATA_AMBICLIMATE_IMPL, {}) @@ -63,8 +62,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_auth(self, user_input=None): """Handle a flow start.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() errors = {} @@ -85,8 +83,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_code(self, code=None): """Received code for authentication.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() token_info = await self._get_token_info(code) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 4457f4d2675..884a6a9d102 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -298,11 +298,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_info): """Import a device.""" - if any( - import_info[CONF_HOST] == entry.data[CONF_HOST] - for entry in self._async_current_entries() - ): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: import_info[CONF_HOST]}) return await self.async_step_user(import_info) async def async_step_reauth(self, data): diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index a58b0ae991f..dc97e81bafd 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -199,9 +199,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "unique_id's will not be available", self.host, ) - for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == self.host: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: self.host}) return self.async_create_entry( title=receiver.name, diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index 3094999dc62..cbb248410e0 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -73,8 +73,7 @@ class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle configuration by yaml file.""" self.host = user_input[CONF_HOST] - if self.host_already_configured(self.host): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: self.host}) try: await self.init_device(self.host) diff --git a/homeassistant/components/emulated_roku/config_flow.py b/homeassistant/components/emulated_roku/config_flow.py index f1c00a7f8b4..dd7cd87c96a 100644 --- a/homeassistant/components/emulated_roku/config_flow.py +++ b/homeassistant/components/emulated_roku/config_flow.py @@ -26,12 +26,8 @@ class EmulatedRokuFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - name = user_input[CONF_NAME] - - if name in configured_servers(self.hass): - return self.async_abort(reason="already_configured") - - return self.async_create_entry(title=name, data=user_input) + self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]}) + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) servers_num = len(configured_servers(self.hass)) diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index 1d01218b776..16ebc1f82f7 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -133,9 +133,7 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """ if user_input is not None: # check for any entries with same host, abort if found - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == user_input[CONF_HOST]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) validate_result = await self.validate_input(user_input) if validate_result[0] == "ok": # success _LOGGER.debug("Connected successfully. Creating entry") diff --git a/homeassistant/components/foscam/config_flow.py b/homeassistant/components/foscam/config_flow.py index 23d5b335edc..0ab4e5d9866 100644 --- a/homeassistant/components/foscam/config_flow.py +++ b/homeassistant/components/foscam/config_flow.py @@ -47,13 +47,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): Data has the keys from DATA_SCHEMA with values provided by the user. """ - - for entry in self.hass.config_entries.async_entries(DOMAIN): - if ( - entry.data[CONF_HOST] == data[CONF_HOST] - and entry.data[CONF_PORT] == data[CONF_PORT] - ): - raise AbortFlow("already_configured") + self._async_abort_entries_match( + {CONF_HOST: data[CONF_HOST], CONF_PORT: data[CONF_PORT]} + ) camera = FoscamCamera( data[CONF_HOST], diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 79763d18d2a..ecbcfa0bf68 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -92,10 +92,7 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == user_input[CONF_HOST]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) self._host = user_input[CONF_HOST] self._name = user_input[CONF_HOST] diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index c570554d50e..269885e5c3a 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -28,8 +28,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host = user_input[CONF_HOST] name = user_input[CONF_NAME] - if await self._async_endpoint_existed(host): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: host}) try: await self._async_try_connect(host) @@ -64,12 +63,6 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _async_endpoint_existed(self, endpoint): - for entry in self._async_current_entries(): - if endpoint == entry.data.get(CONF_HOST): - return True - return False - async def _async_try_connect(self, host): session = async_get_clientsession(self.hass) api = Yeti(host, self.hass.loop, session) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index bfe740ecaa5..b70a6120153 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -35,9 +35,7 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): ip_address = discovery_info["host"] - for entry in self._async_current_entries(): - if entry.data.get(CONF_IP_ADDRESS) == ip_address: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address}) self._ip_address = ip_address self._device_type = DEVICE_TYPE_ISMARTGATE diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py index 2f0dafba0c3..adf62d348f4 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -6,7 +6,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import callback from .const import ( CONF_2FA, @@ -22,15 +21,6 @@ from .hangups_utils import ( ) -@callback -def configured_hangouts(hass): - """Return the configures Google Hangouts Account.""" - entries = hass.config_entries.async_entries(HANGOUTS_DOMAIN) - if entries: - return entries[0] - return None - - @config_entries.HANDLERS.register(HANGOUTS_DOMAIN) class HangoutsFlowHandler(config_entries.ConfigFlow): """Config flow Google Hangouts.""" @@ -46,8 +36,7 @@ class HangoutsFlowHandler(config_entries.ConfigFlow): """Handle a flow start.""" errors = {} - if configured_hangouts(self.hass) is not None: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() if user_input is not None: user_email = user_input[CONF_EMAIL] diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index d6ffa3d1787..be765ee4cb0 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -85,8 +85,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] - if self._host_already_configured(parsed_url.hostname): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: parsed_url.hostname}) self.context["title_placeholders"] = {"name": friendly_name} @@ -147,16 +146,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=validated[CONF_NAME], data=data) - def _host_already_configured(self, host): - """See if we already have a harmony entry matching the host.""" - for entry in self._async_current_entries(): - if CONF_HOST not in entry.data: - continue - - if entry.data[CONF_HOST] == host: - return True - return False - def _options_from_user_input(user_input): options = {} diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 7615185b5c4..4a7ebd01fbd 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -125,12 +125,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_HOST): str}), ) - if any( - user_input["host"] == entry.data.get("host") - for entry in self._async_current_entries() - ): - return self.async_abort(reason="already_configured") - + self._async_abort_entries_match({"host": user_input["host"]}) self.bridge = self._async_get_bridge(user_input[CONF_HOST]) return await self.async_step_link() @@ -233,11 +228,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is also triggered by `async_step_discovery`. """ # Check if host exists, abort if so. - if any( - import_info["host"] == entry.data.get("host") - for entry in self._async_current_entries() - ): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({"host": import_info["host"]}) self.bridge = self._async_get_bridge(import_info["host"]) return await self.async_step_link() diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 6ff7a0027ba..3ae60d5e62e 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -7,7 +7,7 @@ from aiopvapi.helpers.aiorequest import AioRequest import async_timeout import voluptuous as vol -from homeassistant import config_entries, core, data_entry_flow, exceptions +from homeassistant import config_entries, core, exceptions from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -73,8 +73,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def _async_validate_or_error(self, host): - if self._host_already_configured(host): - raise data_entry_flow.AbortFlow("already_configured") + self._async_abort_entries_match({CONF_HOST: host}) try: info = await validate_input(self.hass, host) @@ -118,8 +117,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if progress.get("context", {}).get(CONF_HOST) == self.discovered_ip: return self.async_abort(reason="already_in_progress") - if self._host_already_configured(self.discovered_ip): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: self.discovered_ip}) info, error = await self._async_validate_or_error(self.discovered_ip) if error: @@ -148,15 +146,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="link", description_placeholders=self.powerview_config ) - def _host_already_configured(self, host): - """See if we already have a hub with the host address configured.""" - existing_hosts = { - entry.data.get(CONF_HOST) - for entry in self._async_current_entries() - if CONF_HOST in entry.data - } - return host in existing_hosts - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index 7ce03a1bbc5..4437aa12edf 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -47,9 +47,7 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == user_input[CONF_HOST]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) _client = Client( TelnetConnection( diff --git a/homeassistant/components/kostal_plenticore/config_flow.py b/homeassistant/components/kostal_plenticore/config_flow.py index 7e3eb55e630..359efef651a 100644 --- a/homeassistant/components/kostal_plenticore/config_flow.py +++ b/homeassistant/components/kostal_plenticore/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_BASE, CONF_HOST, CONF_PASSWORD -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -23,14 +23,6 @@ DATA_SCHEMA = vol.Schema( ) -@callback -def configured_instances(hass): - """Return a set of configured Kostal Plenticore HOSTS.""" - return { - entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) - } - - async def test_connection(hass: HomeAssistant, data) -> str: """Test the connection to the inverter. @@ -56,8 +48,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): hostname = None if user_input is not None: - if user_input[CONF_HOST] in configured_instances(self.hass): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + try: hostname = await test_connection(self.hass, user_input) except PlenticoreAuthenticationException as ex: diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index d30cd868162..c49d18c5257 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -27,9 +27,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - for entry in self._async_current_entries(): - if entry.data[CONF_USERNAME] == user_input[CONF_USERNAME]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]}) hub = LitterRobotHub(self.hass, user_input) try: diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index ec21d3f6726..ff0fd83ff5b 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -65,8 +65,7 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input=None): """Handle external yaml configuration.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() self.flow_impl = DOMAIN @@ -76,8 +75,7 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow start.""" flows = self.hass.data.get(DATA_FLOW_IMPL, {}) - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() if not flows: return self.async_abort(reason="missing_configuration") @@ -138,8 +136,7 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_code(self, code=None): """Received code for authentication.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() return await self._async_create_session(code) diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index f48dc19b1d7..ec7010295ec 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -14,7 +14,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback from .const import ( - ABORT_REASON_ALREADY_CONFIGURED, ABORT_REASON_CANNOT_CONNECT, BRIDGE_TIMEOUT, CONF_CA_CERTS, @@ -89,8 +88,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle pairing with the hub.""" errors = {} # Abort if existing entry with matching host exists. - if self._async_data_host_is_already_configured(): - return self.async_abort(reason=ABORT_REASON_ALREADY_CONFIGURED) + self._async_abort_entries_match({CONF_HOST: self.data[CONF_HOST]}) self._configure_tls_assets() @@ -155,15 +153,6 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): for asset_key, conf_key in FILE_MAPPING.items(): self.data[conf_key] = TLS_ASSET_TEMPLATE.format(self.bridge_id, asset_key) - @callback - def _async_data_host_is_already_configured(self): - """Check to see if the host is already configured.""" - return any( - self.data[CONF_HOST] == entry.data[CONF_HOST] - for entry in self._async_current_entries() - if CONF_HOST in entry.data - ) - async def async_step_import(self, import_info): """Import a new Caseta bridge as a config entry. @@ -174,8 +163,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.data[CONF_HOST] = host # Abort if existing entry with matching host exists. - if self._async_data_host_is_already_configured(): - return self.async_abort(reason=ABORT_REASON_ALREADY_CONFIGURED) + self._async_abort_entries_match({CONF_HOST: self.data[CONF_HOST]}) self.data[CONF_KEYFILE] = import_info[CONF_KEYFILE] self.data[CONF_CERTFILE] = import_info[CONF_CERTFILE] diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 40a5d2b01fd..89472f3366b 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -9,7 +9,6 @@ CONF_CA_CERTS = "ca_certs" STEP_IMPORT_FAILED = "import_failed" ERROR_CANNOT_CONNECT = "cannot_connect" ABORT_REASON_CANNOT_CONNECT = "cannot_connect" -ABORT_REASON_ALREADY_CONFIGURED = "already_configured" BRIDGE_LEAP = "leap" BRIDGE_LIP = "lip" diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index acba3e16bb2..a8562189d1f 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -121,9 +121,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): # Search for duplicates: there isn't a useful unique_id, but # at least prevent entries with the same motionEye URL. - for existing_entry in self._async_current_entries(include_ignore=False): - if existing_entry.data.get(CONF_URL) == user_input[CONF_URL]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) return self.async_create_entry( title=f"{user_input[CONF_URL]}", diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py index ef737581faf..1b330d4f6a3 100644 --- a/homeassistant/components/mullvad/config_flow.py +++ b/homeassistant/components/mullvad/config_flow.py @@ -17,8 +17,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() errors = {} if user_input is not None: diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 856eb95aa57..468aa6b9acf 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -56,20 +56,6 @@ async def validate_input_owserver( return {"title": host} -def is_duplicate_owserver_entry( - hass: HomeAssistant, user_input: dict[str, Any] -) -> bool: - """Check existing entries for matching host and port.""" - for config_entry in hass.config_entries.async_entries(DOMAIN): - if ( - config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER - and config_entry.data[CONF_HOST] == user_input[CONF_HOST] - and config_entry.data[CONF_PORT] == user_input[CONF_PORT] - ): - return True - return False - - async def validate_input_mount_dir( hass: HomeAssistant, data: dict[str, Any] ) -> dict[str, str]: @@ -125,8 +111,13 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): errors = {} if user_input: # Prevent duplicate entries - if is_duplicate_owserver_entry(self.hass, user_input): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match( + { + CONF_TYPE: CONF_TYPE_OWSERVER, + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + } + ) self.onewire_config.update(user_input) diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 327fa135e61..84303e6ca92 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -49,9 +49,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, conf: dict) -> dict: """Import a configuration from config.yaml.""" - for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == conf[CONF_HOST]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: conf[CONF_HOST]}) return await self.async_step_user( { diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 6ad06f3bfd5..d19c3b49920 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -113,9 +113,7 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_HOST] = self.discovery_info[CONF_HOST] user_input[CONF_PORT] = self.discovery_info.get(CONF_PORT, DEFAULT_PORT) - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == user_input[CONF_HOST]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) try: api = await validate_gw_input(self.hass, user_input) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 538382542ef..bd4e49f45d3 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.components.dhcp import IP_ADDRESS from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD -from homeassistant.core import callback from .const import DOMAIN @@ -60,9 +59,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - if self._async_ip_address_already_configured(discovery_info[IP_ADDRESS]): - return self.async_abort(reason="already_configured") - + self.ip_address = discovery_info[IP_ADDRESS] + self._async_abort_entries_match({CONF_IP_ADDRESS: self.ip_address}) self.ip_address = discovery_info[IP_ADDRESS] self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address} return await self.async_step_user() @@ -111,14 +109,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.ip_address = data[CONF_IP_ADDRESS] return await self.async_step_user() - @callback - def _async_ip_address_already_configured(self, ip_address): - """See if we already have an entry matching the ip_address.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_IP_ADDRESS) == ip_address: - return True - return False - class WrongVersion(exceptions.HomeAssistantError): """Error to indicate the powerwall uses a software version we cannot interact with.""" diff --git a/homeassistant/components/progettihwsw/config_flow.py b/homeassistant/components/progettihwsw/config_flow.py index 9dcda920a64..89d5916a3fd 100644 --- a/homeassistant/components/progettihwsw/config_flow.py +++ b/homeassistant/components/progettihwsw/config_flow.py @@ -15,17 +15,6 @@ DATA_SCHEMA = vol.Schema( async def validate_input(hass: core.HomeAssistant, data): """Validate the user host input.""" - confs = hass.config_entries.async_entries(DOMAIN) - same_entries = [ - True - for entry in confs - if entry.data.get("host") == data["host"] - and entry.data.get("port") == data["port"] - ] - - if same_entries: - raise ExistingEntry - api_instance = ProgettiHWSWAPI(f'{data["host"]}:{data["port"]}') is_valid = await api_instance.check_board() @@ -80,13 +69,14 @@ class ProgettiHWSWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: + self._async_abort_entries_match( + {"host": user_input["host"], "port": user_input["port"]} + ) try: info = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" - except ExistingEntry: - return self.async_abort(reason="already_configured") except Exception: # pylint: disable=broad-except errors["base"] = "unknown" else: diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 7d73344aadc..b664ebcd45d 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -79,14 +79,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" - if self._async_current_entries(): - # We can see rachio on the network to tell them to configure - # it, but since the device will not give up the account it is - # bound to and there can be multiple rachio systems on a single - # account, we avoid showing the device as discovered once - # they already have one configured as they can always - # add a new one via "+" - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() properties = { key.lower(): value for (key, value) in discovery_info["properties"].items() } diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 9febb2b3a92..55ff68c5ea0 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -6,7 +6,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.typing import DiscoveryInfoType @@ -53,14 +52,6 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return RainMachineOptionsFlowHandler(config_entry) - @callback - def _async_abort_ip_address_configured(self, ip_address): - """Abort if we already have an entry for the ip.""" - # IP already configured - for entry in self._async_current_entries(include_ignore=False): - if ip_address == entry.data[CONF_IP_ADDRESS]: - raise AbortFlow("already_configured") - async def async_step_homekit(self, discovery_info): """Handle a flow initialized by homekit discovery.""" return await self.async_step_zeroconf(discovery_info) @@ -69,7 +60,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle discovery via zeroconf.""" ip_address = discovery_info["host"] - self._async_abort_ip_address_configured(ip_address) + self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address}) # Handle IP change for entry in self._async_current_entries(include_ignore=False): # Try our existing credentials to check for ip change @@ -109,7 +100,9 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle the start of the config flow.""" errors = {} if user_input: - self._async_abort_ip_address_configured(user_input[CONF_IP_ADDRESS]) + self._async_abort_entries_match( + {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]} + ) controller = await async_get_controller( self.hass, user_input[CONF_IP_ADDRESS], diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 0accf352fb1..470dccbe37f 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -88,8 +88,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): # If we already have the host configured do # not open connections to it if we can avoid it. - if self._host_already_configured(discovery_info[CONF_HOST]): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: discovery_info[CONF_HOST]}) self.discovery_info.update({CONF_HOST: discovery_info[CONF_HOST]}) @@ -151,12 +150,3 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): title=self.discovery_info[CONF_NAME], data=self.discovery_info, ) - - def _host_already_configured(self, host): - """See if we already have a hub with the host address configured.""" - existing_hosts = { - entry.data[CONF_HOST] - for entry in self._async_current_entries() - if CONF_HOST in entry.data - } - return host in existing_hosts diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 0f3bc44eba6..c3ccd051dd8 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -79,8 +79,7 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - if self._async_host_already_configured(discovery_info[IP_ADDRESS]): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: discovery_info[IP_ADDRESS]}) if not discovery_info[HOSTNAME].startswith(("irobot-", "roomba-")): return self.async_abort(reason="not_irobot_device") @@ -183,11 +182,7 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), ) - if any( - user_input["host"] == entry.data.get("host") - for entry in self._async_current_entries() - ): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: user_input["host"]}) self.host = user_input[CONF_HOST] self.blid = user_input[CONF_BLID].upper() @@ -260,14 +255,6 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - @callback - def _async_host_already_configured(self, host): - """See if we already have an entry matching the host.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == host: - return True - return False - class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options.""" diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index e97fe088173..88185629b27 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -60,8 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - if self._host_already_configured(discovery_info[IP_ADDRESS]): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: discovery_info[IP_ADDRESS]}) formatted_mac = format_mac(discovery_info[MAC_ADDRESS]) await self.async_set_unique_id(format_mac(formatted_mac)) @@ -79,8 +78,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - if self._host_already_configured(user_input[CONF_HOST]): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) try: info = await validate_input(self.hass, user_input) @@ -108,18 +106,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input): """Handle import.""" - if self._host_already_configured(user_input[CONF_HOST]): - return self.async_abort(reason="already_configured") - + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) return await self.async_step_user(user_input) - def _host_already_configured(self, host): - """See if we already have an entry matching the host.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == host: - return True - return False - @staticmethod @callback def async_get_options_flow(config_entry): diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py index ae7c9406a92..1475e51afb5 100644 --- a/homeassistant/components/songpal/config_flow.py +++ b/homeassistant/components/songpal/config_flow.py @@ -10,7 +10,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import callback from .const import CONF_ENDPOINT, DOMAIN @@ -75,9 +74,7 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_init(self, user_input=None): """Handle a flow start.""" # Check if already configured - if self._async_endpoint_already_configured(): - return self.async_abort(reason="already_configured") - + self._async_abort_entries_match({CONF_ENDPOINT: self.conf.endpoint}) if user_input is None: return self.async_show_form( step_id="init", @@ -144,11 +141,3 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.conf = SongpalConfig(name, parsed_url.hostname, endpoint) return await self.async_step_init(user_input) - - @callback - def _async_endpoint_already_configured(self): - """See if we already have an endpoint matching user input configured.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_ENDPOINT) == self.conf.endpoint: - return True - return False diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py index 980608893ac..0c41a478c04 100644 --- a/homeassistant/components/subaru/config_flow.py +++ b/homeassistant/components/subaru/config_flow.py @@ -37,10 +37,7 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): error = None if user_input: - if user_input[CONF_USERNAME] in [ - entry.data[CONF_USERNAME] for entry in self._async_current_entries() - ]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]}) try: await self.validate_login_creds(user_input) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index 0b9d9fef9ad..7359273e2de 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -82,14 +82,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" - if self._async_current_entries(): - # We can see tado on the network to tell them to configure - # it, but since the device will not give up the account it is - # bound to and there can be multiple tado devices on a single - # account, we avoid showing the device as discovered once - # they already have one configured as they can always - # add a new one via "+" - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() properties = { key.lower(): value for (key, value) in discovery_info["properties"].items() } diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py index 8e82cbdf733..1c1cef88776 100644 --- a/homeassistant/components/tibber/config_flow.py +++ b/homeassistant/components/tibber/config_flow.py @@ -26,8 +26,7 @@ class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() if user_input is not None: access_token = user_input[CONF_ACCESS_TOKEN].replace(" ", "") diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 0c1fa9bb744..1a6ae8706f2 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -106,9 +106,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input): """Import a config entry.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == user_input["host"]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: user_input["host"]}) # Happens if user has host directly in configuration.yaml if "key" not in user_input: diff --git a/homeassistant/components/twentemilieu/config_flow.py b/homeassistant/components/twentemilieu/config_flow.py index 6d665154778..870ce591788 100644 --- a/homeassistant/components/twentemilieu/config_flow.py +++ b/homeassistant/components/twentemilieu/config_flow.py @@ -66,10 +66,7 @@ class TwenteMilieuFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_address" return await self._show_setup_form(errors) - entries = self._async_current_entries() - for entry in entries: - if entry.data[CONF_ID] == unique_id: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_ID: unique_id}) return self.async_create_entry( title=str(unique_id), diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 8f90e13c9fa..5edc71f9f5d 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -225,8 +225,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): CONF_HOST: parsed_url.hostname, } - if self._host_already_configured(self.config[CONF_HOST]): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: self.config[CONF_HOST]}) await self.async_set_unique_id(mac_address) self._abort_if_unique_id_configured(updates=self.config) @@ -242,13 +241,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return await self.async_step_user() - def _host_already_configured(self, host): - """See if we already have a UniFi entry matching the host.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == host: - return True - return False - class UnifiOptionsFlowHandler(config_entries.OptionsFlow): """Handle Unifi options.""" diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 98767860fd8..3c794792ac3 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -52,8 +52,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) except CannotConnect: errors["base"] = "cannot_connect" - except AlreadyConfigured: - return self.async_abort(reason="already_configured") else: return await self.async_step_pick_device() @@ -114,8 +112,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except CannotConnect: _LOGGER.error("Failed to import %s: cannot connect", host) return self.async_abort(reason="cannot_connect") - except AlreadyConfigured: - return self.async_abort(reason="already_configured") if CONF_NIGHTLIGHT_SWITCH_TYPE in user_input: user_input[CONF_NIGHTLIGHT_SWITCH] = ( user_input.pop(CONF_NIGHTLIGHT_SWITCH_TYPE) @@ -125,9 +121,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_try_connect(self, host): """Set up with options.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == host: - raise AlreadyConfigured + self._async_abort_entries_match({CONF_HOST: host}) bulb = yeelight.Bulb(host) try: @@ -195,7 +189,3 @@ class OptionsFlowHandler(config_entries.OptionsFlow): class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" - - -class AlreadyConfigured(exceptions.HomeAssistantError): - """Indicate the ip address is already configured.""" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8c9ad8da4c5..c586fcad79f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1085,6 +1085,17 @@ class ConfigFlow(data_entry_flow.FlowHandler): """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler + @callback + def _async_abort_entries_match( + self, match_dict: dict[str, Any] | None = None + ) -> None: + """Abort if current entries match all data.""" + if match_dict is None: + match_dict = {} # Match any entry + for entry in self._async_current_entries(include_ignore=False): + if all(item in entry.data.items() for item in match_dict.items()): + raise data_entry_flow.AbortFlow("already_configured") + @callback def _abort_if_unique_id_configured( self, diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 3a325490064..2da550afd42 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -2,14 +2,17 @@ from unittest.mock import AsyncMock, patch import ambiclimate +import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.ambiclimate import config_flow from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp +from tests.common import MockConfigEntry + async def init_config_flow(hass): """Init a configuration flow.""" @@ -40,12 +43,15 @@ async def test_abort_if_already_setup(hass): """Test we abort if Ambiclimate is already setup.""" flow = await init_config_flow(hass) - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await flow.async_step_user() + MockConfigEntry(domain=config_flow.DOMAIN).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): + with pytest.raises(data_entry_flow.AbortFlow): result = await flow.async_step_code() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -103,11 +109,11 @@ async def test_abort_invalid_code(hass): async def test_already_setup(hass): """Test when already setup.""" - config_flow.register_flow_implementation(hass, None, None) - flow = await init_config_flow(hass) - - with patch.object(hass.config_entries, "async_entries", return_value=True): - result = await flow.async_step_user() + MockConfigEntry(domain=config_flow.DOMAIN).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/emulated_roku/test_config_flow.py b/tests/components/emulated_roku/test_config_flow.py index 879d95d0cfc..23c807cbfa3 100644 --- a/tests/components/emulated_roku/test_config_flow.py +++ b/tests/components/emulated_roku/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for emulated_roku config flow.""" +from homeassistant import config_entries from homeassistant.components.emulated_roku import config_flow from tests.common import MockConfigEntry @@ -6,10 +7,10 @@ from tests.common import MockConfigEntry async def test_flow_works(hass): """Test that config flow works.""" - flow = config_flow.EmulatedRokuFlowHandler() - flow.hass = hass - result = await flow.async_step_user( - user_input={"name": "Emulated Roku Test", "listen_port": 8060} + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={"name": "Emulated Roku Test", "listen_port": 8060}, ) assert result["type"] == "create_entry" @@ -22,10 +23,12 @@ async def test_flow_already_registered_entry(hass): MockConfigEntry( domain="emulated_roku", data={"name": "Emulated Roku Test", "listen_port": 8062} ).add_to_hass(hass) - flow = config_flow.EmulatedRokuFlowHandler() - flow.hass = hass - result = await flow.async_step_user( - user_input={"name": "Emulated Roku Test", "listen_port": 8062} + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={"name": "Emulated Roku Test", "listen_port": 8062}, ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 7ce95f71e8e..2e9b25f5721 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import ANY, AsyncMock, MagicMock, patch from kostal.plenticore import PlenticoreAuthenticationException from homeassistant import config_entries, setup -from homeassistant.components.kostal_plenticore import config_flow from homeassistant.components.kostal_plenticore.const import DOMAIN from tests.common import MockConfigEntry @@ -188,16 +187,3 @@ async def test_already_configured(hass): assert result2["type"] == "abort" assert result2["reason"] == "already_configured" - - -def test_configured_instances(hass): - """Test configured_instances returns all configured hosts.""" - MockConfigEntry( - domain="kostal_plenticore", - data={"host": "2.2.2.2", "password": "foobar"}, - unique_id="112233445566", - ).add_to_hass(hass) - - result = config_flow.configured_instances(hass) - - assert result == {"2.2.2.2"} diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 28335b93ad9..dbd35469d79 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.logi_circle import config_flow from homeassistant.components.logi_circle.config_flow import ( DOMAIN, @@ -13,7 +13,7 @@ from homeassistant.components.logi_circle.config_flow import ( ) from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.common import MockConfigEntry, mock_coro class MockRequest: @@ -121,24 +121,26 @@ async def test_abort_if_no_implementation_registered(hass): async def test_abort_if_already_setup(hass): """Test we abort if Logi Circle is already setup.""" flow = init_config_flow(hass) + MockConfigEntry(domain=config_flow.DOMAIN).add_to_hass(hass) - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await flow.async_step_import() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): + with pytest.raises(data_entry_flow.AbortFlow): result = await flow.async_step_code() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await flow.async_step_auth() + result = await flow.async_step_auth() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "external_setup" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 14adefc37db..36e86778d0a 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -189,7 +189,7 @@ async def test_duplicate_bridge_import(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == CasetaConfigFlow.ABORT_REASON_ALREADY_CONFIGURED + assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index 93ee7116f76..e4e4b0c8335 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -1,7 +1,8 @@ """Tests for the Twente Milieu config flow.""" import aiohttp -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.twentemilieu import config_flow from homeassistant.components.twentemilieu.const import ( CONF_HOUSE_LETTER, CONF_HOUSE_NUMBER, @@ -83,7 +84,9 @@ async def test_address_already_set_up( ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 0c12e69364c..77f4fdd0361 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2755,3 +2755,60 @@ async def test_setup_retrying_during_shutdown(hass): await hass.async_block_till_done() assert len(mock_call.return_value.mock_calls) == 0 + + +@pytest.mark.parametrize( + "matchers, reason", + [ + ({}, "already_configured"), + ({"host": "3.3.3.3"}, "no_match"), + ({"host": "3.4.5.6"}, "already_configured"), + ({"host": "3.4.5.6", "ip": "3.4.5.6"}, "no_match"), + ({"host": "3.4.5.6", "ip": "1.2.3.4"}, "already_configured"), + ({"host": "3.4.5.6", "ip": "1.2.3.4", "port": 23}, "already_configured"), + ({"ip": "9.9.9.9"}, "already_configured"), + ({"ip": "7.7.7.7"}, "no_match"), # ignored + ], +) +async def test__async_abort_entries_match(hass, manager, matchers, reason): + """Test aborting if matching config entries exist.""" + MockConfigEntry( + domain="comp", data={"ip": "1.2.3.4", "host": "4.5.6.7", "port": 23} + ).add_to_hass(hass) + MockConfigEntry( + domain="comp", data={"ip": "9.9.9.9", "host": "4.5.6.7", "port": 23} + ).add_to_hass(hass) + MockConfigEntry( + domain="comp", data={"ip": "1.2.3.4", "host": "3.4.5.6", "port": 23} + ).add_to_hass(hass) + MockConfigEntry( + domain="comp", + source=config_entries.SOURCE_IGNORE, + data={"ip": "7.7.7.7", "host": "4.5.6.7", "port": 23}, + ).add_to_hass(hass) + + await async_setup_component(hass, "persistent_notification", {}) + + mock_setup_entry = AsyncMock(return_value=True) + + mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + self._async_abort_entries_match(matchers) + return self.async_abort(reason="no_match") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow, "beer": 5}): + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == reason From 214fd41bb6060ccbaad8345c8b119db04a34f6d1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 11 May 2021 22:18:18 +0200 Subject: [PATCH 335/852] Add fail-fast for wheel (#50487) --- .github/workflows/wheels.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 16818a37cb2..44ff9f8e5fe 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -60,6 +60,7 @@ jobs: needs: init runs-on: ubuntu-latest strategy: + fail-fast: false matrix: arch: ${{ fromJson(needs.init.outputs.architectures) }} tag: @@ -100,6 +101,7 @@ jobs: needs: init runs-on: ubuntu-latest strategy: + fail-fast: false matrix: arch: ${{ fromJson(needs.init.outputs.architectures) }} tag: From d877c0c1ff3763fa95634a81d8d004c0acd5fb7e Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 11 May 2021 22:56:52 +0200 Subject: [PATCH 336/852] Add Fritz services (#50056) --- .coveragerc | 1 + homeassistant/components/fritz/__init__.py | 5 ++ homeassistant/components/fritz/common.py | 27 +++++++++ homeassistant/components/fritz/const.py | 5 +- homeassistant/components/fritz/services.py | 62 ++++++++++++++++++++ homeassistant/components/fritz/services.yaml | 13 ++++ 6 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fritz/services.py create mode 100644 homeassistant/components/fritz/services.yaml diff --git a/.coveragerc b/.coveragerc index 81f2d17c671..6be3ac8cef6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -335,6 +335,7 @@ omit = homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py homeassistant/components/fritz/sensor.py + homeassistant/components/fritz/services.py homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/base.py diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 5cbaa23c1b5..bc762dadcb7 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -16,6 +16,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .common import FritzBoxTools, FritzData from .const import DATA_FRITZ, DOMAIN, PLATFORMS +from .services import async_setup_services, async_unload_services _LOGGER = logging.getLogger(__name__) @@ -55,6 +56,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Load the other platforms like switch hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await async_setup_services(hass) + return True @@ -73,4 +76,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + await async_unload_services(hass) + return unload_ok diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 3a6bf132fb6..2708293b327 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -8,10 +8,16 @@ from typing import Any # pylint: disable=import-error from fritzconnection import FritzConnection +from fritzconnection.core.exceptions import ( + FritzActionError, + FritzConnectionException, + FritzServiceError, +) from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -22,6 +28,8 @@ from .const import ( DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, + SERVICE_REBOOT, + SERVICE_RECONNECT, TRACKER_SCAN_INTERVAL, ) @@ -157,6 +165,25 @@ class FritzBoxTools: if new_device: async_dispatcher_send(self.hass, self.signal_device_new) + async def service_fritzbox(self, service: str) -> None: + """Define FRITZ!Box services.""" + _LOGGER.debug("FRITZ!Box router: %s", service) + try: + if service == SERVICE_REBOOT: + await self.hass.async_add_executor_job( + self.connection.call_action, "DeviceConfig1", "Reboot" + ) + elif service == SERVICE_RECONNECT: + await self.hass.async_add_executor_job( + self.connection.call_action, + "WANIPConn1", + "ForceTermination", + ) + except (FritzServiceError, FritzActionError) as ex: + raise HomeAssistantError("Service or parameter unknown") from ex + except FritzConnectionException as ex: + raise HomeAssistantError("Service not supported") from ex + class FritzData: """Storage class for platform global data.""" diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 0bc33786a0f..dafe9f4d126 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -11,11 +11,14 @@ DEFAULT_HOST = "192.168.178.1" DEFAULT_PORT = 49000 DEFAULT_USERNAME = "" - ERROR_AUTH_INVALID = "invalid_auth" ERROR_CONNECTION_ERROR = "connection_error" ERROR_UNKNOWN = "unknown_error" +FRITZ_SERVICES = "fritz_services" +SERVICE_REBOOT = "reboot" +SERVICE_RECONNECT = "reconnect" + TRACKER_SCAN_INTERVAL = 30 UPTIME_DEVIATION = 5 diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py new file mode 100644 index 00000000000..197a138c36e --- /dev/null +++ b/homeassistant/components/fritz/services.py @@ -0,0 +1,62 @@ +"""Services for Fritz integration.""" +import logging + +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.service import async_extract_config_entry_ids + +from .const import DOMAIN, FRITZ_SERVICES, SERVICE_REBOOT, SERVICE_RECONNECT + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_services(hass: HomeAssistant): + """Set up services for Fritz integration.""" + if hass.data.get(FRITZ_SERVICES, False): + return + + hass.data[FRITZ_SERVICES] = True + + async def async_call_fritz_service(service_call): + """Call correct Fritz service.""" + + if not ( + fritzbox_entry_ids := await _async_get_configured_fritz_tools( + hass, service_call + ) + ): + raise HomeAssistantError( + f"Failed to call service '{service_call.service}'. Config entry for target not found" + ) + + for entry in fritzbox_entry_ids: + _LOGGER.debug("Executing service %s", service_call.service) + fritz_tools = hass.data[DOMAIN].get(entry) + await fritz_tools.service_fritzbox(service_call.service) + + for service in [SERVICE_REBOOT, SERVICE_RECONNECT]: + hass.services.async_register(DOMAIN, service, async_call_fritz_service) + + +async def _async_get_configured_fritz_tools( + hass: HomeAssistant, service_call: ServiceCall +): + """Get FritzBoxTools class from config entry.""" + + return [ + entry_id + for entry_id in await async_extract_config_entry_ids(hass, service_call) + if hass.config_entries.async_get_entry(entry_id).domain == DOMAIN + ] + + +async def async_unload_services(hass: HomeAssistant): + """Unload services for Fritz integration.""" + + if not hass.data.get(FRITZ_SERVICES): + return + + hass.data[FRITZ_SERVICES] = False + + hass.services.async_remove(DOMAIN, SERVICE_REBOOT) + hass.services.async_remove(DOMAIN, SERVICE_RECONNECT) diff --git a/homeassistant/components/fritz/services.yaml b/homeassistant/components/fritz/services.yaml new file mode 100644 index 00000000000..87b0e6fca71 --- /dev/null +++ b/homeassistant/components/fritz/services.yaml @@ -0,0 +1,13 @@ +reconnect: + description: Reconnects your FRITZ!Box internet connection + target: + entity: + integration: fritz + domain: binary_sensor + +reboot: + description: Reboots your FRITZ!Box + target: + entity: + integration: fritz + domain: binary_sensor From 897dd012cd2b2c30edb36c754429e6e8aba233e5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 11 May 2021 16:00:56 -0500 Subject: [PATCH 337/852] Handle transport errors when updating media via events (#50481) --- homeassistant/components/sonos/speaker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 7c44245bf5d..8b56b38af8f 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -722,12 +722,11 @@ class SonosSpeaker: update_position = new_status != self.media.playback_status self.media.playback_status = new_status - if variables: + if variables and "transport_state" in variables: self.media.play_mode = variables["current_play_mode"] track_uri = variables["current_track_uri"] music_source = self.soco.music_source_from_uri(track_uri) else: - # This causes a network round-trip so we avoid it when possible self.media.play_mode = self.soco.play_mode music_source = self.soco.music_source @@ -765,7 +764,7 @@ class SonosSpeaker: self.media.title = source self.media.source_name = source - def update_media_radio(self, variables: dict) -> None: + def update_media_radio(self, variables: dict | None) -> None: """Update state when streaming radio.""" self.media.clear_position() From afe02a4ad2b19108e2d970561465c2a1d401b369 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 11 May 2021 16:06:51 -0500 Subject: [PATCH 338/852] Fix Sonos const comparison (#50482) * Fix Sonos const comparison * Use constants for playback states --- homeassistant/components/sonos/const.py | 3 +++ homeassistant/components/sonos/media_player.py | 7 ++++++- homeassistant/components/sonos/speaker.py | 11 ++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 6cecf5169d1..e016a473328 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -33,6 +33,9 @@ SONOS_ALBUM_ARTIST = "album_artists" SONOS_TRACKS = "tracks" SONOS_COMPOSER = "composers" +SONOS_STATE_PLAYING = "PLAYING" +SONOS_STATE_TRANSITIONING = "TRANSITIONING" + EXPANDABLE_MEDIA_TYPES = [ MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2150cd3a464..45da26644e3 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -58,6 +58,8 @@ from .const import ( MEDIA_TYPES_TO_SONOS, PLAYABLE_MEDIA_TYPES, SONOS_CREATE_MEDIA_PLAYER, + SONOS_STATE_PLAYING, + SONOS_STATE_TRANSITIONING, SOURCE_LINEIN, SOURCE_TV, ) @@ -281,7 +283,10 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if self.media.title is None: return STATE_IDLE return STATE_PAUSED - if self.media.playback_status in ("PLAYING", "TRANSITIONING"): + if self.media.playback_status in ( + SONOS_STATE_PLAYING, + SONOS_STATE_TRANSITIONING, + ): return STATE_PLAYING return STATE_IDLE diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 8b56b38af8f..0b40b23fe40 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -20,7 +20,6 @@ from pysonos.snapshot import Snapshot from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as ent_reg from homeassistant.helpers.dispatcher import ( @@ -43,6 +42,8 @@ from .const import ( SONOS_ENTITY_UPDATE, SONOS_GROUP_UPDATE, SONOS_SEEN, + SONOS_STATE_PLAYING, + SONOS_STATE_TRANSITIONING, SONOS_STATE_UPDATED, SOURCE_LINEIN, SOURCE_TV, @@ -578,7 +579,7 @@ class SonosSpeaker: ) -> list[list[SonosSpeaker]]: """Pause all current coordinators and restore groups.""" for speaker in (s for s in speakers if s.is_coordinator): - if speaker.media.playback_status == STATE_PLAYING: + if speaker.media.playback_status == SONOS_STATE_PLAYING: hass.async_create_task(speaker.soco.pause()) groups = [] @@ -715,7 +716,7 @@ class SonosSpeaker: new_status = transport_info["current_transport_state"] # Ignore transitions, we should get the target state soon - if new_status == "TRANSITIONING": + if new_status == SONOS_STATE_TRANSITIONING: return self.media.clear() @@ -782,7 +783,7 @@ class SonosSpeaker: try: uri_meta_data = variables["enqueued_transport_uri_meta_data"] if isinstance(uri_meta_data, DidlAudioBroadcast) and ( - self.media.playback_status != STATE_PLAYING + self.media.playback_status != SONOS_STATE_PLAYING or self.soco.music_source_from_uri(self.media.title) == MUSIC_SRC_RADIO or ( isinstance(self.media.title, str) @@ -814,7 +815,7 @@ class SonosSpeaker: # position jumped? if current_position is not None and self.media.position is not None: - if self.media.playback_status == STATE_PLAYING: + if self.media.playback_status == SONOS_STATE_PLAYING: assert self.media.position_updated_at is not None time_delta = dt_util.utcnow() - self.media.position_updated_at time_diff = time_delta.total_seconds() From 3a93151aa2bd55059aba02b4e276e275a382bb66 Mon Sep 17 00:00:00 2001 From: karliemeads <68717336+karliemeads@users.noreply.github.com> Date: Tue, 11 May 2021 17:31:36 -0400 Subject: [PATCH 339/852] Improve light tests for brightness step and profiles (#49887) --- tests/components/light/test_init.py | 57 +++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index ef781b56a56..432959b67e4 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -562,7 +562,7 @@ async def test_default_profiles_group(hass, mock_light_profiles): @pytest.mark.parametrize( - "extra_call_params, expected_params", + "extra_call_params, expected_params_state_was_off, expected_params_state_was_on", ( ( {}, @@ -571,6 +571,11 @@ async def test_default_profiles_group(hass, mock_light_profiles): light.ATTR_BRIGHTNESS: 100, light.ATTR_TRANSITION: 3, }, + { + light.ATTR_HS_COLOR: (50.353, 100), + light.ATTR_BRIGHTNESS: 100, + light.ATTR_TRANSITION: 3, + }, ), ( {light.ATTR_BRIGHTNESS: 22}, @@ -579,6 +584,10 @@ async def test_default_profiles_group(hass, mock_light_profiles): light.ATTR_BRIGHTNESS: 22, light.ATTR_TRANSITION: 3, }, + { + light.ATTR_BRIGHTNESS: 22, + light.ATTR_TRANSITION: 3, + }, ), ( {light.ATTR_TRANSITION: 22}, @@ -587,6 +596,9 @@ async def test_default_profiles_group(hass, mock_light_profiles): light.ATTR_BRIGHTNESS: 100, light.ATTR_TRANSITION: 22, }, + { + light.ATTR_TRANSITION: 22, + }, ), ( { @@ -599,6 +611,11 @@ async def test_default_profiles_group(hass, mock_light_profiles): light.ATTR_BRIGHTNESS: 11, light.ATTR_TRANSITION: 1, }, + { + light.ATTR_HS_COLOR: (38.88, 49.02), + light.ATTR_BRIGHTNESS: 11, + light.ATTR_TRANSITION: 1, + }, ), ( {light.ATTR_BRIGHTNESS: 11, light.ATTR_TRANSITION: 1}, @@ -607,11 +624,19 @@ async def test_default_profiles_group(hass, mock_light_profiles): light.ATTR_BRIGHTNESS: 11, light.ATTR_TRANSITION: 1, }, + { + light.ATTR_BRIGHTNESS: 11, + light.ATTR_TRANSITION: 1, + }, ), ), ) async def test_default_profiles_light( - hass, mock_light_profiles, extra_call_params, expected_params + hass, + mock_light_profiles, + extra_call_params, + expected_params_state_was_off, + expected_params_state_was_on, ): """Test default turn-on light profile for a specific light.""" platform = getattr(hass.components, "test.light") @@ -639,14 +664,26 @@ async def test_default_profiles_light( ) _, data = dev.last_call("turn_on") - assert data == expected_params + assert data == expected_params_state_was_off await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: dev.entity_id, - light.ATTR_BRIGHTNESS: 0, + **extra_call_params, + }, + blocking=True, + ) + + _, data = dev.last_call("turn_on") + assert data == expected_params_state_was_on + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: dev.entity_id, }, blocking=True, ) @@ -752,6 +789,18 @@ async def test_light_brightness_step(hass): _, data = entity1.last_call("turn_on") assert data["brightness"] == 66 # 40 + (255 * 0.10) + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": entity0.entity_id, + "brightness_step": -126, + }, + blocking=True, + ) + + assert entity0.state == "off" # 126 - 126; brightness is 0, light should turn off + async def test_light_brightness_pct_conversion(hass): """Test that light brightness percent conversion.""" From d29e8120331cebb82554c2d73105a7d2b662c631 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 12 May 2021 00:44:26 +0300 Subject: [PATCH 340/852] New overrides in universal media player (#48611) * Update media_player.py fix missing overrides in universal * Update media_player.py Black * add tests and allow overrides for missing services * switch sync to async * Update tests/components/universal/test_media_player.py Co-authored-by: Martin Hjelmare * setup component after modifying config * switch test to sync * fix black * fix test * rework tests, disable override media_seek Co-authored-by: raman325 <7243222+raman325@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- .../components/universal/media_player.py | 24 ++- .../components/universal/test_media_player.py | 179 +++++++++++++++++- 2 files changed, 192 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index f6e770c126f..17dcde55381 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -46,6 +46,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_REPEAT_SET, SUPPORT_SELECT_SOUND_MODE, @@ -485,6 +486,9 @@ class UniversalMediaPlayer(MediaPlayerEntity): ): flags |= SUPPORT_SELECT_SOURCE + if SERVICE_PLAY_MEDIA in self._cmds: + flags |= SUPPORT_PLAY_MEDIA + if SERVICE_CLEAR_PLAYLIST in self._cmds: flags |= SUPPORT_CLEAR_PLAYLIST @@ -538,23 +542,25 @@ class UniversalMediaPlayer(MediaPlayerEntity): async def async_media_play(self): """Send play command.""" - await self._async_call_service(SERVICE_MEDIA_PLAY) + await self._async_call_service(SERVICE_MEDIA_PLAY, allow_override=True) async def async_media_pause(self): """Send pause command.""" - await self._async_call_service(SERVICE_MEDIA_PAUSE) + await self._async_call_service(SERVICE_MEDIA_PAUSE, allow_override=True) async def async_media_stop(self): """Send stop command.""" - await self._async_call_service(SERVICE_MEDIA_STOP) + await self._async_call_service(SERVICE_MEDIA_STOP, allow_override=True) async def async_media_previous_track(self): """Send previous track command.""" - await self._async_call_service(SERVICE_MEDIA_PREVIOUS_TRACK) + await self._async_call_service( + SERVICE_MEDIA_PREVIOUS_TRACK, allow_override=True + ) async def async_media_next_track(self): """Send next track command.""" - await self._async_call_service(SERVICE_MEDIA_NEXT_TRACK) + await self._async_call_service(SERVICE_MEDIA_NEXT_TRACK, allow_override=True) async def async_media_seek(self, position): """Send seek command.""" @@ -564,7 +570,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id} - await self._async_call_service(SERVICE_PLAY_MEDIA, data) + await self._async_call_service(SERVICE_PLAY_MEDIA, data, allow_override=True) async def async_volume_up(self): """Turn volume up for media player.""" @@ -576,7 +582,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): async def async_media_play_pause(self): """Play or pause the media player.""" - await self._async_call_service(SERVICE_MEDIA_PLAY_PAUSE) + await self._async_call_service(SERVICE_MEDIA_PLAY_PAUSE, allow_override=True) async def async_select_sound_mode(self, sound_mode): """Select sound mode.""" @@ -592,7 +598,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): async def async_clear_playlist(self): """Clear players playlist.""" - await self._async_call_service(SERVICE_CLEAR_PLAYLIST) + await self._async_call_service(SERVICE_CLEAR_PLAYLIST, allow_override=True) async def async_set_shuffle(self, shuffle): """Enable/disable shuffling.""" @@ -606,7 +612,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): async def async_toggle(self): """Toggle the power on the media player.""" - await self._async_call_service(SERVICE_TOGGLE) + await self._async_call_service(SERVICE_TOGGLE, allow_override=True) async def async_update(self): """Update state in HA.""" diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 7bf19116d93..54617a61348 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -22,7 +22,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Context, callback -from homeassistant.setup import async_setup_component +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant, mock_service @@ -630,7 +630,7 @@ class TestMediaPlayer(unittest.TestCase): def test_supported_features_children_and_cmds(self): """Test supported media commands with children and attrs.""" config = copy(self.config_children_and_attr) - excmd = {"service": "media_player.test", "data": {"entity_id": "test"}} + excmd = {"service": "media_player.test", "data": {}} config["commands"] = { "turn_on": excmd, "turn_off": excmd, @@ -648,6 +648,7 @@ class TestMediaPlayer(unittest.TestCase): "media_next_track": excmd, "media_previous_track": excmd, "toggle": excmd, + "play_media": excmd, "clear_playlist": excmd, } config = validate_config(config) @@ -676,11 +677,185 @@ class TestMediaPlayer(unittest.TestCase): | universal.SUPPORT_STOP | universal.SUPPORT_NEXT_TRACK | universal.SUPPORT_PREVIOUS_TRACK + | universal.SUPPORT_PLAY_MEDIA | universal.SUPPORT_CLEAR_PLAYLIST ) assert check_flags == ump.supported_features + def test_overrides(self): + """Test overrides.""" + config = copy(self.config_children_and_attr) + excmd = {"service": "test.override", "data": {}} + config["name"] = "overridden" + config["commands"] = { + "turn_on": excmd, + "turn_off": excmd, + "volume_up": excmd, + "volume_down": excmd, + "volume_mute": excmd, + "volume_set": excmd, + "select_sound_mode": excmd, + "select_source": excmd, + "repeat_set": excmd, + "shuffle_set": excmd, + "media_play": excmd, + "media_play_pause": excmd, + "media_pause": excmd, + "media_stop": excmd, + "media_next_track": excmd, + "media_previous_track": excmd, + "clear_playlist": excmd, + "play_media": excmd, + "toggle": excmd, + } + setup_component(self.hass, "media_player", {"media_player": config}) + + service = mock_service(self.hass, "test", "override") + self.hass.services.call( + "media_player", + "turn_on", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 1 + self.hass.services.call( + "media_player", + "turn_off", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 2 + self.hass.services.call( + "media_player", + "volume_up", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 3 + self.hass.services.call( + "media_player", + "volume_down", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 4 + self.hass.services.call( + "media_player", + "volume_mute", + service_data={ + "entity_id": "media_player.overridden", + "is_volume_muted": True, + }, + blocking=True, + ) + assert len(service) == 5 + self.hass.services.call( + "media_player", + "volume_set", + service_data={"entity_id": "media_player.overridden", "volume_level": 1}, + blocking=True, + ) + assert len(service) == 6 + self.hass.services.call( + "media_player", + "select_sound_mode", + service_data={ + "entity_id": "media_player.overridden", + "sound_mode": "music", + }, + blocking=True, + ) + assert len(service) == 7 + self.hass.services.call( + "media_player", + "select_source", + service_data={"entity_id": "media_player.overridden", "source": "video1"}, + blocking=True, + ) + assert len(service) == 8 + self.hass.services.call( + "media_player", + "repeat_set", + service_data={"entity_id": "media_player.overridden", "repeat": "all"}, + blocking=True, + ) + assert len(service) == 9 + self.hass.services.call( + "media_player", + "shuffle_set", + service_data={"entity_id": "media_player.overridden", "shuffle": True}, + blocking=True, + ) + assert len(service) == 10 + self.hass.services.call( + "media_player", + "media_play", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 11 + self.hass.services.call( + "media_player", + "media_pause", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 12 + self.hass.services.call( + "media_player", + "media_stop", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 13 + self.hass.services.call( + "media_player", + "media_next_track", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 14 + self.hass.services.call( + "media_player", + "media_previous_track", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 15 + self.hass.services.call( + "media_player", + "clear_playlist", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 16 + self.hass.services.call( + "media_player", + "media_play_pause", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 17 + self.hass.services.call( + "media_player", + "play_media", + service_data={ + "entity_id": "media_player.overridden", + "media_content_id": 1, + "media_content_type": "channel", + }, + blocking=True, + ) + assert len(service) == 18 + self.hass.services.call( + "media_player", + "toggle", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 19 + def test_supported_features_play_pause(self): """Test supported media commands with play_pause function.""" config = copy(self.config_children_and_attr) From c1cf07768b3d0ac7655bd673bca26eea8e2f7390 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 17:06:55 -0500 Subject: [PATCH 341/852] Add dhcp discovery support to isy994 (#50488) - SSDP may not be enabled by default --- .../components/isy994/config_flow.py | 17 +++++++ homeassistant/components/isy994/manifest.json | 3 ++ homeassistant/generated/dhcp.py | 5 ++ tests/components/isy994/test_config_flow.py | 46 ++++++++++++++++++- 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index ce919003622..502008ff0ab 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.components import ssdp +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback @@ -149,6 +150,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle import.""" return await self.async_step_user(user_input) + async def async_step_dhcp(self, discovery_info): + """Handle a discovered isy994 via dhcp.""" + friendly_name = discovery_info[HOSTNAME] + url = f"http://{discovery_info[IP_ADDRESS]}" + mac = discovery_info[MAC_ADDRESS] + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured() + + self.discovered_conf = { + CONF_NAME: friendly_name, + CONF_HOST: url, + } + + self.context["title_placeholders"] = self.discovered_conf + return await self.async_step_user() + async def async_step_ssdp(self, discovery_info): """Handle a discovered isy994.""" friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 8758a9d828b..a2326cbae5f 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -11,5 +11,8 @@ "deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1" } ], + "dhcp": [ + {"hostname":"isy*", "macaddress":"0021B9*"} + ], "iot_class": "local_push" } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 368d0189ab4..dc32b9c4c99 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -81,6 +81,11 @@ DHCP = [ "hostname": "hunter*", "macaddress": "002674*" }, + { + "domain": "isy994", + "hostname": "isy*", + "macaddress": "0021B9*" + }, { "domain": "lyric", "hostname": "lyric-*", diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 1107e184e9b..bf08e6526ba 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components import ssdp +from homeassistant.components import dhcp, ssdp from homeassistant.components.isy994.config_flow import CannotConnect from homeassistant.components.isy994.const import ( CONF_IGNORE_STRING, @@ -15,7 +15,7 @@ from homeassistant.components.isy994.const import ( ISY_URL_POSTFIX, UDN_UUID_PREFIX, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_IMPORT, SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -319,3 +319,45 @@ async def test_form_ssdp(hass: HomeAssistant): assert result2["data"] == MOCK_USER_INPUT assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_dhcp(hass: HomeAssistant): + """Test we can setup from dhcp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data={ + dhcp.IP_ADDRESS: "1.2.3.4", + dhcp.HOSTNAME: "isy994-ems", + dhcp.MAC_ADDRESS: MOCK_UUID, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch(PATCH_CONFIGURATION) as mock_config_class, patch( + PATCH_CONNECTION + ) as mock_connection_class, patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry: + isy_conn = mock_connection_class.return_value + isy_conn.get_config.return_value = None + mock_config_class.return_value = MOCK_VALIDATED_RESPONSE + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" + assert result2["result"].unique_id == MOCK_UUID + assert result2["data"] == MOCK_USER_INPUT + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From dc39edd090c605871cbf18c98dc6aa6229448ec1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 12 May 2021 00:09:22 +0200 Subject: [PATCH 342/852] update denonavr version 0.10.8 (#50476) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index ed6b94e207a..c684c8b0dc5 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.10.7"], + "requirements": ["denonavr==0.10.8"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 1001a9640b3..10c32dbdf43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -482,7 +482,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.10.7 +denonavr==0.10.8 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af92bd17488..110d77282fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ debugpy==1.3.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.10.7 +denonavr==0.10.8 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.3 From 3b272ec54cd4fc59eff2eb22655cee237ea8da53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 17:10:33 -0500 Subject: [PATCH 343/852] Add dhcp discovery to smartthings (#50306) --- .../components/smartthings/manifest.json | 20 ++++++++++++++++++- homeassistant/generated/dhcp.py | 20 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 7d8bc17d430..0c05c5abb90 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -7,5 +7,23 @@ "dependencies": ["webhook"], "after_dependencies": ["cloud"], "codeowners": ["@andrewsayre"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "dhcp": [ + { + "hostname": "st*", + "macaddress": "24FD5B*" + }, + { + "hostname": "smartthings*", + "macaddress": "24FD5B*" + }, + { + "hostname": "hub*", + "macaddress": "24FD5B*" + }, + { + "hostname": "hub*", + "macaddress": "D052A8*" + } + ] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index dc32b9c4c99..a4436a1cebe 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -178,6 +178,26 @@ DHCP = [ "hostname": "sense-*", "macaddress": "DCEFCA*" }, + { + "domain": "smartthings", + "hostname": "st*", + "macaddress": "24FD5B*" + }, + { + "domain": "smartthings", + "hostname": "smartthings*", + "macaddress": "24FD5B*" + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "24FD5B*" + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "D052A8*" + }, { "domain": "solaredge", "hostname": "target", From 7314247ce397492f06fca45d7f1f5c58406e0b10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 17:20:03 -0500 Subject: [PATCH 344/852] Add dhcp support to iSmartGate (#50309) --- .../components/gogogate2/config_flow.py | 28 +++- .../components/gogogate2/manifest.json | 7 +- .../components/gogogate2/strings.json | 3 +- .../components/gogogate2/translations/en.json | 3 +- homeassistant/generated/dhcp.py | 4 + tests/components/gogogate2/__init__.py | 138 ++++++++++++++++++ .../components/gogogate2/test_config_flow.py | 111 +++++++++++++- tests/components/gogogate2/test_cover.py | 133 +---------------- 8 files changed, 292 insertions(+), 135 deletions(-) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index b70a6120153..6fd61b79795 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -6,6 +6,8 @@ from ismartgate.common import AbstractInfoResponse, ApiError from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode import voluptuous as vol +from homeassistant import data_entry_flow +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_DEVICE, @@ -17,6 +19,11 @@ from homeassistant.const import ( from .common import get_api from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN +DEVICE_NAMES = { + DEVICE_TYPE_GOGOGATE2: "Gogogate2", + DEVICE_TYPE_ISMARTGATE: "ismartgate", +} + class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): """Gogogate2 config flow.""" @@ -31,13 +38,25 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_homekit(self, discovery_info): """Handle homekit discovery.""" await self.async_set_unique_id(discovery_info["properties"]["id"]) - self._abort_if_unique_id_configured({CONF_IP_ADDRESS: discovery_info["host"]}) + return await self._async_discovery_handler(discovery_info["host"]) - ip_address = discovery_info["host"] + async def async_step_dhcp(self, discovery_info): + """Handle dhcp discovery.""" + await self.async_set_unique_id(discovery_info[MAC_ADDRESS]) + return await self._async_discovery_handler(discovery_info[IP_ADDRESS]) + + async def _async_discovery_handler(self, ip_address): + """Start the user flow from any discovery.""" + self.context[CONF_IP_ADDRESS] = ip_address + self._abort_if_unique_id_configured({CONF_IP_ADDRESS: ip_address}) self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address}) self._ip_address = ip_address + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_IP_ADDRESS) == self._ip_address: + raise data_entry_flow.AbortFlow("already_in_progress") + self._device_type = DEVICE_TYPE_ISMARTGATE return await self.async_step_user() @@ -83,6 +102,11 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except errors["base"] = "cannot_connect" + if self._ip_address and self._device_type: + self.context["title_placeholders"] = { + CONF_DEVICE: DEVICE_NAMES[self._device_type], + CONF_IP_ADDRESS: self._ip_address, + } return self.async_show_form( step_id="user", data_schema=vol.Schema( diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index a4c07fa1fb8..94a57c47be7 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -1,6 +1,6 @@ { "domain": "gogogate2", - "name": "Gogogate2 and iSmartGate", + "name": "Gogogate2 and ismartgate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", "requirements": ["ismartgate==4.0.0"], @@ -8,5 +8,10 @@ "homekit": { "models": ["iSmartGate"] }, + "dhcp": [ + { + "hostname": "ismartgate*" + } + ], "iot_class": "local_polling" } diff --git a/homeassistant/components/gogogate2/strings.json b/homeassistant/components/gogogate2/strings.json index f5385ff5d54..7c165f90d06 100644 --- a/homeassistant/components/gogogate2/strings.json +++ b/homeassistant/components/gogogate2/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{device} ({ip_address})", "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" @@ -9,7 +10,7 @@ }, "step": { "user": { - "title": "Setup GogoGate2 or iSmartGate", + "title": "Setup Gogogate2 or ismartgate", "description": "Provide requisite information below.", "data": { "ip_address": "[%key:common::config_flow::data::ip%]", diff --git a/homeassistant/components/gogogate2/translations/en.json b/homeassistant/components/gogogate2/translations/en.json index b39bdfd7bb7..53e578526b2 100644 --- a/homeassistant/components/gogogate2/translations/en.json +++ b/homeassistant/components/gogogate2/translations/en.json @@ -7,6 +7,7 @@ "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { @@ -15,7 +16,7 @@ "username": "Username" }, "description": "Provide requisite information below.", - "title": "Setup GogoGate2 or iSmartGate" + "title": "Setup Gogogate2 or ismartgate" } } } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index a4436a1cebe..74287c3a9e4 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -76,6 +76,10 @@ DHCP = [ "hostname": "guardian*", "macaddress": "30AEA4*" }, + { + "domain": "gogogate2", + "hostname": "ismartgate*" + }, { "domain": "hunterdouglas_powerview", "hostname": "hunter*", diff --git a/tests/components/gogogate2/__init__.py b/tests/components/gogogate2/__init__.py index bc867ab646b..f7e3d40a44b 100644 --- a/tests/components/gogogate2/__init__.py +++ b/tests/components/gogogate2/__init__.py @@ -1 +1,139 @@ """Tests for the GogoGate2 component.""" + +from ismartgate.common import ( + DoorMode, + DoorStatus, + GogoGate2Door, + GogoGate2InfoResponse, + ISmartGateDoor, + ISmartGateInfoResponse, + Network, + Outputs, + Wifi, +) + + +def _mocked_gogogate_open_door_response(): + return GogoGate2InfoResponse( + user="user1", + gogogatename="gogogatename0", + model="gogogate2", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abc123.blah.blah", + firmwareversion="222", + apicode="", + door1=GogoGate2Door( + door_id=1, + permission=True, + name="Door1", + gate=False, + mode=DoorMode.GARAGE, + status=DoorStatus.OPENED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + voltage=40, + ), + door2=GogoGate2Door( + door_id=2, + permission=True, + name=None, + gate=True, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + voltage=40, + ), + door3=GogoGate2Door( + door_id=3, + permission=True, + name=None, + gate=False, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + voltage=40, + ), + outputs=Outputs(output1=True, output2=False, output3=True), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) + + +def _mocked_ismartgate_closed_door_response(): + return ISmartGateInfoResponse( + user="user1", + ismartgatename="ismartgatename0", + model="ismartgatePRO", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abc321.blah.blah", + firmwareversion="555", + pin=123, + lang="en", + newfirmware=False, + door1=ISmartGateDoor( + door_id=1, + permission=True, + name="Door1", + gate=False, + mode=DoorMode.GARAGE, + status=DoorStatus.CLOSED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, + voltage=40, + ), + door2=ISmartGateDoor( + door_id=2, + permission=True, + name="Door2", + gate=True, + mode=DoorMode.GARAGE, + status=DoorStatus.CLOSED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, + voltage=40, + ), + door3=ISmartGateDoor( + door_id=3, + permission=True, + name=None, + gate=False, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, + voltage=40, + ), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 3cc70ddf7ab..0722874e9e5 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for the GogoGate2 component.""" from unittest.mock import MagicMock, patch -from ismartgate import GogoGate2Api +from ismartgate import GogoGate2Api, ISmartGateApi from ismartgate.common import ApiError from ismartgate.const import GogoGate2ApiErrorCode @@ -19,7 +19,13 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import _mocked_ismartgate_closed_door_response from tests.common import MockConfigEntry @@ -75,6 +81,24 @@ async def test_auth_fail( assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} + api.reset_mock() + api.async_info.side_effect = ApiError(0, "blah") + result = await hass.config_entries.flow.async_init( + "gogogate2", context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, + CONF_IP_ADDRESS: "127.0.0.2", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + ) + assert result + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + async def test_form_homekit_unique_id_already_setup(hass): """Test that we abort from homekit if gogogate2 is already setup.""" @@ -145,3 +169,86 @@ async def test_form_homekit_ip_address(hass): CONF_PASSWORD: "password", CONF_USERNAME: "username", } + + +@patch("homeassistant.components.gogogate2.async_setup_entry", return_value=True) +@patch("homeassistant.components.gogogate2.common.ISmartGateApi") +async def test_discovered_dhcp( + ismartgateapi_mock, async_setup_entry_mock, hass +) -> None: + """Test we get the form with homekit and abort for dhcp source when we get both.""" + api: ISmartGateApi = MagicMock(spec=ISmartGateApi) + ismartgateapi_mock.return_value = api + + api.reset_mock() + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.4", "macaddress": MOCK_MAC_ADDR}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, + CONF_IP_ADDRESS: "1.2.3.4", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + ) + assert result2 + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + api.reset_mock() + + closed_door_response = _mocked_ismartgate_closed_door_response() + api.async_info.return_value = closed_door_response + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, + CONF_IP_ADDRESS: "1.2.3.4", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + ) + assert result3 + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["data"] == { + "device": "ismartgate", + "ip_address": "1.2.3.4", + "password": "password0", + "username": "user0", + } + + +async def test_discovered_by_homekit_and_dhcp(hass): + """Test we get the form with homekit and abort for dhcp source when we get both.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.4", "macaddress": MOCK_MAC_ADDR}, + ) + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.4", "macaddress": "00:00:00:00:00:00"}, + ) + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_in_progress" diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 5391bf1e4aa..d3507283426 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -9,8 +9,6 @@ from ismartgate.common import ( GogoGate2ActivateResponse, GogoGate2Door, GogoGate2InfoResponse, - ISmartGateDoor, - ISmartGateInfoResponse, Network, Outputs, TransitionDoorStatus, @@ -47,135 +45,14 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow +from . import ( + _mocked_gogogate_open_door_response, + _mocked_ismartgate_closed_door_response, +) + from tests.common import MockConfigEntry, async_fire_time_changed, mock_device_registry -def _mocked_gogogate_open_door_response(): - return GogoGate2InfoResponse( - user="user1", - gogogatename="gogogatename0", - model="gogogate2", - apiversion="", - remoteaccessenabled=False, - remoteaccess="abc123.blah.blah", - firmwareversion="222", - apicode="", - door1=GogoGate2Door( - door_id=1, - permission=True, - name="Door1", - gate=False, - mode=DoorMode.GARAGE, - status=DoorStatus.OPENED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - voltage=40, - ), - door2=GogoGate2Door( - door_id=2, - permission=True, - name=None, - gate=True, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - voltage=40, - ), - door3=GogoGate2Door( - door_id=3, - permission=True, - name=None, - gate=False, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - voltage=40, - ), - outputs=Outputs(output1=True, output2=False, output3=True), - network=Network(ip=""), - wifi=Wifi(SSID="", linkquality="", signal=""), - ) - - -def _mocked_ismartgate_closed_door_response(): - return ISmartGateInfoResponse( - user="user1", - ismartgatename="ismartgatename0", - model="ismartgatePRO", - apiversion="", - remoteaccessenabled=False, - remoteaccess="abc321.blah.blah", - firmwareversion="555", - pin=123, - lang="en", - newfirmware=False, - door1=ISmartGateDoor( - door_id=1, - permission=True, - name="Door1", - gate=False, - mode=DoorMode.GARAGE, - status=DoorStatus.CLOSED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - enabled=True, - apicode="apicode0", - customimage=False, - voltage=40, - ), - door2=ISmartGateDoor( - door_id=2, - permission=True, - name="Door2", - gate=True, - mode=DoorMode.GARAGE, - status=DoorStatus.CLOSED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - enabled=True, - apicode="apicode0", - customimage=False, - voltage=40, - ), - door3=ISmartGateDoor( - door_id=3, - permission=True, - name=None, - gate=False, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - enabled=True, - apicode="apicode0", - customimage=False, - voltage=40, - ), - network=Network(ip=""), - wifi=Wifi(SSID="", linkquality="", signal=""), - ) - - @patch("homeassistant.components.gogogate2.common.GogoGate2Api") async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None: """Test open and close and data update.""" From e2f497ceba47a0948b41fe8103b6f2bf78f2c9db Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 12 May 2021 00:22:07 +0200 Subject: [PATCH 345/852] Fix Netatmo selector for setting persons being at home (#50373) --- homeassistant/components/netatmo/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 06f56d084c6..4cbb7cba2ba 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -48,10 +48,10 @@ set_persons_home: fields: persons: description: List of names - example: Bob + example: "[Alice, Bob]" required: true selector: - text: + object: set_person_away: name: Set person away From e9f8b3e7efeb1eb682d33568f4ed865c1c830107 Mon Sep 17 00:00:00 2001 From: karliemeads <68717336+karliemeads@users.noreply.github.com> Date: Tue, 11 May 2021 18:44:17 -0400 Subject: [PATCH 346/852] Remove unused py_noaa dependency (#50494) --- .github/workflows/wheels.yml | 1 - script/gen_requirements_all.py | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 44ff9f8e5fe..7c83ecec786 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -146,7 +146,6 @@ jobs: 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|# bme680|bme680|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} done diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 23d13ee9de9..4fd96cb1b04 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -27,7 +27,6 @@ COMMENT_REQUIREMENTS = ( "face_recognition", "i2csense", "opencv-python-headless", - "py_noaa", "pybluez", "pycups", "PySwitchbot", From 8d7318430cd724d1d89259cf33107ed9b05a16ff Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 12 May 2021 01:47:02 +0200 Subject: [PATCH 347/852] Fix mypy for Fritz after #50056, #50327 conflict (#50497) --- homeassistant/components/fritz/services.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py index 197a138c36e..1e3a792f9bc 100644 --- a/homeassistant/components/fritz/services.py +++ b/homeassistant/components/fritz/services.py @@ -43,11 +43,12 @@ async def _async_get_configured_fritz_tools( ): """Get FritzBoxTools class from config entry.""" - return [ - entry_id - for entry_id in await async_extract_config_entry_ids(hass, service_call) - if hass.config_entries.async_get_entry(entry_id).domain == DOMAIN - ] + list_entry_id = [] + for entry_id in await async_extract_config_entry_ids(hass, service_call): + config_entry = hass.config_entries.async_get_entry(entry_id) + if config_entry and config_entry.domain == DOMAIN: + list_entry_id.append(entry_id) + return list_entry_id async def async_unload_services(hass: HomeAssistant): From 7df47664e8590dcb58877730b09cde0e5bcc305a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 12 May 2021 00:04:03 +0000 Subject: [PATCH 348/852] [ci skip] Translation update --- .../components/adguard/translations/bg.json | 3 +-- .../components/adguard/translations/ca.json | 3 +-- .../components/adguard/translations/cs.json | 3 +-- .../components/adguard/translations/da.json | 3 +-- .../components/adguard/translations/de.json | 3 +-- .../components/adguard/translations/en.json | 3 +-- .../adguard/translations/es-419.json | 3 +-- .../components/adguard/translations/es.json | 3 +-- .../components/adguard/translations/et.json | 3 +-- .../components/adguard/translations/fr.json | 3 +-- .../components/adguard/translations/hu.json | 3 --- .../components/adguard/translations/id.json | 3 +-- .../components/adguard/translations/it.json | 3 +-- .../components/adguard/translations/ko.json | 3 +-- .../components/adguard/translations/lb.json | 3 +-- .../components/adguard/translations/nl.json | 3 +-- .../components/adguard/translations/no.json | 3 +-- .../components/adguard/translations/pl.json | 3 +-- .../adguard/translations/pt-BR.json | 3 +-- .../components/adguard/translations/pt.json | 3 --- .../components/adguard/translations/ru.json | 3 +-- .../components/adguard/translations/sl.json | 3 +-- .../components/adguard/translations/sv.json | 3 +-- .../components/adguard/translations/tr.json | 3 --- .../components/adguard/translations/uk.json | 3 +-- .../adguard/translations/zh-Hant.json | 3 +-- .../components/airvisual/translations/ca.json | 14 ---------- .../components/airvisual/translations/cs.json | 12 --------- .../components/airvisual/translations/de.json | 14 ---------- .../components/airvisual/translations/en.json | 14 ---------- .../airvisual/translations/es-419.json | 14 ---------- .../components/airvisual/translations/es.json | 14 ---------- .../components/airvisual/translations/et.json | 14 ---------- .../components/airvisual/translations/fi.json | 14 ---------- .../components/airvisual/translations/fr.json | 14 ---------- .../components/airvisual/translations/he.json | 6 ----- .../components/airvisual/translations/hi.json | 15 ----------- .../components/airvisual/translations/hu.json | 12 --------- .../components/airvisual/translations/id.json | 14 ---------- .../components/airvisual/translations/it.json | 14 ---------- .../components/airvisual/translations/ko.json | 14 ---------- .../components/airvisual/translations/lb.json | 14 ---------- .../components/airvisual/translations/nl.json | 14 ---------- .../components/airvisual/translations/no.json | 14 ---------- .../components/airvisual/translations/pl.json | 14 ---------- .../airvisual/translations/pt-BR.json | 11 -------- .../components/airvisual/translations/pt.json | 7 ----- .../components/airvisual/translations/ru.json | 14 ---------- .../components/airvisual/translations/sl.json | 14 ---------- .../components/airvisual/translations/sv.json | 4 --- .../components/airvisual/translations/tr.json | 7 ----- .../components/airvisual/translations/uk.json | 14 ---------- .../components/airvisual/translations/vi.json | 11 -------- .../airvisual/translations/zh-Hant.json | 14 ---------- .../components/apple_tv/translations/ca.json | 2 +- .../components/apple_tv/translations/et.json | 2 +- .../components/apple_tv/translations/no.json | 2 +- .../components/apple_tv/translations/ru.json | 2 +- .../apple_tv/translations/zh-Hant.json | 2 +- .../components/arcam_fmj/translations/ca.json | 2 +- .../components/arcam_fmj/translations/en.json | 1 - .../components/arcam_fmj/translations/et.json | 2 +- .../components/arcam_fmj/translations/no.json | 2 +- .../components/arcam_fmj/translations/ru.json | 2 +- .../arcam_fmj/translations/zh-Hant.json | 2 +- .../components/atag/translations/ca.json | 1 - .../components/atag/translations/cs.json | 1 - .../components/atag/translations/de.json | 1 - .../components/atag/translations/en.json | 1 - .../components/atag/translations/es-419.json | 1 - .../components/atag/translations/es.json | 1 - .../components/atag/translations/et.json | 1 - .../components/atag/translations/fr.json | 1 - .../components/atag/translations/he.json | 11 -------- .../components/atag/translations/hu.json | 1 - .../components/atag/translations/id.json | 1 - .../components/atag/translations/it.json | 1 - .../components/atag/translations/ko.json | 1 - .../components/atag/translations/lb.json | 1 - .../components/atag/translations/nl.json | 1 - .../components/atag/translations/no.json | 1 - .../components/atag/translations/pl.json | 1 - .../components/atag/translations/pt-BR.json | 7 ----- .../components/atag/translations/pt.json | 1 - .../components/atag/translations/ru.json | 1 - .../components/atag/translations/tr.json | 1 - .../components/atag/translations/uk.json | 1 - .../components/atag/translations/zh-Hant.json | 1 - .../components/august/translations/ca.json | 10 ------- .../components/august/translations/cs.json | 10 ------- .../components/august/translations/da.json | 10 ------- .../components/august/translations/de.json | 10 ------- .../components/august/translations/en.json | 10 ------- .../august/translations/es-419.json | 10 ------- .../components/august/translations/es.json | 10 ------- .../components/august/translations/et.json | 10 ------- .../components/august/translations/fr.json | 10 ------- .../components/august/translations/he.json | 12 --------- .../components/august/translations/hu.json | 7 ----- .../components/august/translations/id.json | 10 ------- .../components/august/translations/it.json | 10 ------- .../components/august/translations/ko.json | 10 ------- .../components/august/translations/lb.json | 10 ------- .../components/august/translations/lv.json | 12 --------- .../components/august/translations/nl.json | 10 ------- .../components/august/translations/no.json | 10 ------- .../components/august/translations/pl.json | 10 ------- .../components/august/translations/pt-BR.json | 7 ----- .../components/august/translations/pt.json | 8 ------ .../components/august/translations/ru.json | 10 ------- .../components/august/translations/sl.json | 10 ------- .../components/august/translations/sv.json | 10 ------- .../components/august/translations/tr.json | 9 ------- .../components/august/translations/uk.json | 10 ------- .../august/translations/zh-Hans.json | 11 -------- .../august/translations/zh-Hant.json | 10 ------- .../azure_devops/translations/ca.json | 2 +- .../azure_devops/translations/et.json | 2 +- .../azure_devops/translations/no.json | 2 +- .../azure_devops/translations/ru.json | 2 +- .../azure_devops/translations/zh-Hant.json | 2 +- .../components/blebox/translations/ca.json | 2 +- .../components/blebox/translations/et.json | 2 +- .../components/blebox/translations/no.json | 2 +- .../components/blebox/translations/ru.json | 2 +- .../blebox/translations/zh-Hant.json | 2 +- .../components/bond/translations/ca.json | 2 +- .../components/bond/translations/et.json | 2 +- .../components/bond/translations/no.json | 2 +- .../components/bond/translations/ru.json | 2 +- .../components/bond/translations/zh-Hant.json | 2 +- .../components/brother/translations/ca.json | 2 +- .../components/brother/translations/et.json | 2 +- .../components/brother/translations/no.json | 2 +- .../components/brother/translations/ru.json | 2 +- .../brother/translations/zh-Hant.json | 2 +- .../components/bsblan/translations/ca.json | 2 +- .../components/bsblan/translations/et.json | 2 +- .../components/bsblan/translations/no.json | 2 +- .../components/bsblan/translations/ru.json | 2 +- .../bsblan/translations/zh-Hant.json | 2 +- .../buienradar/translations/de.json | 10 +++++++ .../components/canary/translations/ca.json | 2 +- .../components/canary/translations/et.json | 2 +- .../components/canary/translations/no.json | 2 +- .../components/canary/translations/ru.json | 2 +- .../canary/translations/zh-Hant.json | 2 +- .../components/cast/translations/bg.json | 1 - .../components/cast/translations/ca.json | 9 ------- .../components/cast/translations/cs.json | 1 - .../components/cast/translations/da.json | 1 - .../components/cast/translations/de.json | 17 +++++++----- .../components/cast/translations/en.json | 9 ------- .../components/cast/translations/es-419.json | 11 -------- .../components/cast/translations/es.json | 11 -------- .../components/cast/translations/et.json | 9 ------- .../components/cast/translations/fi.json | 1 - .../components/cast/translations/fr.json | 9 ------- .../components/cast/translations/he.json | 1 - .../components/cast/translations/hu.json | 10 ------- .../components/cast/translations/id.json | 11 -------- .../components/cast/translations/it.json | 9 ------- .../components/cast/translations/ja.json | 3 --- .../components/cast/translations/ko.json | 11 -------- .../components/cast/translations/lb.json | 1 - .../components/cast/translations/nl.json | 9 ------- .../components/cast/translations/nn.json | 1 - .../components/cast/translations/no.json | 9 ------- .../components/cast/translations/pl.json | 11 -------- .../components/cast/translations/pt-BR.json | 1 - .../components/cast/translations/pt.json | 8 ------ .../components/cast/translations/ro.json | 1 - .../components/cast/translations/ru.json | 9 ------- .../components/cast/translations/sl.json | 1 - .../components/cast/translations/sv.json | 1 - .../components/cast/translations/th.json | 3 --- .../components/cast/translations/uk.json | 1 - .../components/cast/translations/vi.json | 1 - .../components/cast/translations/zh-Hans.json | 8 ------ .../components/cast/translations/zh-Hant.json | 9 ------- .../components/climacell/translations/ca.json | 1 - .../components/climacell/translations/de.json | 1 - .../components/climacell/translations/el.json | 1 - .../components/climacell/translations/en.json | 1 - .../components/climacell/translations/es.json | 1 - .../components/climacell/translations/et.json | 1 - .../components/climacell/translations/fr.json | 1 - .../components/climacell/translations/id.json | 1 - .../components/climacell/translations/it.json | 1 - .../components/climacell/translations/ko.json | 1 - .../components/climacell/translations/nl.json | 1 - .../components/climacell/translations/no.json | 1 - .../components/climacell/translations/pl.json | 1 - .../components/climacell/translations/ru.json | 1 - .../climacell/translations/zh-Hant.json | 1 - .../cloudflare/translations/ca.json | 2 +- .../cloudflare/translations/et.json | 2 +- .../cloudflare/translations/no.json | 2 +- .../cloudflare/translations/ru.json | 2 +- .../cloudflare/translations/zh-Hant.json | 2 +- .../components/deconz/translations/ca.json | 2 +- .../components/deconz/translations/et.json | 2 +- .../components/deconz/translations/no.json | 2 +- .../components/deconz/translations/ru.json | 2 +- .../deconz/translations/zh-Hant.json | 2 +- .../components/denonavr/translations/ca.json | 2 +- .../components/denonavr/translations/et.json | 2 +- .../components/denonavr/translations/no.json | 2 +- .../components/denonavr/translations/ru.json | 2 +- .../denonavr/translations/zh-Hant.json | 2 +- .../devolo_home_control/translations/ca.json | 1 - .../devolo_home_control/translations/cs.json | 1 - .../devolo_home_control/translations/de.json | 1 - .../devolo_home_control/translations/en.json | 1 - .../devolo_home_control/translations/es.json | 1 - .../devolo_home_control/translations/et.json | 1 - .../devolo_home_control/translations/fi.json | 1 - .../devolo_home_control/translations/fr.json | 1 - .../devolo_home_control/translations/hu.json | 1 - .../devolo_home_control/translations/id.json | 1 - .../devolo_home_control/translations/it.json | 1 - .../devolo_home_control/translations/ko.json | 1 - .../devolo_home_control/translations/lb.json | 1 - .../devolo_home_control/translations/nl.json | 1 - .../devolo_home_control/translations/no.json | 1 - .../devolo_home_control/translations/pl.json | 1 - .../devolo_home_control/translations/pt.json | 1 - .../devolo_home_control/translations/ru.json | 1 - .../devolo_home_control/translations/sv.json | 1 - .../devolo_home_control/translations/uk.json | 1 - .../translations/zh-Hant.json | 1 - .../components/directv/translations/ca.json | 2 +- .../components/directv/translations/en.json | 1 - .../components/directv/translations/et.json | 2 +- .../components/directv/translations/no.json | 2 +- .../components/directv/translations/ru.json | 2 +- .../directv/translations/zh-Hant.json | 2 +- .../components/doorbird/translations/ca.json | 2 +- .../components/doorbird/translations/et.json | 2 +- .../components/doorbird/translations/no.json | 2 +- .../components/doorbird/translations/ru.json | 2 +- .../doorbird/translations/zh-Hant.json | 2 +- .../components/elgato/translations/ca.json | 2 +- .../components/elgato/translations/et.json | 2 +- .../components/elgato/translations/no.json | 2 +- .../components/elgato/translations/ru.json | 2 +- .../elgato/translations/zh-Hant.json | 2 +- .../components/emonitor/translations/ca.json | 2 +- .../components/emonitor/translations/et.json | 2 +- .../components/emonitor/translations/no.json | 2 +- .../components/emonitor/translations/ru.json | 2 +- .../emonitor/translations/zh-Hant.json | 2 +- .../enphase_envoy/translations/ca.json | 2 +- .../enphase_envoy/translations/et.json | 2 +- .../enphase_envoy/translations/no.json | 2 +- .../enphase_envoy/translations/ru.json | 2 +- .../enphase_envoy/translations/zh-Hant.json | 2 +- .../components/epson/translations/ca.json | 3 +-- .../components/epson/translations/cs.json | 3 +-- .../components/epson/translations/de.json | 6 ++--- .../components/epson/translations/en.json | 3 +-- .../components/epson/translations/es.json | 3 +-- .../components/epson/translations/et.json | 3 +-- .../components/epson/translations/fr.json | 3 +-- .../components/epson/translations/hu.json | 3 +-- .../components/epson/translations/id.json | 3 +-- .../components/epson/translations/it.json | 3 +-- .../components/epson/translations/ka.json | 3 +-- .../components/epson/translations/ko.json | 3 +-- .../components/epson/translations/lb.json | 3 +-- .../components/epson/translations/nl.json | 3 +-- .../components/epson/translations/no.json | 3 +-- .../components/epson/translations/pl.json | 3 +-- .../components/epson/translations/pt.json | 3 +-- .../components/epson/translations/ru.json | 3 +-- .../components/epson/translations/tr.json | 3 +-- .../components/epson/translations/uk.json | 3 +-- .../epson/translations/zh-Hant.json | 3 +-- .../components/esphome/translations/ca.json | 2 +- .../components/esphome/translations/et.json | 2 +- .../components/esphome/translations/no.json | 2 +- .../components/esphome/translations/ru.json | 2 +- .../esphome/translations/zh-Hant.json | 2 +- .../components/ezviz/translations/de.json | 9 +++++-- .../components/flume/translations/de.json | 4 ++- .../forked_daapd/translations/ca.json | 2 +- .../forked_daapd/translations/et.json | 2 +- .../forked_daapd/translations/no.json | 2 +- .../forked_daapd/translations/ru.json | 2 +- .../forked_daapd/translations/zh-Hant.json | 2 +- .../components/fritz/translations/ca.json | 2 +- .../components/fritz/translations/de.json | 4 +-- .../components/fritz/translations/et.json | 2 +- .../components/fritz/translations/no.json | 2 +- .../components/fritz/translations/ru.json | 2 +- .../fritz/translations/zh-Hant.json | 2 +- .../components/fritzbox/translations/ca.json | 2 +- .../components/fritzbox/translations/et.json | 2 +- .../components/fritzbox/translations/no.json | 2 +- .../components/fritzbox/translations/ru.json | 2 +- .../fritzbox/translations/zh-Hant.json | 2 +- .../fritzbox_callmonitor/translations/ca.json | 2 +- .../fritzbox_callmonitor/translations/et.json | 2 +- .../fritzbox_callmonitor/translations/no.json | 2 +- .../fritzbox_callmonitor/translations/ru.json | 2 +- .../translations/zh-Hant.json | 2 +- .../growatt_server/translations/ca.json | 27 +++++++++++++++++++ .../growatt_server/translations/de.json | 26 ++++++++++++++++++ .../growatt_server/translations/et.json | 27 +++++++++++++++++++ .../growatt_server/translations/nl.json | 27 +++++++++++++++++++ .../growatt_server/translations/no.json | 27 +++++++++++++++++++ .../growatt_server/translations/ru.json | 27 +++++++++++++++++++ .../growatt_server/translations/zh-Hant.json | 27 +++++++++++++++++++ .../components/guardian/translations/ca.json | 3 +++ .../components/guardian/translations/en.json | 3 +++ .../components/guardian/translations/et.json | 3 +++ .../components/guardian/translations/nl.json | 3 +++ .../components/guardian/translations/no.json | 3 +++ .../components/guardian/translations/ru.json | 3 +++ .../guardian/translations/zh-Hant.json | 3 +++ .../components/harmony/translations/ca.json | 2 +- .../components/harmony/translations/et.json | 2 +- .../components/harmony/translations/no.json | 2 +- .../components/harmony/translations/ru.json | 2 +- .../harmony/translations/zh-Hant.json | 2 +- .../components/hassio/translations/af.json | 3 --- .../components/hassio/translations/bg.json | 3 --- .../components/hassio/translations/ca.json | 3 +-- .../components/hassio/translations/cs.json | 3 +-- .../components/hassio/translations/cy.json | 3 --- .../components/hassio/translations/da.json | 3 --- .../components/hassio/translations/de.json | 3 +-- .../components/hassio/translations/el.json | 3 --- .../components/hassio/translations/en.json | 3 +-- .../hassio/translations/es-419.json | 3 --- .../components/hassio/translations/es.json | 3 +-- .../components/hassio/translations/et.json | 3 +-- .../components/hassio/translations/eu.json | 3 --- .../components/hassio/translations/fa.json | 3 --- .../components/hassio/translations/fi.json | 3 --- .../components/hassio/translations/fr.json | 3 +-- .../components/hassio/translations/he.json | 3 --- .../components/hassio/translations/hr.json | 3 --- .../components/hassio/translations/hu.json | 3 +-- .../components/hassio/translations/hy.json | 3 --- .../components/hassio/translations/id.json | 3 +-- .../components/hassio/translations/is.json | 3 --- .../components/hassio/translations/it.json | 3 +-- .../components/hassio/translations/ja.json | 3 --- .../components/hassio/translations/ko.json | 3 +-- .../components/hassio/translations/lb.json | 3 +-- .../components/hassio/translations/lt.json | 3 --- .../components/hassio/translations/lv.json | 3 --- .../components/hassio/translations/nl.json | 3 +-- .../components/hassio/translations/nn.json | 3 --- .../components/hassio/translations/no.json | 3 +-- .../components/hassio/translations/pl.json | 3 +-- .../components/hassio/translations/pt-BR.json | 3 --- .../components/hassio/translations/pt.json | 3 +-- .../components/hassio/translations/ro.json | 3 --- .../components/hassio/translations/ru.json | 3 +-- .../components/hassio/translations/sk.json | 3 --- .../components/hassio/translations/sl.json | 3 +-- .../components/hassio/translations/sv.json | 3 --- .../components/hassio/translations/th.json | 3 --- .../components/hassio/translations/tr.json | 3 +-- .../components/hassio/translations/uk.json | 3 +-- .../components/hassio/translations/vi.json | 3 --- .../hassio/translations/zh-Hans.json | 3 +-- .../hassio/translations/zh-Hant.json | 3 +-- .../homeassistant/translations/ca.json | 4 --- .../homeassistant/translations/cs.json | 4 --- .../homeassistant/translations/de.json | 4 --- .../homeassistant/translations/en.json | 4 --- .../homeassistant/translations/es.json | 4 --- .../homeassistant/translations/et.json | 4 --- .../homeassistant/translations/fr.json | 4 --- .../homeassistant/translations/hu.json | 4 --- .../homeassistant/translations/id.json | 4 --- .../homeassistant/translations/it.json | 4 --- .../homeassistant/translations/ka.json | 4 --- .../homeassistant/translations/ko.json | 4 --- .../homeassistant/translations/lb.json | 4 --- .../homeassistant/translations/nl.json | 4 --- .../homeassistant/translations/no.json | 4 --- .../homeassistant/translations/pl.json | 4 --- .../homeassistant/translations/pt.json | 4 --- .../homeassistant/translations/ru.json | 4 --- .../homeassistant/translations/sl.json | 1 - .../homeassistant/translations/tr.json | 4 --- .../homeassistant/translations/uk.json | 4 --- .../homeassistant/translations/zh-Hans.json | 4 --- .../homeassistant/translations/zh-Hant.json | 4 --- .../components/homekit/translations/ca.json | 21 ++------------- .../components/homekit/translations/cs.json | 11 +------- .../components/homekit/translations/de.json | 21 ++------------- .../components/homekit/translations/en.json | 21 ++------------- .../homekit/translations/es-419.json | 4 +-- .../components/homekit/translations/es.json | 21 ++------------- .../components/homekit/translations/et.json | 21 ++------------- .../components/homekit/translations/fr.json | 21 ++------------- .../components/homekit/translations/hu.json | 9 +------ .../components/homekit/translations/id.json | 21 ++------------- .../components/homekit/translations/it.json | 21 ++------------- .../components/homekit/translations/ko.json | 21 ++------------- .../components/homekit/translations/lb.json | 4 +-- .../components/homekit/translations/nl.json | 21 ++------------- .../components/homekit/translations/no.json | 21 ++------------- .../components/homekit/translations/pl.json | 21 ++------------- .../components/homekit/translations/pt.json | 3 +++ .../components/homekit/translations/ru.json | 21 ++------------- .../components/homekit/translations/sl.json | 4 +-- .../components/homekit/translations/sv.json | 5 ---- .../components/homekit/translations/tr.json | 24 ----------------- .../components/homekit/translations/uk.json | 4 +-- .../homekit/translations/zh-Hans.json | 4 +-- .../homekit/translations/zh-Hant.json | 21 ++------------- .../homekit_controller/translations/ca.json | 2 +- .../homekit_controller/translations/et.json | 2 +- .../homekit_controller/translations/no.json | 2 +- .../homekit_controller/translations/ru.json | 2 +- .../translations/zh-Hant.json | 2 +- .../huawei_lte/translations/ca.json | 2 +- .../huawei_lte/translations/en.json | 1 + .../huawei_lte/translations/et.json | 2 +- .../huawei_lte/translations/no.json | 2 +- .../huawei_lte/translations/ru.json | 2 +- .../huawei_lte/translations/zh-Hant.json | 2 +- .../huisbaasje/translations/ca.json | 2 -- .../huisbaasje/translations/cs.json | 2 -- .../huisbaasje/translations/de.json | 2 -- .../huisbaasje/translations/en.json | 2 -- .../huisbaasje/translations/es.json | 2 -- .../huisbaasje/translations/et.json | 2 -- .../huisbaasje/translations/fr.json | 2 -- .../huisbaasje/translations/hu.json | 2 -- .../huisbaasje/translations/id.json | 2 -- .../huisbaasje/translations/it.json | 2 -- .../huisbaasje/translations/ko.json | 2 -- .../huisbaasje/translations/nl.json | 2 -- .../huisbaasje/translations/no.json | 2 -- .../huisbaasje/translations/pl.json | 2 -- .../huisbaasje/translations/ru.json | 2 -- .../huisbaasje/translations/tr.json | 2 -- .../huisbaasje/translations/zh-Hant.json | 2 -- .../components/ialarm/translations/ca.json | 1 - .../components/ialarm/translations/cs.json | 1 - .../components/ialarm/translations/de.json | 1 - .../components/ialarm/translations/en.json | 1 - .../components/ialarm/translations/es.json | 1 - .../components/ialarm/translations/et.json | 1 - .../components/ialarm/translations/fr.json | 1 - .../components/ialarm/translations/id.json | 1 - .../components/ialarm/translations/it.json | 1 - .../components/ialarm/translations/ko.json | 1 - .../components/ialarm/translations/nl.json | 1 - .../components/ialarm/translations/no.json | 1 - .../components/ialarm/translations/pl.json | 1 - .../components/ialarm/translations/ru.json | 1 - .../ialarm/translations/zh-Hant.json | 1 - .../components/ipp/translations/ca.json | 2 +- .../components/ipp/translations/et.json | 2 +- .../components/ipp/translations/no.json | 2 +- .../components/ipp/translations/ru.json | 2 +- .../components/ipp/translations/zh-Hant.json | 2 +- .../components/isy994/translations/ca.json | 2 +- .../components/isy994/translations/et.json | 2 +- .../components/isy994/translations/no.json | 2 +- .../components/isy994/translations/ru.json | 2 +- .../isy994/translations/zh-Hant.json | 2 +- .../keenetic_ndms2/translations/bg.json | 1 - .../keenetic_ndms2/translations/ca.json | 1 - .../keenetic_ndms2/translations/cs.json | 1 - .../keenetic_ndms2/translations/de.json | 1 - .../keenetic_ndms2/translations/en.json | 1 - .../keenetic_ndms2/translations/es.json | 1 - .../keenetic_ndms2/translations/et.json | 1 - .../keenetic_ndms2/translations/fr.json | 1 - .../keenetic_ndms2/translations/hu.json | 1 - .../keenetic_ndms2/translations/id.json | 1 - .../keenetic_ndms2/translations/it.json | 1 - .../keenetic_ndms2/translations/ko.json | 1 - .../keenetic_ndms2/translations/nl.json | 1 - .../keenetic_ndms2/translations/no.json | 1 - .../keenetic_ndms2/translations/pl.json | 1 - .../keenetic_ndms2/translations/ru.json | 1 - .../keenetic_ndms2/translations/zh-Hant.json | 1 - .../components/kodi/translations/ca.json | 2 +- .../components/kodi/translations/et.json | 2 +- .../components/kodi/translations/no.json | 2 +- .../components/kodi/translations/ru.json | 2 +- .../components/kodi/translations/zh-Hant.json | 2 +- .../components/local_ip/translations/ca.json | 3 --- .../components/local_ip/translations/cs.json | 3 --- .../components/local_ip/translations/da.json | 3 --- .../components/local_ip/translations/de.json | 3 --- .../components/local_ip/translations/en.json | 3 --- .../local_ip/translations/es-419.json | 3 --- .../components/local_ip/translations/es.json | 3 --- .../components/local_ip/translations/et.json | 3 --- .../components/local_ip/translations/fr.json | 3 --- .../components/local_ip/translations/hu.json | 3 --- .../components/local_ip/translations/id.json | 3 --- .../components/local_ip/translations/it.json | 3 --- .../components/local_ip/translations/ko.json | 3 --- .../components/local_ip/translations/lb.json | 3 --- .../components/local_ip/translations/nl.json | 3 --- .../components/local_ip/translations/no.json | 3 --- .../components/local_ip/translations/pl.json | 3 --- .../local_ip/translations/pt-BR.json | 3 --- .../components/local_ip/translations/ru.json | 3 --- .../components/local_ip/translations/sl.json | 3 --- .../components/local_ip/translations/sv.json | 3 --- .../components/local_ip/translations/tr.json | 3 --- .../components/local_ip/translations/uk.json | 3 --- .../local_ip/translations/zh-Hant.json | 3 --- .../lutron_caseta/translations/ca.json | 2 +- .../lutron_caseta/translations/et.json | 2 +- .../lutron_caseta/translations/no.json | 2 +- .../lutron_caseta/translations/ru.json | 2 +- .../lutron_caseta/translations/zh-Hant.json | 2 +- .../components/lyric/translations/de.json | 2 +- .../components/mazda/translations/bg.json | 6 ----- .../components/mazda/translations/ca.json | 9 ------- .../components/mazda/translations/cs.json | 7 ----- .../components/mazda/translations/de.json | 9 ------- .../components/mazda/translations/en.json | 9 ------- .../components/mazda/translations/es.json | 9 ------- .../components/mazda/translations/et.json | 9 ------- .../components/mazda/translations/fr.json | 9 ------- .../components/mazda/translations/hu.json | 8 ------ .../components/mazda/translations/id.json | 9 ------- .../components/mazda/translations/it.json | 9 ------- .../components/mazda/translations/ko.json | 9 ------- .../components/mazda/translations/nl.json | 9 ------- .../components/mazda/translations/no.json | 9 ------- .../components/mazda/translations/pl.json | 9 ------- .../components/mazda/translations/ru.json | 9 ------- .../mazda/translations/zh-Hant.json | 9 ------- .../motion_blinds/translations/en.json | 2 ++ .../components/mqtt/translations/de.json | 3 ++- .../components/mullvad/translations/bg.json | 8 ------ .../components/mullvad/translations/ca.json | 6 ----- .../components/mullvad/translations/cs.json | 10 ------- .../components/mullvad/translations/de.json | 6 ----- .../components/mullvad/translations/el.json | 6 ----- .../components/mullvad/translations/en.json | 6 ----- .../components/mullvad/translations/es.json | 6 ----- .../components/mullvad/translations/et.json | 6 ----- .../components/mullvad/translations/fr.json | 6 ----- .../components/mullvad/translations/he.json | 10 ------- .../components/mullvad/translations/hu.json | 10 ------- .../components/mullvad/translations/id.json | 6 ----- .../components/mullvad/translations/it.json | 6 ----- .../components/mullvad/translations/ko.json | 6 ----- .../components/mullvad/translations/nl.json | 6 ----- .../components/mullvad/translations/no.json | 6 ----- .../components/mullvad/translations/pl.json | 6 ----- .../components/mullvad/translations/pt.json | 10 ------- .../components/mullvad/translations/ru.json | 6 ----- .../components/mullvad/translations/sv.json | 8 ------ .../components/mullvad/translations/tr.json | 12 --------- .../mullvad/translations/zh-Hans.json | 9 ------- .../mullvad/translations/zh-Hant.json | 6 ----- .../components/mutesync/translations/de.json | 2 +- .../components/myq/translations/de.json | 4 ++- .../components/nam/translations/de.json | 6 ++++- .../components/neato/translations/bg.json | 11 -------- .../components/neato/translations/ca.json | 14 ---------- .../components/neato/translations/cs.json | 13 --------- .../components/neato/translations/da.json | 11 -------- .../components/neato/translations/de.json | 14 ---------- .../components/neato/translations/el.json | 11 -------- .../components/neato/translations/en.json | 14 ---------- .../components/neato/translations/es-419.json | 11 -------- .../components/neato/translations/es.json | 14 ---------- .../components/neato/translations/et.json | 14 ---------- .../components/neato/translations/fr.json | 14 ---------- .../components/neato/translations/he.json | 11 -------- .../components/neato/translations/hu.json | 14 ---------- .../components/neato/translations/id.json | 14 ---------- .../components/neato/translations/it.json | 14 ---------- .../components/neato/translations/ko.json | 14 ---------- .../components/neato/translations/lb.json | 14 ---------- .../components/neato/translations/lv.json | 13 --------- .../components/neato/translations/nl.json | 14 ---------- .../components/neato/translations/nn.json | 11 -------- .../components/neato/translations/no.json | 14 ---------- .../components/neato/translations/pl.json | 14 ---------- .../components/neato/translations/pt-BR.json | 11 -------- .../components/neato/translations/pt.json | 11 -------- .../components/neato/translations/ru.json | 14 ---------- .../components/neato/translations/sl.json | 9 ------- .../components/neato/translations/sv.json | 11 -------- .../components/neato/translations/tr.json | 12 --------- .../components/neato/translations/uk.json | 14 ---------- .../neato/translations/zh-Hans.json | 12 --------- .../neato/translations/zh-Hant.json | 14 ---------- .../components/nest/translations/bg.json | 1 - .../components/nest/translations/ca.json | 1 - .../components/nest/translations/cs.json | 1 - .../components/nest/translations/da.json | 1 - .../components/nest/translations/de.json | 1 - .../components/nest/translations/en.json | 1 - .../components/nest/translations/es-419.json | 1 - .../components/nest/translations/es.json | 1 - .../components/nest/translations/et.json | 1 - .../components/nest/translations/fi.json | 3 --- .../components/nest/translations/fr.json | 1 - .../components/nest/translations/he.json | 1 - .../components/nest/translations/hu.json | 1 - .../components/nest/translations/id.json | 1 - .../components/nest/translations/it.json | 1 - .../components/nest/translations/ko.json | 1 - .../components/nest/translations/lb.json | 1 - .../components/nest/translations/nl.json | 1 - .../components/nest/translations/nn.json | 1 - .../components/nest/translations/no.json | 1 - .../components/nest/translations/pl.json | 1 - .../components/nest/translations/pt-BR.json | 1 - .../components/nest/translations/pt.json | 1 - .../components/nest/translations/ru.json | 1 - .../components/nest/translations/sl.json | 1 - .../components/nest/translations/sv.json | 1 - .../components/nest/translations/uk.json | 1 - .../components/nest/translations/zh-Hans.json | 1 - .../components/nest/translations/zh-Hant.json | 1 - .../nightscout/translations/en.json | 1 + .../components/nzbget/translations/ca.json | 2 +- .../components/nzbget/translations/et.json | 2 +- .../components/nzbget/translations/no.json | 2 +- .../components/nzbget/translations/ru.json | 2 +- .../nzbget/translations/zh-Hant.json | 2 +- .../ondilo_ico/translations/ca.json | 3 +-- .../ondilo_ico/translations/cs.json | 3 +-- .../ondilo_ico/translations/de.json | 3 +-- .../ondilo_ico/translations/en.json | 3 +-- .../ondilo_ico/translations/es.json | 3 +-- .../ondilo_ico/translations/et.json | 3 +-- .../ondilo_ico/translations/fr.json | 3 +-- .../ondilo_ico/translations/id.json | 3 +-- .../ondilo_ico/translations/it.json | 3 +-- .../ondilo_ico/translations/ko.json | 3 +-- .../ondilo_ico/translations/nl.json | 3 +-- .../ondilo_ico/translations/no.json | 3 +-- .../ondilo_ico/translations/pl.json | 3 +-- .../ondilo_ico/translations/ru.json | 3 +-- .../ondilo_ico/translations/tr.json | 3 +-- .../ondilo_ico/translations/uk.json | 3 +-- .../ondilo_ico/translations/zh-Hant.json | 3 +-- .../opentherm_gw/translations/bg.json | 3 +-- .../opentherm_gw/translations/ca.json | 1 - .../opentherm_gw/translations/cs.json | 3 +-- .../opentherm_gw/translations/da.json | 3 +-- .../opentherm_gw/translations/de.json | 1 - .../opentherm_gw/translations/en.json | 1 - .../opentherm_gw/translations/es-419.json | 1 - .../opentherm_gw/translations/es.json | 1 - .../opentherm_gw/translations/et.json | 1 - .../opentherm_gw/translations/fr.json | 1 - .../opentherm_gw/translations/hu.json | 1 - .../opentherm_gw/translations/id.json | 1 - .../opentherm_gw/translations/it.json | 1 - .../opentherm_gw/translations/ko.json | 1 - .../opentherm_gw/translations/lb.json | 3 +-- .../opentherm_gw/translations/lv.json | 3 +-- .../opentherm_gw/translations/nl.json | 1 - .../opentherm_gw/translations/no.json | 1 - .../opentherm_gw/translations/pl.json | 1 - .../opentherm_gw/translations/pt-BR.json | 11 -------- .../opentherm_gw/translations/pt.json | 9 ------- .../opentherm_gw/translations/ru.json | 1 - .../opentherm_gw/translations/sl.json | 3 +-- .../opentherm_gw/translations/sv.json | 3 +-- .../opentherm_gw/translations/uk.json | 3 +-- .../opentherm_gw/translations/zh-Hant.json | 1 - .../ovo_energy/translations/ca.json | 2 +- .../ovo_energy/translations/et.json | 2 +- .../ovo_energy/translations/no.json | 2 +- .../ovo_energy/translations/ru.json | 2 +- .../ovo_energy/translations/zh-Hant.json | 2 +- .../components/plugwise/translations/ca.json | 2 +- .../components/plugwise/translations/et.json | 2 +- .../components/plugwise/translations/no.json | 2 +- .../components/plugwise/translations/ru.json | 2 +- .../plugwise/translations/zh-Hant.json | 2 +- .../components/point/translations/bg.json | 1 - .../components/point/translations/ca.json | 1 - .../components/point/translations/cs.json | 1 - .../components/point/translations/da.json | 1 - .../components/point/translations/de.json | 1 - .../components/point/translations/en.json | 1 - .../components/point/translations/es-419.json | 1 - .../components/point/translations/es.json | 1 - .../components/point/translations/et.json | 1 - .../components/point/translations/fr.json | 1 - .../components/point/translations/hu.json | 1 - .../components/point/translations/id.json | 1 - .../components/point/translations/it.json | 1 - .../components/point/translations/ko.json | 1 - .../components/point/translations/lb.json | 1 - .../components/point/translations/nl.json | 1 - .../components/point/translations/no.json | 1 - .../components/point/translations/pl.json | 1 - .../components/point/translations/pt-BR.json | 1 - .../components/point/translations/pt.json | 1 - .../components/point/translations/ru.json | 1 - .../components/point/translations/sl.json | 1 - .../components/point/translations/sv.json | 1 - .../components/point/translations/uk.json | 1 - .../point/translations/zh-Hans.json | 1 - .../point/translations/zh-Hant.json | 1 - .../components/powerwall/translations/ca.json | 2 +- .../components/powerwall/translations/et.json | 2 +- .../components/powerwall/translations/no.json | 2 +- .../components/powerwall/translations/ru.json | 2 +- .../powerwall/translations/zh-Hant.json | 2 +- .../rainmachine/translations/ca.json | 2 +- .../rainmachine/translations/et.json | 2 +- .../rainmachine/translations/no.json | 2 +- .../rainmachine/translations/ru.json | 2 +- .../rainmachine/translations/zh-Hant.json | 2 +- .../components/roku/translations/ca.json | 2 +- .../components/roku/translations/en.json | 5 +++- .../components/roku/translations/et.json | 2 +- .../components/roku/translations/no.json | 2 +- .../components/roku/translations/ru.json | 2 +- .../components/roku/translations/zh-Hant.json | 2 +- .../components/roomba/translations/ca.json | 2 +- .../components/roomba/translations/en.json | 11 ++++++++ .../components/roomba/translations/et.json | 2 +- .../components/roomba/translations/no.json | 2 +- .../components/roomba/translations/ru.json | 2 +- .../roomba/translations/zh-Hant.json | 2 +- .../components/roon/translations/ca.json | 1 - .../components/roon/translations/cs.json | 1 - .../components/roon/translations/de.json | 1 - .../components/roon/translations/el.json | 3 --- .../components/roon/translations/en.json | 1 - .../components/roon/translations/es.json | 1 - .../components/roon/translations/et.json | 1 - .../components/roon/translations/fr.json | 1 - .../components/roon/translations/hu.json | 1 - .../components/roon/translations/id.json | 1 - .../components/roon/translations/it.json | 1 - .../components/roon/translations/ko.json | 1 - .../components/roon/translations/lb.json | 1 - .../components/roon/translations/nl.json | 1 - .../components/roon/translations/no.json | 1 - .../components/roon/translations/pl.json | 1 - .../components/roon/translations/pt-BR.json | 1 - .../components/roon/translations/ru.json | 1 - .../components/roon/translations/tr.json | 1 - .../components/roon/translations/uk.json | 1 - .../components/roon/translations/zh-Hant.json | 1 - .../components/samsungtv/translations/ca.json | 2 +- .../components/samsungtv/translations/et.json | 2 +- .../components/samsungtv/translations/no.json | 2 +- .../components/samsungtv/translations/ru.json | 2 +- .../samsungtv/translations/zh-Hant.json | 2 +- .../screenlogic/translations/ca.json | 2 +- .../screenlogic/translations/et.json | 2 +- .../screenlogic/translations/no.json | 2 +- .../screenlogic/translations/ru.json | 2 +- .../screenlogic/translations/zh-Hant.json | 2 +- .../components/sensor/translations/bg.json | 2 -- .../components/sensor/translations/ca.json | 2 -- .../components/sensor/translations/cs.json | 2 -- .../components/sensor/translations/da.json | 2 -- .../components/sensor/translations/de.json | 2 -- .../components/sensor/translations/en.json | 2 -- .../components/sensor/translations/es.json | 2 -- .../components/sensor/translations/et.json | 2 -- .../components/sensor/translations/fr.json | 2 -- .../components/sensor/translations/hu.json | 2 -- .../components/sensor/translations/id.json | 2 -- .../components/sensor/translations/it.json | 2 -- .../components/sensor/translations/ko.json | 2 -- .../components/sensor/translations/lb.json | 2 -- .../components/sensor/translations/nl.json | 2 -- .../components/sensor/translations/no.json | 2 -- .../components/sensor/translations/pl.json | 2 -- .../components/sensor/translations/pt.json | 2 -- .../components/sensor/translations/ru.json | 2 -- .../components/sensor/translations/sl.json | 2 -- .../components/sensor/translations/sv.json | 2 -- .../components/sensor/translations/tr.json | 2 -- .../components/sensor/translations/uk.json | 2 -- .../sensor/translations/zh-Hans.json | 2 -- .../sensor/translations/zh-Hant.json | 2 -- .../components/sma/translations/de.json | 3 ++- .../components/smappee/translations/ca.json | 2 +- .../components/smappee/translations/et.json | 2 +- .../components/smappee/translations/no.json | 2 +- .../components/smappee/translations/ru.json | 2 +- .../smappee/translations/zh-Hant.json | 2 +- .../components/smarttub/translations/bg.json | 3 --- .../components/smarttub/translations/ca.json | 3 +-- .../components/smarttub/translations/cs.json | 3 +-- .../components/smarttub/translations/de.json | 5 ++-- .../components/smarttub/translations/en.json | 3 +-- .../components/smarttub/translations/es.json | 3 +-- .../components/smarttub/translations/et.json | 3 +-- .../components/smarttub/translations/fr.json | 3 +-- .../components/smarttub/translations/hu.json | 3 +-- .../components/smarttub/translations/id.json | 3 +-- .../components/smarttub/translations/it.json | 3 +-- .../components/smarttub/translations/ko.json | 3 +-- .../components/smarttub/translations/nl.json | 3 +-- .../components/smarttub/translations/no.json | 3 +-- .../components/smarttub/translations/pl.json | 3 +-- .../components/smarttub/translations/pt.json | 3 +-- .../components/smarttub/translations/ru.json | 3 +-- .../smarttub/translations/zh-Hant.json | 3 +-- .../components/solaredge/translations/bg.json | 6 ----- .../components/solaredge/translations/ca.json | 4 +-- .../components/solaredge/translations/cs.json | 4 +-- .../components/solaredge/translations/da.json | 6 ----- .../components/solaredge/translations/de.json | 4 +-- .../components/solaredge/translations/en.json | 4 +-- .../solaredge/translations/es-419.json | 6 ----- .../components/solaredge/translations/es.json | 4 +-- .../components/solaredge/translations/et.json | 4 +-- .../components/solaredge/translations/fr.json | 4 +-- .../components/solaredge/translations/id.json | 4 +-- .../components/solaredge/translations/it.json | 4 +-- .../components/solaredge/translations/ko.json | 4 +-- .../components/solaredge/translations/lb.json | 4 +-- .../components/solaredge/translations/nl.json | 4 +-- .../components/solaredge/translations/no.json | 4 +-- .../components/solaredge/translations/pl.json | 4 +-- .../components/solaredge/translations/ru.json | 4 +-- .../components/solaredge/translations/sl.json | 4 +-- .../components/solaredge/translations/sv.json | 6 ----- .../components/solaredge/translations/uk.json | 4 +-- .../solaredge/translations/zh-Hant.json | 4 +-- .../somfy_mylink/translations/ca.json | 2 +- .../somfy_mylink/translations/en.json | 12 ++++++++- .../somfy_mylink/translations/et.json | 2 +- .../somfy_mylink/translations/no.json | 2 +- .../somfy_mylink/translations/ru.json | 2 +- .../somfy_mylink/translations/zh-Hant.json | 2 +- .../components/sonarr/translations/ca.json | 2 +- .../components/sonarr/translations/et.json | 2 +- .../components/sonarr/translations/no.json | 2 +- .../components/sonarr/translations/ru.json | 2 +- .../sonarr/translations/zh-Hant.json | 2 +- .../components/songpal/translations/ca.json | 2 +- .../components/songpal/translations/et.json | 2 +- .../components/songpal/translations/no.json | 2 +- .../components/songpal/translations/ru.json | 2 +- .../songpal/translations/zh-Hant.json | 2 +- .../squeezebox/translations/ca.json | 2 +- .../squeezebox/translations/et.json | 2 +- .../squeezebox/translations/no.json | 2 +- .../squeezebox/translations/ru.json | 2 +- .../squeezebox/translations/zh-Hant.json | 2 +- .../components/subaru/translations/bg.json | 3 --- .../components/subaru/translations/ca.json | 3 +-- .../components/subaru/translations/cs.json | 3 +-- .../components/subaru/translations/de.json | 3 +-- .../components/subaru/translations/el.json | 3 --- .../components/subaru/translations/en.json | 3 +-- .../components/subaru/translations/es.json | 3 +-- .../components/subaru/translations/et.json | 3 +-- .../components/subaru/translations/fr.json | 3 +-- .../components/subaru/translations/hu.json | 3 +-- .../components/subaru/translations/id.json | 3 +-- .../components/subaru/translations/it.json | 3 +-- .../components/subaru/translations/ko.json | 3 +-- .../components/subaru/translations/nl.json | 3 +-- .../components/subaru/translations/no.json | 3 +-- .../components/subaru/translations/pl.json | 3 +-- .../components/subaru/translations/pt.json | 3 +-- .../components/subaru/translations/ru.json | 3 +-- .../subaru/translations/zh-Hant.json | 3 +-- .../components/syncthing/translations/de.json | 1 + .../components/syncthru/translations/ca.json | 2 +- .../components/syncthru/translations/et.json | 2 +- .../components/syncthru/translations/no.json | 2 +- .../components/syncthru/translations/ru.json | 2 +- .../syncthru/translations/zh-Hant.json | 2 +- .../synology_dsm/translations/ca.json | 2 +- .../synology_dsm/translations/et.json | 2 +- .../synology_dsm/translations/no.json | 2 +- .../synology_dsm/translations/ru.json | 2 +- .../synology_dsm/translations/zh-Hant.json | 2 +- .../system_bridge/translations/ca.json | 2 +- .../system_bridge/translations/de.json | 6 +++-- .../system_bridge/translations/en.json | 3 ++- .../system_bridge/translations/et.json | 2 +- .../system_bridge/translations/no.json | 2 +- .../system_bridge/translations/ru.json | 2 +- .../system_bridge/translations/zh-Hant.json | 2 +- .../components/tado/translations/de.json | 2 +- .../tellduslive/translations/bg.json | 1 - .../tellduslive/translations/ca.json | 1 - .../tellduslive/translations/cs.json | 1 - .../tellduslive/translations/da.json | 1 - .../tellduslive/translations/de.json | 1 - .../tellduslive/translations/en.json | 1 - .../tellduslive/translations/es-419.json | 1 - .../tellduslive/translations/es.json | 1 - .../tellduslive/translations/et.json | 1 - .../tellduslive/translations/fr.json | 1 - .../tellduslive/translations/hu.json | 1 - .../tellduslive/translations/id.json | 1 - .../tellduslive/translations/it.json | 1 - .../tellduslive/translations/ko.json | 1 - .../tellduslive/translations/lb.json | 1 - .../tellduslive/translations/nl.json | 1 - .../tellduslive/translations/no.json | 1 - .../tellduslive/translations/pl.json | 1 - .../tellduslive/translations/pt-BR.json | 1 - .../tellduslive/translations/pt.json | 1 - .../tellduslive/translations/ru.json | 1 - .../tellduslive/translations/sl.json | 1 - .../tellduslive/translations/sv.json | 1 - .../tellduslive/translations/uk.json | 1 - .../tellduslive/translations/zh-Hans.json | 1 - .../tellduslive/translations/zh-Hant.json | 1 - .../components/toon/translations/ca.json | 1 - .../components/toon/translations/cs.json | 1 - .../components/toon/translations/de.json | 1 - .../components/toon/translations/en.json | 1 - .../components/toon/translations/es.json | 1 - .../components/toon/translations/et.json | 1 - .../components/toon/translations/fr.json | 1 - .../components/toon/translations/id.json | 1 - .../components/toon/translations/it.json | 1 - .../components/toon/translations/ko.json | 1 - .../components/toon/translations/lb.json | 1 - .../components/toon/translations/nl.json | 1 - .../components/toon/translations/no.json | 1 - .../components/toon/translations/pl.json | 1 - .../components/toon/translations/pt.json | 1 - .../components/toon/translations/ru.json | 1 - .../components/toon/translations/uk.json | 1 - .../components/toon/translations/zh-Hant.json | 1 - .../components/tuya/translations/en.json | 1 + .../components/unifi/translations/ca.json | 2 +- .../components/unifi/translations/et.json | 2 +- .../components/unifi/translations/no.json | 2 +- .../components/unifi/translations/ru.json | 2 +- .../unifi/translations/zh-Hant.json | 2 +- .../components/upnp/translations/ca.json | 2 +- .../components/upnp/translations/en.json | 1 - .../components/upnp/translations/et.json | 2 +- .../components/upnp/translations/no.json | 2 +- .../components/upnp/translations/ru.json | 2 +- .../components/upnp/translations/zh-Hant.json | 2 +- .../components/wilight/translations/ca.json | 2 +- .../components/wilight/translations/et.json | 2 +- .../components/wilight/translations/no.json | 2 +- .../components/wilight/translations/ru.json | 2 +- .../wilight/translations/zh-Hant.json | 2 +- .../components/withings/translations/ca.json | 2 +- .../components/withings/translations/et.json | 2 +- .../components/withings/translations/no.json | 2 +- .../components/withings/translations/ru.json | 2 +- .../withings/translations/zh-Hant.json | 2 +- .../components/wled/translations/ca.json | 2 +- .../components/wled/translations/et.json | 2 +- .../components/wled/translations/no.json | 2 +- .../components/wled/translations/ru.json | 2 +- .../components/wled/translations/zh-Hant.json | 2 +- .../xiaomi_aqara/translations/ca.json | 2 +- .../xiaomi_aqara/translations/et.json | 2 +- .../xiaomi_aqara/translations/no.json | 2 +- .../xiaomi_aqara/translations/ru.json | 2 +- .../xiaomi_aqara/translations/zh-Hant.json | 2 +- .../xiaomi_miio/translations/ca.json | 2 +- .../xiaomi_miio/translations/en.json | 18 +++++++++++++ .../xiaomi_miio/translations/et.json | 2 +- .../xiaomi_miio/translations/no.json | 2 +- .../xiaomi_miio/translations/ru.json | 2 +- .../xiaomi_miio/translations/zh-Hant.json | 2 +- .../components/zha/translations/ca.json | 2 +- .../components/zha/translations/et.json | 2 +- .../components/zha/translations/no.json | 2 +- .../components/zha/translations/ru.json | 2 +- .../components/zha/translations/zh-Hant.json | 2 +- .../components/zwave/translations/bg.json | 3 +-- .../components/zwave/translations/ca.json | 3 +-- .../components/zwave/translations/cs.json | 3 +-- .../components/zwave/translations/da.json | 3 +-- .../components/zwave/translations/de.json | 3 +-- .../components/zwave/translations/en.json | 3 +-- .../components/zwave/translations/es-419.json | 3 +-- .../components/zwave/translations/es.json | 3 +-- .../components/zwave/translations/et.json | 3 +-- .../components/zwave/translations/fi.json | 3 +-- .../components/zwave/translations/fr.json | 3 +-- .../components/zwave/translations/hu.json | 3 +-- .../components/zwave/translations/id.json | 3 +-- .../components/zwave/translations/it.json | 3 +-- .../components/zwave/translations/ko.json | 3 +-- .../components/zwave/translations/lb.json | 3 +-- .../components/zwave/translations/nl.json | 3 +-- .../components/zwave/translations/no.json | 3 +-- .../components/zwave/translations/pl.json | 3 +-- .../components/zwave/translations/pt-BR.json | 3 +-- .../components/zwave/translations/pt.json | 3 +-- .../components/zwave/translations/ro.json | 3 +-- .../components/zwave/translations/ru.json | 3 +-- .../components/zwave/translations/sl.json | 3 +-- .../components/zwave/translations/sv.json | 3 +-- .../components/zwave/translations/uk.json | 3 +-- .../zwave/translations/zh-Hans.json | 3 +-- .../zwave/translations/zh-Hant.json | 3 +-- .../components/zwave_js/translations/bg.json | 5 ---- .../components/zwave_js/translations/ca.json | 6 ----- .../components/zwave_js/translations/cs.json | 5 ---- .../components/zwave_js/translations/de.json | 6 ----- .../components/zwave_js/translations/en.json | 6 ----- .../components/zwave_js/translations/es.json | 6 ----- .../components/zwave_js/translations/et.json | 6 ----- .../components/zwave_js/translations/fr.json | 6 ----- .../components/zwave_js/translations/hu.json | 5 ---- .../components/zwave_js/translations/id.json | 6 ----- .../components/zwave_js/translations/it.json | 6 ----- .../components/zwave_js/translations/ko.json | 6 ----- .../components/zwave_js/translations/lb.json | 7 ----- .../components/zwave_js/translations/nl.json | 6 ----- .../components/zwave_js/translations/no.json | 6 ----- .../components/zwave_js/translations/pl.json | 6 ----- .../components/zwave_js/translations/ru.json | 6 ----- .../components/zwave_js/translations/tr.json | 6 ----- .../components/zwave_js/translations/uk.json | 7 ----- .../zwave_js/translations/zh-Hant.json | 6 ----- 1030 files changed, 790 insertions(+), 3241 deletions(-) delete mode 100644 homeassistant/components/airvisual/translations/vi.json delete mode 100644 homeassistant/components/atag/translations/he.json delete mode 100644 homeassistant/components/august/translations/he.json delete mode 100644 homeassistant/components/august/translations/lv.json delete mode 100644 homeassistant/components/august/translations/zh-Hans.json create mode 100644 homeassistant/components/growatt_server/translations/ca.json create mode 100644 homeassistant/components/growatt_server/translations/de.json create mode 100644 homeassistant/components/growatt_server/translations/et.json create mode 100644 homeassistant/components/growatt_server/translations/nl.json create mode 100644 homeassistant/components/growatt_server/translations/no.json create mode 100644 homeassistant/components/growatt_server/translations/ru.json create mode 100644 homeassistant/components/growatt_server/translations/zh-Hant.json delete mode 100644 homeassistant/components/hassio/translations/af.json delete mode 100644 homeassistant/components/hassio/translations/bg.json delete mode 100644 homeassistant/components/hassio/translations/cy.json delete mode 100644 homeassistant/components/hassio/translations/da.json delete mode 100644 homeassistant/components/hassio/translations/el.json delete mode 100644 homeassistant/components/hassio/translations/es-419.json delete mode 100644 homeassistant/components/hassio/translations/eu.json delete mode 100644 homeassistant/components/hassio/translations/fa.json delete mode 100644 homeassistant/components/hassio/translations/fi.json delete mode 100644 homeassistant/components/hassio/translations/he.json delete mode 100644 homeassistant/components/hassio/translations/hr.json delete mode 100644 homeassistant/components/hassio/translations/hy.json delete mode 100644 homeassistant/components/hassio/translations/is.json delete mode 100644 homeassistant/components/hassio/translations/ja.json delete mode 100644 homeassistant/components/hassio/translations/lt.json delete mode 100644 homeassistant/components/hassio/translations/lv.json delete mode 100644 homeassistant/components/hassio/translations/nn.json delete mode 100644 homeassistant/components/hassio/translations/pt-BR.json delete mode 100644 homeassistant/components/hassio/translations/ro.json delete mode 100644 homeassistant/components/hassio/translations/sk.json delete mode 100644 homeassistant/components/hassio/translations/sv.json delete mode 100644 homeassistant/components/hassio/translations/th.json delete mode 100644 homeassistant/components/hassio/translations/vi.json delete mode 100644 homeassistant/components/mullvad/translations/tr.json delete mode 100644 homeassistant/components/neato/translations/el.json delete mode 100644 homeassistant/components/neato/translations/he.json delete mode 100644 homeassistant/components/neato/translations/lv.json delete mode 100644 homeassistant/components/neato/translations/nn.json delete mode 100644 homeassistant/components/neato/translations/pt-BR.json delete mode 100644 homeassistant/components/neato/translations/zh-Hans.json delete mode 100644 homeassistant/components/opentherm_gw/translations/pt-BR.json diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json index 82c658e7c15..97d8547861f 100644 --- a/homeassistant/components/adguard/translations/bg.json +++ b/homeassistant/components/adguard/translations/bg.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", - "single_instance_allowed": "\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 AdGuard Home." + "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/adguard/translations/ca.json b/homeassistant/components/adguard/translations/ca.json index 82897df6b2a..300c843b57e 100644 --- a/homeassistant/components/adguard/translations/ca.json +++ b/homeassistant/components/adguard/translations/ca.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "El servei ja est\u00e0 configurat", - "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" diff --git a/homeassistant/components/adguard/translations/cs.json b/homeassistant/components/adguard/translations/cs.json index b56ed228b4d..f82589900d4 100644 --- a/homeassistant/components/adguard/translations/cs.json +++ b/homeassistant/components/adguard/translations/cs.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Slu\u017eba je ji\u017e nastavena", - "existing_instance_updated": "St\u00e1vaj\u00edc\u00ed nastaven\u00ed aktualizov\u00e1no.", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + "existing_instance_updated": "St\u00e1vaj\u00edc\u00ed nastaven\u00ed aktualizov\u00e1no." }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" diff --git a/homeassistant/components/adguard/translations/da.json b/homeassistant/components/adguard/translations/da.json index 79a1937eba8..8bb4c26eed6 100644 --- a/homeassistant/components/adguard/translations/da.json +++ b/homeassistant/components/adguard/translations/da.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "existing_instance_updated": "Opdaterede eksisterende konfiguration.", - "single_instance_allowed": "Kun en enkelt konfiguration af AdGuard Home er tilladt." + "existing_instance_updated": "Opdaterede eksisterende konfiguration." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json index 0819dc1c0a5..f73c25d769e 100644 --- a/homeassistant/components/adguard/translations/de.json +++ b/homeassistant/components/adguard/translations/de.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Der Dienst ist bereits konfiguriert", - "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" diff --git a/homeassistant/components/adguard/translations/en.json b/homeassistant/components/adguard/translations/en.json index 31eb1ff06a3..f354aaab10a 100644 --- a/homeassistant/components/adguard/translations/en.json +++ b/homeassistant/components/adguard/translations/en.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Service is already configured", - "existing_instance_updated": "Updated existing configuration.", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "existing_instance_updated": "Updated existing configuration." }, "error": { "cannot_connect": "Failed to connect" diff --git a/homeassistant/components/adguard/translations/es-419.json b/homeassistant/components/adguard/translations/es-419.json index 8fac53b61ab..6a734ffea9a 100644 --- a/homeassistant/components/adguard/translations/es-419.json +++ b/homeassistant/components/adguard/translations/es-419.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente.", - "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." + "existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index fa12995ea59..5750808ab76 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", - "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." + "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/adguard/translations/et.json b/homeassistant/components/adguard/translations/et.json index 1e53492510b..fc1d043994e 100644 --- a/homeassistant/components/adguard/translations/et.json +++ b/homeassistant/components/adguard/translations/et.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Teenus on juba seadistatud", - "existing_instance_updated": "Olemasolevad seaded v\u00e4rskendatud.", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + "existing_instance_updated": "Olemasolevad seaded v\u00e4rskendatud." }, "error": { "cannot_connect": "\u00dchendamine nurjus" diff --git a/homeassistant/components/adguard/translations/fr.json b/homeassistant/components/adguard/translations/fr.json index f97eb7a0df1..7add7c9829f 100644 --- a/homeassistant/components/adguard/translations/fr.json +++ b/homeassistant/components/adguard/translations/fr.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", - "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.", - "single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e." + "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." }, "error": { "cannot_connect": "\u00c9chec de connexion" diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index 3813fae8f3c..251b72574ee 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." - }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, diff --git a/homeassistant/components/adguard/translations/id.json b/homeassistant/components/adguard/translations/id.json index d2e36cfe5b9..d787fd5620d 100644 --- a/homeassistant/components/adguard/translations/id.json +++ b/homeassistant/components/adguard/translations/id.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "existing_instance_updated": "Memperbarui konfigurasi yang ada.", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + "existing_instance_updated": "Memperbarui konfigurasi yang ada." }, "error": { "cannot_connect": "Gagal terhubung" diff --git a/homeassistant/components/adguard/translations/it.json b/homeassistant/components/adguard/translations/it.json index 9383de7b853..5f8bc33997d 100644 --- a/homeassistant/components/adguard/translations/it.json +++ b/homeassistant/components/adguard/translations/it.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", - "existing_instance_updated": "Configurazione esistente aggiornata.", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + "existing_instance_updated": "Configurazione esistente aggiornata." }, "error": { "cannot_connect": "Impossibile connettersi" diff --git a/homeassistant/components/adguard/translations/ko.json b/homeassistant/components/adguard/translations/ko.json index fa5b3254ad4..63d672a2fff 100644 --- a/homeassistant/components/adguard/translations/ko.json +++ b/homeassistant/components/adguard/translations/ko.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/adguard/translations/lb.json b/homeassistant/components/adguard/translations/lb.json index ae7e6ad99be..f1bd1876dc7 100644 --- a/homeassistant/components/adguard/translations/lb.json +++ b/homeassistant/components/adguard/translations/lb.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert.", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + "existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert." }, "error": { "cannot_connect": "Feeler beim verbannen" diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json index 3ad3fe741da..9f991cbd407 100644 --- a/homeassistant/components/adguard/translations/nl.json +++ b/homeassistant/components/adguard/translations/nl.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "existing_instance_updated": "Bestaande configuratie bijgewerkt.", - "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan." + "existing_instance_updated": "Bestaande configuratie bijgewerkt." }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index 442c5a9e6b4..fc95d3bde66 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + "existing_instance_updated": "Oppdatert eksisterende konfigurasjon." }, "error": { "cannot_connect": "Tilkobling mislyktes" diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json index c194afb63da..7ea17b246bc 100644 --- a/homeassistant/components/adguard/translations/pl.json +++ b/homeassistant/components/adguard/translations/pl.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", - "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" diff --git a/homeassistant/components/adguard/translations/pt-BR.json b/homeassistant/components/adguard/translations/pt-BR.json index 959c7ba3638..5d291f4cadb 100644 --- a/homeassistant/components/adguard/translations/pt-BR.json +++ b/homeassistant/components/adguard/translations/pt-BR.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada.", - "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do AdGuard Home \u00e9 permitida." + "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index a7e494936b8..df9b6c03bc5 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index b2eb34f061f..b1bb7d3ccf7 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", - "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\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." diff --git a/homeassistant/components/adguard/translations/sl.json b/homeassistant/components/adguard/translations/sl.json index 34b03263ceb..f878a2cc206 100644 --- a/homeassistant/components/adguard/translations/sl.json +++ b/homeassistant/components/adguard/translations/sl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.", - "single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home." + "existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/adguard/translations/sv.json b/homeassistant/components/adguard/translations/sv.json index ca6158eaf32..0b58d9dcc97 100644 --- a/homeassistant/components/adguard/translations/sv.json +++ b/homeassistant/components/adguard/translations/sv.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "existing_instance_updated": "Uppdaterade existerande konfiguration.", - "single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten." + "existing_instance_updated": "Uppdaterade existerande konfiguration." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/adguard/translations/tr.json b/homeassistant/components/adguard/translations/tr.json index 26bef46408a..065af7b49cf 100644 --- a/homeassistant/components/adguard/translations/tr.json +++ b/homeassistant/components/adguard/translations/tr.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." - }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, diff --git a/homeassistant/components/adguard/translations/uk.json b/homeassistant/components/adguard/translations/uk.json index 28d02f25b7e..34a336364a0 100644 --- a/homeassistant/components/adguard/translations/uk.json +++ b/homeassistant/components/adguard/translations/uk.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json index eeec0d6b17c..7db4bbcea83 100644 --- a/homeassistant/components/adguard/translations/zh-Hant.json +++ b/homeassistant/components/adguard/translations/zh-Hant.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/airvisual/translations/ca.json b/homeassistant/components/airvisual/translations/ca.json index 29df3dc7ca2..0440189cdb9 100644 --- a/homeassistant/components/airvisual/translations/ca.json +++ b/homeassistant/components/airvisual/translations/ca.json @@ -11,15 +11,6 @@ "location_not_found": "No s'ha trobat la ubicaci\u00f3" }, "step": { - "geography": { - "data": { - "api_key": "[%key::common::config_flow::data::api_key%]", - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "Utilitza l'API d'AirVisual per monitoritzar una ubicaci\u00f3 geogr\u00e0fica.", - "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" - }, "geography_by_coords": { "data": { "api_key": "Clau API", @@ -54,11 +45,6 @@ "title": "Re-autenticaci\u00f3 amb AirVisual" }, "user": { - "data": { - "cloud_api": "Ubicaci\u00f3 geogr\u00e0fica", - "node_pro": "AirVisual Node Pro", - "type": "Tipus d'integraci\u00f3" - }, "description": "Tria quin tipus de dades d'AirVisual vols monitoritzar.", "title": "Configura AirVisual" } diff --git a/homeassistant/components/airvisual/translations/cs.json b/homeassistant/components/airvisual/translations/cs.json index 75720b0f30b..4fd193e6ddc 100644 --- a/homeassistant/components/airvisual/translations/cs.json +++ b/homeassistant/components/airvisual/translations/cs.json @@ -10,13 +10,6 @@ "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" }, "step": { - "geography": { - "data": { - "api_key": "Kl\u00ed\u010d API", - "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", - "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" - } - }, "geography_by_coords": { "data": { "api_key": "Kl\u00ed\u010d API", @@ -45,11 +38,6 @@ "title": "Znovu ov\u011b\u0159it AirVisual" }, "user": { - "data": { - "cloud_api": "Geografick\u00e1 poloha", - "node_pro": "AirVisual Node Pro", - "type": "Typ integrace" - }, "description": "Vyberte, jak\u00fd typ dat AirVisual chcete sledovat.", "title": "Nastaven\u00ed AirVisual" } diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index 6e2a5f60c6f..588d69f96fe 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -11,15 +11,6 @@ "location_not_found": "Standort nicht gefunden" }, "step": { - "geography": { - "data": { - "api_key": "API-Schl\u00fcssel", - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad" - }, - "description": "Verwende die AirVisual Cloud API, um einen geografischen Standort zu \u00fcberwachen.", - "title": "Konfigurieren Sie eine Geografie" - }, "geography_by_coords": { "data": { "api_key": "API-Schl\u00fcssel", @@ -54,11 +45,6 @@ "title": "AirVisual erneut authentifizieren" }, "user": { - "data": { - "cloud_api": "Geografische Position", - "node_pro": "AirVisual Node Pro", - "type": "Integrationstyp" - }, "description": "W\u00e4hlen Sie aus, welche Art von AirVisual-Daten Sie \u00fcberwachen m\u00f6chten.", "title": "Konfigurieren Sie AirVisual" } diff --git a/homeassistant/components/airvisual/translations/en.json b/homeassistant/components/airvisual/translations/en.json index 64eb11f902c..1e3cb59a520 100644 --- a/homeassistant/components/airvisual/translations/en.json +++ b/homeassistant/components/airvisual/translations/en.json @@ -11,15 +11,6 @@ "location_not_found": "Location not found" }, "step": { - "geography": { - "data": { - "api_key": "API Key", - "latitude": "Latitude", - "longitude": "Longitude" - }, - "description": "Use the AirVisual cloud API to monitor a geographical location.", - "title": "Configure a Geography" - }, "geography_by_coords": { "data": { "api_key": "API Key", @@ -54,11 +45,6 @@ "title": "Re-authenticate AirVisual" }, "user": { - "data": { - "cloud_api": "Geographical Location", - "node_pro": "AirVisual Node Pro", - "type": "Integration Type" - }, "description": "Pick what type of AirVisual data you want to monitor.", "title": "Configure AirVisual" } diff --git a/homeassistant/components/airvisual/translations/es-419.json b/homeassistant/components/airvisual/translations/es-419.json index 0cc07d27f17..b0022391e62 100644 --- a/homeassistant/components/airvisual/translations/es-419.json +++ b/homeassistant/components/airvisual/translations/es-419.json @@ -9,15 +9,6 @@ "location_not_found": "Ubicaci\u00f3n no encontrada" }, "step": { - "geography": { - "data": { - "api_key": "Clave API", - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "Use la API de AirVisual para monitorear una ubicaci\u00f3n geogr\u00e1fica.", - "title": "Configurar una geograf\u00eda" - }, "geography_by_coords": { "description": "Utilice la API en la nube de AirVisual para monitorear una latitud / longitud.", "title": "Configurar una geograf\u00eda" @@ -31,11 +22,6 @@ "title": "Configurar un AirVisual Node/Pro" }, "user": { - "data": { - "cloud_api": "Localizaci\u00f3n geogr\u00e1fica", - "node_pro": "AirVisual Node Pro", - "type": "Tipo de integraci\u00f3n" - }, "description": "Monitoree la calidad del aire en una ubicaci\u00f3n geogr\u00e1fica.", "title": "Configurar AirVisual" } diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 53768f679be..6e7ea6e6903 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -11,15 +11,6 @@ "location_not_found": "Ubicaci\u00f3n no encontrada" }, "step": { - "geography": { - "data": { - "api_key": "Clave API", - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "Utilizar la API en la nube de AirVisual para monitorizar una ubicaci\u00f3n geogr\u00e1fica.", - "title": "Configurar una Geograf\u00eda" - }, "geography_by_coords": { "data": { "api_key": "Clave API", @@ -54,11 +45,6 @@ "title": "Volver a autenticar AirVisual" }, "user": { - "data": { - "cloud_api": "Ubicaci\u00f3n Geogr\u00e1fica", - "node_pro": "AirVisual Node Pro", - "type": "Tipo de Integraci\u00f3n" - }, "description": "Elige qu\u00e9 tipo de datos de AirVisual quieres monitorizar.", "title": "Configurar AirVisual" } diff --git a/homeassistant/components/airvisual/translations/et.json b/homeassistant/components/airvisual/translations/et.json index 0fae2bcc57b..45490bb63fe 100644 --- a/homeassistant/components/airvisual/translations/et.json +++ b/homeassistant/components/airvisual/translations/et.json @@ -11,15 +11,6 @@ "location_not_found": "Asukohta ei leitud" }, "step": { - "geography": { - "data": { - "api_key": "API v\u00f5ti", - "latitude": "Laiuskraad", - "longitude": "Pikkuskraad" - }, - "description": "Kasuta AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.", - "title": "Seadista Geography" - }, "geography_by_coords": { "data": { "api_key": "API v\u00f5ti", @@ -54,11 +45,6 @@ "title": "Taastuvasta AirVisual" }, "user": { - "data": { - "cloud_api": "Geograafiline asukoht", - "node_pro": "", - "type": "Sidumise t\u00fc\u00fcp" - }, "description": "Vali millist t\u00fc\u00fcpi AirVisuali andmeid soovid j\u00e4lgida.", "title": "Seadista AirVisual" } diff --git a/homeassistant/components/airvisual/translations/fi.json b/homeassistant/components/airvisual/translations/fi.json index b193b59de26..044d7688551 100644 --- a/homeassistant/components/airvisual/translations/fi.json +++ b/homeassistant/components/airvisual/translations/fi.json @@ -4,24 +4,10 @@ "general_error": "Tapahtui tuntematon virhe." }, "step": { - "geography": { - "data": { - "api_key": "API-avain", - "latitude": "Leveysaste", - "longitude": "Pituusaste" - } - }, "node_pro": { "data": { "password": "Salasana" } - }, - "user": { - "data": { - "cloud_api": "Maantieteellinen sijainti", - "node_pro": "AirVisual Node Pro", - "type": "Integrointityyppi" - } } } } diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index 62c144e075d..510bf8597d1 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -11,15 +11,6 @@ "location_not_found": "Emplacement introuvable" }, "step": { - "geography": { - "data": { - "api_key": "Cl\u00e9 API", - "latitude": "Latitude", - "longitude": "Longitude" - }, - "description": "Utilisez l'API cloud AirVisual pour surveiller une position g\u00e9ographique.", - "title": "Configurer une g\u00e9ographie" - }, "geography_by_coords": { "data": { "api_key": "Clef d'API", @@ -54,11 +45,6 @@ "title": "R\u00e9-authentifier AirVisual" }, "user": { - "data": { - "cloud_api": "Localisation g\u00e9ographique", - "node_pro": "AirVisual Node Pro", - "type": "Type d'int\u00e9gration" - }, "description": "Choisissez le type de donn\u00e9es AirVisual que vous souhaitez surveiller.", "title": "Configurer AirVisual" } diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json index e32efda96dc..7fc0c2983df 100644 --- a/homeassistant/components/airvisual/translations/he.json +++ b/homeassistant/components/airvisual/translations/he.json @@ -4,12 +4,6 @@ "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05e1\u05d5\u05e4\u05e7" }, "step": { - "geography": { - "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API", - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" - } - }, "node_pro": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" diff --git a/homeassistant/components/airvisual/translations/hi.json b/homeassistant/components/airvisual/translations/hi.json index bb21909ede0..ee03f27ccc0 100644 --- a/homeassistant/components/airvisual/translations/hi.json +++ b/homeassistant/components/airvisual/translations/hi.json @@ -4,14 +4,6 @@ "general_error": "\u0915\u094b\u0908 \u0905\u091c\u094d\u091e\u093e\u0924 \u0924\u094d\u0930\u0941\u091f\u093f \u0925\u0940\u0964" }, "step": { - "geography": { - "data": { - "latitude": "\u0905\u0915\u094d\u0937\u093e\u0902\u0936", - "longitude": "\u0926\u0947\u0936\u093e\u0928\u094d\u0924\u0930" - }, - "description": "\u092d\u094c\u0917\u094b\u0932\u093f\u0915 \u0938\u094d\u0925\u093f\u0924\u093f \u0915\u0940 \u0928\u093f\u0917\u0930\u093e\u0928\u0940 \u0915\u0947 \u0932\u093f\u090f \u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0915\u094d\u0932\u093e\u0909\u0921 \u090f\u092a\u0940\u0906\u0908 \u0915\u093e \u0909\u092a\u092f\u094b\u0917 \u0915\u0930\u0947\u0902\u0964", - "title": "\u092d\u0942\u0917\u094b\u0932 \u0915\u0949\u0928\u094d\u092b\u093c\u093f\u0917\u0930 \u0915\u0930\u0947\u0902" - }, "node_pro": { "data": { "ip_address": "\u0907\u0915\u093e\u0908 \u0915\u0947 \u0906\u0908\u092a\u0940 \u092a\u0924\u0947/\u0939\u094b\u0938\u094d\u091f\u0928\u093e\u092e", @@ -19,13 +11,6 @@ }, "description": "\u090f\u0915 \u0935\u094d\u092f\u0915\u094d\u0924\u093f\u0917\u0924 \u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0907\u0915\u093e\u0908 \u0915\u0940 \u0928\u093f\u0917\u0930\u093e\u0928\u0940 \u0915\u0930\u0947\u0902\u0964 \u092a\u093e\u0938\u0935\u0930\u094d\u0921 \u092f\u0942\u0928\u093f\u091f \u0915\u0947 \u092f\u0942\u0906\u0908 \u0938\u0947 \u092a\u094d\u0930\u093e\u092a\u094d\u0924 \u0915\u093f\u092f\u093e \u091c\u093e \u0938\u0915\u0924\u093e \u0939\u0948\u0964", "title": "\u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0928\u094b\u0921 \u092a\u094d\u0930\u094b" - }, - "user": { - "data": { - "cloud_api": "\u092d\u094c\u0917\u094b\u0932\u093f\u0915 \u0938\u094d\u0925\u093f\u0924\u093f", - "node_pro": "\u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0928\u094b\u0921 \u092a\u094d\u0930\u094b", - "type": "\u090f\u0915\u0940\u0915\u0930\u0923 \u092a\u094d\u0930\u0915\u093e\u0930" - } } } } diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index 89584cd128b..704ce33ab67 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -10,13 +10,6 @@ "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" }, "step": { - "geography": { - "data": { - "api_key": "API kulcs", - "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g" - } - }, "geography_by_coords": { "data": { "api_key": "API kulcs", @@ -41,11 +34,6 @@ "data": { "api_key": "API kulcs" } - }, - "user": { - "data": { - "type": "Integr\u00e1ci\u00f3 t\u00edpusa" - } } } } diff --git a/homeassistant/components/airvisual/translations/id.json b/homeassistant/components/airvisual/translations/id.json index 3c689338d9f..6fcd6eb5410 100644 --- a/homeassistant/components/airvisual/translations/id.json +++ b/homeassistant/components/airvisual/translations/id.json @@ -11,15 +11,6 @@ "location_not_found": "Lokasi tidak ditemukan" }, "step": { - "geography": { - "data": { - "api_key": "Kunci API", - "latitude": "Lintang", - "longitude": "Bujur" - }, - "description": "Gunakan API cloud AirVisual untuk memantau lokasi geografis.", - "title": "Konfigurasikan Lokasi Geografi" - }, "geography_by_coords": { "data": { "api_key": "Kunci API", @@ -54,11 +45,6 @@ "title": "Autentikasi Ulang AirVisual" }, "user": { - "data": { - "cloud_api": "Lokasi Geografis", - "node_pro": "AirVisual Node Pro", - "type": "Jenis Integrasi" - }, "description": "Pilih jenis data AirVisual yang ingin dipantau.", "title": "Konfigurasikan AirVisual" } diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json index 3ce45ff1342..6201c6f19d9 100644 --- a/homeassistant/components/airvisual/translations/it.json +++ b/homeassistant/components/airvisual/translations/it.json @@ -11,15 +11,6 @@ "location_not_found": "Posizione non trovata" }, "step": { - "geography": { - "data": { - "api_key": "Chiave API", - "latitude": "Latitudine", - "longitude": "Logitudine" - }, - "description": "Utilizzare l'API di AirVisual cloud per monitorare una posizione geografica.", - "title": "Configurare una Geografia" - }, "geography_by_coords": { "data": { "api_key": "Chiave API", @@ -54,11 +45,6 @@ "title": "Riautenticare AirVisual" }, "user": { - "data": { - "cloud_api": "Posizione geografica", - "node_pro": "AirVisual Node Pro", - "type": "Tipo di integrazione" - }, "description": "Scegliere il tipo di dati AirVisual che si desidera monitorare.", "title": "Configura AirVisual" } diff --git a/homeassistant/components/airvisual/translations/ko.json b/homeassistant/components/airvisual/translations/ko.json index 3ab3dbcf286..ddee51dcb3e 100644 --- a/homeassistant/components/airvisual/translations/ko.json +++ b/homeassistant/components/airvisual/translations/ko.json @@ -11,15 +11,6 @@ "location_not_found": "\uc704\uce58\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { - "geography": { - "data": { - "api_key": "API \ud0a4", - "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4" - }, - "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API \ub97c \uc0ac\uc6a9\ud558\uc5ec \uc9c0\ub9ac\uc801 \uc704\uce58\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", - "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30" - }, "geography_by_coords": { "data": { "api_key": "API \ud0a4", @@ -54,11 +45,6 @@ "title": "AirVisual \uc7ac\uc778\uc99d\ud558\uae30" }, "user": { - "data": { - "cloud_api": "\uc9c0\ub9ac\uc801 \uc704\uce58", - "node_pro": "AirVisual Node Pro", - "type": "\uc5f0\ub3d9 \uc720\ud615" - }, "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 AirVisual \ub370\uc774\ud130 \uc720\ud615\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", "title": "AirVisual \uad6c\uc131\ud558\uae30" } diff --git a/homeassistant/components/airvisual/translations/lb.json b/homeassistant/components/airvisual/translations/lb.json index 5a4fb2c07f2..12906b45277 100644 --- a/homeassistant/components/airvisual/translations/lb.json +++ b/homeassistant/components/airvisual/translations/lb.json @@ -11,15 +11,6 @@ "location_not_found": "Standuert net fonnt." }, "step": { - "geography": { - "data": { - "api_key": "API Schl\u00ebssel", - "latitude": "Breedegrad", - "longitude": "L\u00e4ngegrad" - }, - "description": "Benotz Airvisual cloud API fir eng geografescher Lag z'iwwerwaachen.", - "title": "Geografie ariichten" - }, "geography_by_name": { "data": { "city": "Stad", @@ -42,11 +33,6 @@ "title": "AirVisual re-authentifiz\u00e9ieren" }, "user": { - "data": { - "cloud_api": "Geografesche Standuert", - "node_pro": "Airvisual Node Pro", - "type": "Typ vun der Integratioun" - }, "description": "Typ vun Airvisual Donn\u00e9\u00eb fir d'Iwwerwachung auswielen.", "title": "AirVisual konfigur\u00e9ieren" } diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json index ed81d6568ed..ddbcc6e6009 100644 --- a/homeassistant/components/airvisual/translations/nl.json +++ b/homeassistant/components/airvisual/translations/nl.json @@ -11,15 +11,6 @@ "location_not_found": "Locatie niet gevonden" }, "step": { - "geography": { - "data": { - "api_key": "API-sleutel", - "latitude": "Breedtegraad", - "longitude": "Lengtegraad" - }, - "description": "Gebruik de AirVisual cloud API om een geografische locatie te bewaken.", - "title": "Configureer een geografie" - }, "geography_by_coords": { "data": { "api_key": "API-sleutel", @@ -54,11 +45,6 @@ "title": "Verifieer AirVisual opnieuw" }, "user": { - "data": { - "cloud_api": "Geografische ligging", - "node_pro": "AirVisual Node Pro", - "type": "Integratietype" - }, "description": "Kies welk type AirVisual-gegevens u wilt bewaken.", "title": "Configureer AirVisual" } diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index 7c5b0333652..d4ca80d4805 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -11,15 +11,6 @@ "location_not_found": "Stedet ble ikke funnet" }, "step": { - "geography": { - "data": { - "api_key": "API-n\u00f8kkel", - "latitude": "Breddegrad", - "longitude": "Lengdegrad" - }, - "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en geografisk plassering.", - "title": "Konfigurer en Geography" - }, "geography_by_coords": { "data": { "api_key": "API-n\u00f8kkel", @@ -54,11 +45,6 @@ "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { - "data": { - "cloud_api": "Geografisk plassering", - "node_pro": "", - "type": "Integrasjonstype" - }, "description": "Velg hvilken type AirVisual-data du vil overv\u00e5ke.", "title": "Konfigurer AirVisual" } diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json index 5590a951641..26883f514cd 100644 --- a/homeassistant/components/airvisual/translations/pl.json +++ b/homeassistant/components/airvisual/translations/pl.json @@ -11,15 +11,6 @@ "location_not_found": "Nie znaleziono lokalizacji" }, "step": { - "geography": { - "data": { - "api_key": "Klucz API", - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna" - }, - "description": "U\u017cyj interfejsu API chmury AirVisual do monitorowania lokalizacji geograficznej.", - "title": "Konfiguracja Geography" - }, "geography_by_coords": { "data": { "api_key": "Klucz API", @@ -54,11 +45,6 @@ "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { - "data": { - "cloud_api": "Lokalizacja geograficzna", - "node_pro": "AirVisual Node Pro", - "type": "Typ integracji" - }, "description": "Wybierz, kt\u00f3re dane AirVisual chcesz monitorowa\u0107.", "title": "Konfiguracja AirVisual" } diff --git a/homeassistant/components/airvisual/translations/pt-BR.json b/homeassistant/components/airvisual/translations/pt-BR.json index 9f78c46b5e0..733411f2465 100644 --- a/homeassistant/components/airvisual/translations/pt-BR.json +++ b/homeassistant/components/airvisual/translations/pt-BR.json @@ -5,21 +5,10 @@ "invalid_api_key": "Chave de API fornecida \u00e9 inv\u00e1lida." }, "step": { - "geography": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - } - }, "node_pro": { "data": { "password": "Senha" } - }, - "user": { - "data": { - "type": "Tipo de Integra\u00e7\u00e3o" - } } } } diff --git a/homeassistant/components/airvisual/translations/pt.json b/homeassistant/components/airvisual/translations/pt.json index d6732cdddcf..cc1c500946d 100644 --- a/homeassistant/components/airvisual/translations/pt.json +++ b/homeassistant/components/airvisual/translations/pt.json @@ -10,13 +10,6 @@ "invalid_api_key": "Chave de API inv\u00e1lida" }, "step": { - "geography": { - "data": { - "api_key": "API Key", - "latitude": "Latitude", - "longitude": "Longitude" - } - }, "node_pro": { "data": { "ip_address": "Servidor", diff --git a/homeassistant/components/airvisual/translations/ru.json b/homeassistant/components/airvisual/translations/ru.json index bc648c84bfe..4f0073d3132 100644 --- a/homeassistant/components/airvisual/translations/ru.json +++ b/homeassistant/components/airvisual/translations/ru.json @@ -11,15 +11,6 @@ "location_not_found": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e." }, "step": { - "geography": { - "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" - }, - "description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e API AirVisual.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" - }, "geography_by_coords": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", @@ -54,11 +45,6 @@ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" }, "user": { - "data": { - "cloud_api": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", - "node_pro": "AirVisual Node Pro", - "type": "\u0422\u0438\u043f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438" - }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0434\u0430\u043d\u043d\u044b\u0445 AirVisual, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c.", "title": "AirVisual" } diff --git a/homeassistant/components/airvisual/translations/sl.json b/homeassistant/components/airvisual/translations/sl.json index 201b696a8ce..fc611a1589e 100644 --- a/homeassistant/components/airvisual/translations/sl.json +++ b/homeassistant/components/airvisual/translations/sl.json @@ -8,15 +8,6 @@ "invalid_api_key": "Vpisan neveljaven API klju\u010d" }, "step": { - "geography": { - "data": { - "api_key": "Klju\u010d API", - "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina" - }, - "description": "Uporabite API oblaka AirVisual za spremljanje geografske lokacije.", - "title": "Konfigurirajte lokacijo" - }, "node_pro": { "data": { "ip_address": "IP naslov/ime gostitelja enote", @@ -26,11 +17,6 @@ "title": "Konfigurirajte AirVisual Node/Pro" }, "user": { - "data": { - "cloud_api": "Geografska lokacija", - "node_pro": "AirVisual Node Pro", - "type": "Vrsta integracije" - }, "description": "Spremljajte kakovost zraka na zemljepisni lokaciji.", "title": "Nastavite AirVisual" } diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index 9faebc9e960..6a33c0393d9 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -12,10 +12,6 @@ } }, "user": { - "data": { - "cloud_api": "Geografisk Plats", - "type": "Integrationstyp" - }, "title": "Konfigurera AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json index 3d20c8ea9fc..6f27841ea13 100644 --- a/homeassistant/components/airvisual/translations/tr.json +++ b/homeassistant/components/airvisual/translations/tr.json @@ -10,13 +10,6 @@ "location_not_found": "Konum bulunamad\u0131" }, "step": { - "geography": { - "data": { - "api_key": "API Anahtar\u0131", - "latitude": "Enlem", - "longitude": "Boylam" - } - }, "geography_by_coords": { "data": { "api_key": "API Anahtar\u0131", diff --git a/homeassistant/components/airvisual/translations/uk.json b/homeassistant/components/airvisual/translations/uk.json index d99c58de7c0..4a4ea6c8b90 100644 --- a/homeassistant/components/airvisual/translations/uk.json +++ b/homeassistant/components/airvisual/translations/uk.json @@ -10,15 +10,6 @@ "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" }, "step": { - "geography": { - "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" - }, - "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0445\u043c\u0430\u0440\u043d\u043e\u0433\u043e API AirVisual.", - "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" - }, "node_pro": { "data": { "ip_address": "\u0425\u043e\u0441\u0442", @@ -34,11 +25,6 @@ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u044e" }, "user": { - "data": { - "cloud_api": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f", - "node_pro": "AirVisual Node Pro", - "type": "\u0422\u0438\u043f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" - }, "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u0434\u0430\u043d\u0438\u0445 AirVisual, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438.", "title": "AirVisual" } diff --git a/homeassistant/components/airvisual/translations/vi.json b/homeassistant/components/airvisual/translations/vi.json deleted file mode 100644 index 6246d8997da..00000000000 --- a/homeassistant/components/airvisual/translations/vi.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "type": "Lo\u1ea1i t\u00edch h\u1ee3p" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index 3767d41b519..172f57de938 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -11,15 +11,6 @@ "location_not_found": "\u627e\u4e0d\u5230\u5730\u9ede" }, "step": { - "geography": { - "data": { - "api_key": "API \u5bc6\u9470", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6" - }, - "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u5730\u7406\u5ea7\u6a19\u3002", - "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" - }, "geography_by_coords": { "data": { "api_key": "API \u5bc6\u9470", @@ -54,11 +45,6 @@ "title": "\u91cd\u65b0\u8a8d\u8b49 AirVisual" }, "user": { - "data": { - "cloud_api": "\u5730\u7406\u5ea7\u6a19", - "node_pro": "AirVisual Node Pro", - "type": "\u6574\u5408\u985e\u578b" - }, "description": "\u9078\u64c7\u6240\u8981\u76e3\u63a7\u7684 AirVisual \u8cc7\u6599\u985e\u578b\u3002", "title": "\u8a2d\u5b9a AirVisual" } diff --git a/homeassistant/components/apple_tv/translations/ca.json b/homeassistant/components/apple_tv/translations/ca.json index e9cd136720f..646931135e2 100644 --- a/homeassistant/components/apple_tv/translations/ca.json +++ b/homeassistant/components/apple_tv/translations/ca.json @@ -16,7 +16,7 @@ "no_usable_service": "S'ha trobat un dispositiu per\u00f2 no ha pogut identificar cap manera d'establir-hi una connexi\u00f3. Si continues veient aquest missatge, prova d'especificar-ne l'adre\u00e7a IP o reinicia l'Apple TV.", "unknown": "Error inesperat" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Est\u00e0s a punt d'afegir l'Apple TV amb nom \"{name}\" a Home Assistant.\n\n **Per completar el proc\u00e9s, \u00e9s possible que hagis d'introduir alguns codis PIN.** \n\n Tingues en compte que *no* pots apagar la teva Apple TV a trav\u00e9s d'aquesta integraci\u00f3. Nom\u00e9s es desactivar\u00e0 el reproductor de Home Assistant.", diff --git a/homeassistant/components/apple_tv/translations/et.json b/homeassistant/components/apple_tv/translations/et.json index 597c4756907..a4a06d8e1b1 100644 --- a/homeassistant/components/apple_tv/translations/et.json +++ b/homeassistant/components/apple_tv/translations/et.json @@ -16,7 +16,7 @@ "no_usable_service": "Leiti seade kuid ei suudetud tuvastada moodust \u00fchenduse loomiseks. Kui n\u00e4ed seda teadet pidevalt, proovi m\u00e4\u00e4rata seadme IP-aadress v\u00f5i taask\u00e4ivita Apple TV.", "unknown": "Ootamatu t\u00f5rge" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Oled Home Assistantile lisamas Apple TV-d nimega {name}.\n\n**Protsessi l\u00f5puleviimiseks pead v\u00f5ib-olla sisestama mitu PIN-koodi.**\n\nPane t\u00e4hele, et selle sidumisega * ei saa * v\u00e4lja l\u00fclitada oma Apple TV-d. Ainult Home Assistant-i meediam\u00e4ngija l\u00fclitub v\u00e4lja!", diff --git a/homeassistant/components/apple_tv/translations/no.json b/homeassistant/components/apple_tv/translations/no.json index 88a7c986152..993f7708367 100644 --- a/homeassistant/components/apple_tv/translations/no.json +++ b/homeassistant/components/apple_tv/translations/no.json @@ -16,7 +16,7 @@ "no_usable_service": "En enhet ble funnet, men kunne ikke identifisere noen m\u00e5te \u00e5 etablere en tilkobling til den. Hvis du fortsetter \u00e5 se denne meldingen, kan du pr\u00f8ve \u00e5 angi IP-adressen eller starte Apple TV p\u00e5 nytt.", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "{name}", "step": { "confirm": { "description": "Du er i ferd med \u00e5 legge til Apple TV med navnet {name} i Home Assistant.\n\n**For \u00e5 fullf\u00f8re prosessen m\u00e5 du kanskje angi flere PIN-koder.**\n\nV\u00e6r oppmerksom p\u00e5 at du *ikke* kan sl\u00e5 av Apple TV med denne integreringen. Bare mediespilleren i Home Assistant sl\u00e5r seg av!", diff --git a/homeassistant/components/apple_tv/translations/ru.json b/homeassistant/components/apple_tv/translations/ru.json index 4ad9b9f52c7..b37452d6bcb 100644 --- a/homeassistant/components/apple_tv/translations/ru.json +++ b/homeassistant/components/apple_tv/translations/ru.json @@ -16,7 +16,7 @@ "no_usable_service": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0415\u0441\u043b\u0438 \u0412\u044b \u0443\u0436\u0435 \u0432\u0438\u0434\u0435\u043b\u0438 \u044d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u0435\u0433\u043e.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u0412\u044b \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Apple TV `{name}` \u0432 Home Assistant. \n\n**\u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u0412\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432\u0432\u0435\u0441\u0442\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e PIN-\u043a\u043e\u0434\u043e\u0432.** \n\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0412\u044b *\u043d\u0435* \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c Apple TV \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u0412 Home Assistant \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440!", diff --git a/homeassistant/components/apple_tv/translations/zh-Hant.json b/homeassistant/components/apple_tv/translations/zh-Hant.json index ea6cbf7d3d4..ced7a18d2a2 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hant.json +++ b/homeassistant/components/apple_tv/translations/zh-Hant.json @@ -16,7 +16,7 @@ "no_usable_service": "\u627e\u5230\u7684\u88dd\u7f6e\u7121\u6cd5\u8b58\u5225\u4ee5\u9032\u884c\u9023\u7dda\u3002\u5047\u5982\u6b64\u8a0a\u606f\u91cd\u8907\u767c\u751f\u3002\u8acb\u8a66\u8457\u6307\u5b9a\u7279\u5b9a IP \u4f4d\u5740\u6216\u91cd\u555f Apple TV\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "Apple TV\uff1a{name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u6b63\u8981\u65b0\u589e\u540d\u70ba `{name}` \u7684 Apple TV \u81f3 Home Assistant\u3002\n\n**\u6b32\u5b8c\u6210\u6b65\u9a5f\uff0c\u5fc5\u9808\u8f38\u5165\u591a\u7d44 PIN \u78bc\u3002**\n\n\u8acb\u6ce8\u610f\uff1a\u6b64\u6574\u5408\u4e26 *\u7121\u6cd5* \u9032\u884c Apple TV \u95dc\u6a5f\u7684\u52d5\u4f5c\uff0c\u50c5\u80fd\u65bc Home Assistant \u4e2d\u95dc\u9589\u5a92\u9ad4\u64ad\u653e\u5668\u529f\u80fd\uff01", diff --git a/homeassistant/components/arcam_fmj/translations/ca.json b/homeassistant/components/arcam_fmj/translations/ca.json index 6d30f32e16a..84ed4dd8990 100644 --- a/homeassistant/components/arcam_fmj/translations/ca.json +++ b/homeassistant/components/arcam_fmj/translations/ca.json @@ -5,7 +5,7 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "Arcam FMJ a {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "Vols afegir l'Arcam FMJ `{host}` a Home Assistant?" diff --git a/homeassistant/components/arcam_fmj/translations/en.json b/homeassistant/components/arcam_fmj/translations/en.json index 20a71df9d67..891f268aa1f 100644 --- a/homeassistant/components/arcam_fmj/translations/en.json +++ b/homeassistant/components/arcam_fmj/translations/en.json @@ -5,7 +5,6 @@ "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect" }, - "error": {}, "flow_title": "{host}", "step": { "confirm": { diff --git a/homeassistant/components/arcam_fmj/translations/et.json b/homeassistant/components/arcam_fmj/translations/et.json index 84735beefab..60f1895039e 100644 --- a/homeassistant/components/arcam_fmj/translations/et.json +++ b/homeassistant/components/arcam_fmj/translations/et.json @@ -5,7 +5,7 @@ "already_in_progress": "Seadistamine on juba k\u00e4imas", "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "Arcam FMJ saidil {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "Kas soovid lisada Arcam FMJ \u00fcksuse {host} Home Assistanti?" diff --git a/homeassistant/components/arcam_fmj/translations/no.json b/homeassistant/components/arcam_fmj/translations/no.json index e98f943f565..8e4d28d80b8 100644 --- a/homeassistant/components/arcam_fmj/translations/no.json +++ b/homeassistant/components/arcam_fmj/translations/no.json @@ -5,7 +5,7 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "Arcam FMJ p\u00e5 {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "Vil du legge Arcam FMJ p\u00e5 `{host}` til Home Assistant?" diff --git a/homeassistant/components/arcam_fmj/translations/ru.json b/homeassistant/components/arcam_fmj/translations/ru.json index 8b3c3092745..20f44f068de 100644 --- a/homeassistant/components/arcam_fmj/translations/ru.json +++ b/homeassistant/components/arcam_fmj/translations/ru.json @@ -5,7 +5,7 @@ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "Arcam FMJ {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Arcam FMJ `{host}`?" diff --git a/homeassistant/components/arcam_fmj/translations/zh-Hant.json b/homeassistant/components/arcam_fmj/translations/zh-Hant.json index 4c7455f8444..358805e0de6 100644 --- a/homeassistant/components/arcam_fmj/translations/zh-Hant.json +++ b/homeassistant/components/arcam_fmj/translations/zh-Hant.json @@ -5,7 +5,7 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "Arcam FMJ \uff08{host}\uff09", + "flow_title": "{host}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u5c07 Arcam FMJ `{host}` \u65b0\u589e\u81f3 Home Assistant\uff1f" diff --git a/homeassistant/components/atag/translations/ca.json b/homeassistant/components/atag/translations/ca.json index dbd73d0bf43..537d347a228 100644 --- a/homeassistant/components/atag/translations/ca.json +++ b/homeassistant/components/atag/translations/ca.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "Correu electr\u00f2nic", "host": "Amfitri\u00f3", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/cs.json b/homeassistant/components/atag/translations/cs.json index 105c53e9a46..a94ba7fe391 100644 --- a/homeassistant/components/atag/translations/cs.json +++ b/homeassistant/components/atag/translations/cs.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "E-mail", "host": "Hostitel", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json index 8b2b7ce4dff..72e8c69cc26 100644 --- a/homeassistant/components/atag/translations/de.json +++ b/homeassistant/components/atag/translations/de.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "E-Mail", "host": "Host", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/en.json b/homeassistant/components/atag/translations/en.json index ea354acffde..1cd25c2a9b2 100644 --- a/homeassistant/components/atag/translations/en.json +++ b/homeassistant/components/atag/translations/en.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "Email", "host": "Host", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/es-419.json b/homeassistant/components/atag/translations/es-419.json index 68da80cbb7e..92e7fae8703 100644 --- a/homeassistant/components/atag/translations/es-419.json +++ b/homeassistant/components/atag/translations/es-419.json @@ -6,7 +6,6 @@ "step": { "user": { "data": { - "email": "Correo electr\u00f3nico (opcional)", "host": "Host", "port": "Puerto (10000)" }, diff --git a/homeassistant/components/atag/translations/es.json b/homeassistant/components/atag/translations/es.json index b71c91693d1..ed89c8c385c 100644 --- a/homeassistant/components/atag/translations/es.json +++ b/homeassistant/components/atag/translations/es.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "Correo electr\u00f3nico (Opcional)", "host": "Host", "port": "Puerto" }, diff --git a/homeassistant/components/atag/translations/et.json b/homeassistant/components/atag/translations/et.json index fd0651a219c..fd157a3d541 100644 --- a/homeassistant/components/atag/translations/et.json +++ b/homeassistant/components/atag/translations/et.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "E-post", "host": "", "port": "" }, diff --git a/homeassistant/components/atag/translations/fr.json b/homeassistant/components/atag/translations/fr.json index c8a19a44eb4..a0f4b9f3808 100644 --- a/homeassistant/components/atag/translations/fr.json +++ b/homeassistant/components/atag/translations/fr.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "Courriel (facultatif)", "host": "Nom d'h\u00f4te ou adresse IP", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/he.json b/homeassistant/components/atag/translations/he.json deleted file mode 100644 index 9212fe8f93f..00000000000 --- a/homeassistant/components/atag/translations/he.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "email": "Payload (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json index 98e947ae643..134f3bedfe8 100644 --- a/homeassistant/components/atag/translations/hu.json +++ b/homeassistant/components/atag/translations/hu.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "email": "E-mail", "host": "Hoszt", "port": "Port" } diff --git a/homeassistant/components/atag/translations/id.json b/homeassistant/components/atag/translations/id.json index 24732f8c235..33f4cf62b2e 100644 --- a/homeassistant/components/atag/translations/id.json +++ b/homeassistant/components/atag/translations/id.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "Email", "host": "Host", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/it.json b/homeassistant/components/atag/translations/it.json index 060f9d21b20..1bc473a6001 100644 --- a/homeassistant/components/atag/translations/it.json +++ b/homeassistant/components/atag/translations/it.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "E-mail", "host": "Host", "port": "Porta" }, diff --git a/homeassistant/components/atag/translations/ko.json b/homeassistant/components/atag/translations/ko.json index 9b0c1ea1b36..25de83f70c3 100644 --- a/homeassistant/components/atag/translations/ko.json +++ b/homeassistant/components/atag/translations/ko.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "\uc774\uba54\uc77c", "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" }, diff --git a/homeassistant/components/atag/translations/lb.json b/homeassistant/components/atag/translations/lb.json index afb8aea1697..1238d2bcf52 100644 --- a/homeassistant/components/atag/translations/lb.json +++ b/homeassistant/components/atag/translations/lb.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "E-Mail", "host": "Apparat", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/nl.json b/homeassistant/components/atag/translations/nl.json index 55478f765e2..98200dd3f6e 100644 --- a/homeassistant/components/atag/translations/nl.json +++ b/homeassistant/components/atag/translations/nl.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "Email", "host": "Host", "port": "Poort " }, diff --git a/homeassistant/components/atag/translations/no.json b/homeassistant/components/atag/translations/no.json index 650605c270f..6a2736ae8b4 100644 --- a/homeassistant/components/atag/translations/no.json +++ b/homeassistant/components/atag/translations/no.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "E-post", "host": "Vert", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/pl.json b/homeassistant/components/atag/translations/pl.json index 4c690ba057e..bdd38a3d980 100644 --- a/homeassistant/components/atag/translations/pl.json +++ b/homeassistant/components/atag/translations/pl.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "Adres e-mail", "host": "Nazwa hosta lub adres IP", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/pt-BR.json b/homeassistant/components/atag/translations/pt-BR.json index a98060320fc..5d9d5079110 100644 --- a/homeassistant/components/atag/translations/pt-BR.json +++ b/homeassistant/components/atag/translations/pt-BR.json @@ -2,13 +2,6 @@ "config": { "abort": { "already_configured": "Este dispositivo j\u00e1 foi adicionado ao Home Assistant" - }, - "step": { - "user": { - "data": { - "email": "E-mail (Opcional)" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/atag/translations/pt.json b/homeassistant/components/atag/translations/pt.json index 16752dd0071..fa5aa3de317 100644 --- a/homeassistant/components/atag/translations/pt.json +++ b/homeassistant/components/atag/translations/pt.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "email": "E-mail (opcional)", "host": "Servidor", "port": "Porta" } diff --git a/homeassistant/components/atag/translations/ru.json b/homeassistant/components/atag/translations/ru.json index beb0ee904cd..feb21d3addd 100644 --- a/homeassistant/components/atag/translations/ru.json +++ b/homeassistant/components/atag/translations/ru.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, diff --git a/homeassistant/components/atag/translations/tr.json b/homeassistant/components/atag/translations/tr.json index f7c94d0a976..577ed02cdca 100644 --- a/homeassistant/components/atag/translations/tr.json +++ b/homeassistant/components/atag/translations/tr.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "E-posta", "host": "Ana Bilgisayar", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/uk.json b/homeassistant/components/atag/translations/uk.json index ee0a077d900..d6b259debc6 100644 --- a/homeassistant/components/atag/translations/uk.json +++ b/homeassistant/components/atag/translations/uk.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, diff --git a/homeassistant/components/atag/translations/zh-Hant.json b/homeassistant/components/atag/translations/zh-Hant.json index 8eb427b95ee..c9904a954d7 100644 --- a/homeassistant/components/atag/translations/zh-Hant.json +++ b/homeassistant/components/atag/translations/zh-Hant.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "email": "\u96fb\u5b50\u90f5\u4ef6", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, diff --git a/homeassistant/components/august/translations/ca.json b/homeassistant/components/august/translations/ca.json index 8faa12e2757..f0b1fa43c3d 100644 --- a/homeassistant/components/august/translations/ca.json +++ b/homeassistant/components/august/translations/ca.json @@ -17,16 +17,6 @@ "description": "Introdueix la contrasenya per a {username}.", "title": "Torna a autenticar compte d'August" }, - "user": { - "data": { - "login_method": "M\u00e8tode d'inici de sessi\u00f3", - "password": "Contrasenya", - "timeout": "Temps d'espera (segons)", - "username": "Nom d'usuari" - }, - "description": "Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'email', el nom d'usuari \u00e9s l'adre\u00e7a de correu electr\u00f2nic. Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'phone', el nom d'usuari \u00e9s el n\u00famero de tel\u00e8fon en el format \"+NNNNNNNNN\".", - "title": "Configuraci\u00f3 de compte August" - }, "user_validate": { "data": { "login_method": "M\u00e8tode d'inici de sessi\u00f3", diff --git a/homeassistant/components/august/translations/cs.json b/homeassistant/components/august/translations/cs.json index 4176da8f1bf..6cb0fbd238d 100644 --- a/homeassistant/components/august/translations/cs.json +++ b/homeassistant/components/august/translations/cs.json @@ -15,16 +15,6 @@ "password": "Heslo" } }, - "user": { - "data": { - "login_method": "Zp\u016fsob p\u0159ihl\u00e1\u0161en\u00ed", - "password": "Heslo", - "timeout": "\u010casov\u00fd limit (v sekund\u00e1ch)", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, - "description": "Pokud je metoda p\u0159ihl\u00e1\u0161en\u00ed \"e-mail\", je e-mailovou adresou u\u017eivatelsk\u00e9 jm\u00e9no. Pokud je p\u0159ihla\u0161ovac\u00ed metoda \"telefon\", u\u017eivatelsk\u00e9 jm\u00e9no je telefonn\u00ed \u010d\u00edslo ve form\u00e1tu \"+NNNNNNNNN\".", - "title": "Nastavte \u00fa\u010det August" - }, "user_validate": { "data": { "password": "Heslo", diff --git a/homeassistant/components/august/translations/da.json b/homeassistant/components/august/translations/da.json index e022fac3790..de3fe4e8639 100644 --- a/homeassistant/components/august/translations/da.json +++ b/homeassistant/components/august/translations/da.json @@ -9,16 +9,6 @@ "unknown": "Uventet fejl" }, "step": { - "user": { - "data": { - "login_method": "Loginmetode", - "password": "Adgangskode", - "timeout": "Timeout (sekunder)", - "username": "Brugernavn" - }, - "description": "Hvis loginmetoden er 'e-mail', er brugernavn e-mailadressen. Hvis loginmetoden er 'telefon', er brugernavn telefonnummeret i formatet '+NNNNNNNNNN'.", - "title": "Konfigurer en August-konto" - }, "validation": { "data": { "code": "Bekr\u00e6ftelseskode" diff --git a/homeassistant/components/august/translations/de.json b/homeassistant/components/august/translations/de.json index ef525fb665d..d2e08a5377c 100644 --- a/homeassistant/components/august/translations/de.json +++ b/homeassistant/components/august/translations/de.json @@ -17,16 +17,6 @@ "description": "Gib das Passwort f\u00fcr {username} ein.", "title": "August-Konto erneut authentifizieren" }, - "user": { - "data": { - "login_method": "Anmeldemethode", - "password": "Passwort", - "timeout": "Zeit\u00fcberschreitung (Sekunden)", - "username": "Benutzername" - }, - "description": "Wenn die Anmeldemethode \"E-Mail\" lautet, ist Benutzername die E-Mail-Adresse. Wenn die Anmeldemethode \"Telefon\" ist, ist Benutzername die Telefonnummer im Format \"+ NNNNNNNNN\".", - "title": "Richten Sie ein August-Konto ein" - }, "user_validate": { "data": { "login_method": "Anmeldemethode", diff --git a/homeassistant/components/august/translations/en.json b/homeassistant/components/august/translations/en.json index 0b8d1511244..f2ceef78d48 100644 --- a/homeassistant/components/august/translations/en.json +++ b/homeassistant/components/august/translations/en.json @@ -17,16 +17,6 @@ "description": "Enter the password for {username}.", "title": "Reauthenticate an August account" }, - "user": { - "data": { - "login_method": "Login Method", - "password": "Password", - "timeout": "Timeout (seconds)", - "username": "Username" - }, - "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", - "title": "Setup an August account" - }, "user_validate": { "data": { "login_method": "Login Method", diff --git a/homeassistant/components/august/translations/es-419.json b/homeassistant/components/august/translations/es-419.json index 914aea1b801..7e5fe76d3af 100644 --- a/homeassistant/components/august/translations/es-419.json +++ b/homeassistant/components/august/translations/es-419.json @@ -9,16 +9,6 @@ "unknown": "Error inesperado" }, "step": { - "user": { - "data": { - "login_method": "M\u00e9todo de inicio de sesi\u00f3n", - "password": "Contrase\u00f1a", - "timeout": "Tiempo de espera (segundos)", - "username": "Nombre de usuario" - }, - "description": "Si el M\u00e9todo de inicio de sesi\u00f3n es 'correo electr\u00f3nico', Nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el M\u00e9todo de inicio de sesi\u00f3n es 'tel\u00e9fono', Nombre de usuario es el n\u00famero de tel\u00e9fono en el formato '+NNNNNNNNN'.", - "title": "Configurar una cuenta de August" - }, "validation": { "data": { "code": "C\u00f3digo de verificaci\u00f3n" diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index d30db423db6..a660d11f996 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -17,16 +17,6 @@ "description": "Introduzca la contrase\u00f1a de {username}.", "title": "Reautorizar una cuenta de August" }, - "user": { - "data": { - "login_method": "M\u00e9todo de inicio de sesi\u00f3n", - "password": "Contrase\u00f1a", - "timeout": "Tiempo de espera (segundos)", - "username": "Usuario" - }, - "description": "Si el M\u00e9todo de Inicio de Sesi\u00f3n es 'correo electr\u00f3nico', Usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el M\u00e9todo de Inicio de Sesi\u00f3n es 'tel\u00e9fono', Usuario es el n\u00famero de tel\u00e9fono en formato '+NNNNNNNNN'.", - "title": "Configurar una cuenta de August" - }, "user_validate": { "data": { "login_method": "M\u00e9todo de inicio de sesi\u00f3n", diff --git a/homeassistant/components/august/translations/et.json b/homeassistant/components/august/translations/et.json index 69cd9e66ce3..e310df86374 100644 --- a/homeassistant/components/august/translations/et.json +++ b/homeassistant/components/august/translations/et.json @@ -17,16 +17,6 @@ "description": "Sisesta kasutaja {username} salas\u00f5na.", "title": "Autendi Augusti konto uuesti" }, - "user": { - "data": { - "login_method": "Sisselogimismeetod", - "password": "Salas\u00f5na", - "timeout": "Ajal\u00f5pp (sekundites)", - "username": "Kasutajanimi" - }, - "description": "Kui sisselogimismeetod on \"e-post\" on kasutajanimi e-posti aadress. Kui sisselogimismeetod on \"telefon\" on kasutajanimi telefoninumber vormingus \"+NNNNNNNNN\".", - "title": "Seadista Augusti sidumise konto" - }, "user_validate": { "data": { "login_method": "Sisselogimismeetod", diff --git a/homeassistant/components/august/translations/fr.json b/homeassistant/components/august/translations/fr.json index 967fb249d97..aebb72a76ed 100644 --- a/homeassistant/components/august/translations/fr.json +++ b/homeassistant/components/august/translations/fr.json @@ -17,16 +17,6 @@ "description": "Saisissez le mot de passe de {username} .", "title": "R\u00e9authentifier un compte August" }, - "user": { - "data": { - "login_method": "M\u00e9thode de connexion", - "password": "Mot de passe", - "timeout": "D\u00e9lai d'expiration (secondes)", - "username": "Nom d'utilisateur" - }, - "description": "Si la m\u00e9thode de connexion est \u00abe-mail\u00bb, le nom d'utilisateur est l'adresse e-mail. Si la m\u00e9thode de connexion est \u00abt\u00e9l\u00e9phone\u00bb, le nom d'utilisateur est le num\u00e9ro de t\u00e9l\u00e9phone au format \u00ab+ NNNNNNNNN\u00bb.", - "title": "Configurer un compte August" - }, "user_validate": { "data": { "login_method": "M\u00e9thode de connexion", diff --git a/homeassistant/components/august/translations/he.json b/homeassistant/components/august/translations/he.json deleted file mode 100644 index ac90b3264ea..00000000000 --- a/homeassistant/components/august/translations/he.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index 1bced4e1036..f95d180b4b5 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -17,13 +17,6 @@ "description": "Add meg a(z) {username} jelszav\u00e1t.", "title": "August fi\u00f3k \u00fajrahiteles\u00edt\u00e9se" }, - "user": { - "data": { - "password": "Jelsz\u00f3", - "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (m\u00e1sodperc)", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } - }, "user_validate": { "data": { "login_method": "Bejelentkez\u00e9si m\u00f3d", diff --git a/homeassistant/components/august/translations/id.json b/homeassistant/components/august/translations/id.json index 5408c2c0f70..19c1309d8ed 100644 --- a/homeassistant/components/august/translations/id.json +++ b/homeassistant/components/august/translations/id.json @@ -17,16 +17,6 @@ "description": "Masukkan sandi untuk {username}.", "title": "Autentikasi ulang akun August" }, - "user": { - "data": { - "login_method": "Metode Masuk", - "password": "Kata Sandi", - "timeout": "Tenggang waktu (detik)", - "username": "Nama Pengguna" - }, - "description": "Jika Metode Masuk adalah 'email', Nama Pengguna adalah alamat email. Jika Metode Masuk adalah 'telepon', Nama Pengguna adalah nomor telepon dalam format '+NNNNNNNNN'.", - "title": "Siapkan akun August" - }, "user_validate": { "data": { "login_method": "Metode Masuk", diff --git a/homeassistant/components/august/translations/it.json b/homeassistant/components/august/translations/it.json index c20f95b90ad..0eb8b20f0a8 100644 --- a/homeassistant/components/august/translations/it.json +++ b/homeassistant/components/august/translations/it.json @@ -17,16 +17,6 @@ "description": "Inserisci la password per {username}.", "title": "Riautentica un account di August" }, - "user": { - "data": { - "login_method": "Metodo di accesso", - "password": "Password", - "timeout": "Timeout (in secondi)", - "username": "Nome utente" - }, - "description": "Se il metodo di accesso \u00e8 \"e-mail\", il nome utente \u00e8 l'indirizzo e-mail. Se il metodo di accesso \u00e8 \"telefono\", il nome utente \u00e8 il numero di telefono nel formato \"+NNNNNNNNN\".", - "title": "Configura un account di August" - }, "user_validate": { "data": { "login_method": "Metodo di accesso", diff --git a/homeassistant/components/august/translations/ko.json b/homeassistant/components/august/translations/ko.json index f3bc64a706c..1902d0112ff 100644 --- a/homeassistant/components/august/translations/ko.json +++ b/homeassistant/components/august/translations/ko.json @@ -17,16 +17,6 @@ "description": "{username}\uc758 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "August \uacc4\uc815 \uc7ac\uc778\uc99d\ud558\uae30" }, - "user": { - "data": { - "login_method": "\ub85c\uadf8\uc778 \ubc29\ubc95", - "password": "\ube44\ubc00\ubc88\ud638", - "timeout": "\uc81c\ud55c \uc2dc\uac04 (\ucd08)", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, - "description": "\ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc774\uba54\uc77c'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4. \ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc804\ud654\ubc88\ud638'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 '+NNNNNNNNN' \ud615\uc2dd\uc758 \uc804\ud654\ubc88\ud638\uc785\ub2c8\ub2e4.", - "title": "August \uacc4\uc815 \uc124\uc815\ud558\uae30" - }, "user_validate": { "data": { "login_method": "\ub85c\uadf8\uc778 \ubc29\ubc95", diff --git a/homeassistant/components/august/translations/lb.json b/homeassistant/components/august/translations/lb.json index 569771dc393..16934964651 100644 --- a/homeassistant/components/august/translations/lb.json +++ b/homeassistant/components/august/translations/lb.json @@ -13,16 +13,6 @@ "reauth_validate": { "description": "G\u00ebff Passwuert an fir {username}." }, - "user": { - "data": { - "login_method": "Login Method", - "password": "Passwuert", - "timeout": "Z\u00e4itiwwerscheidung (sekonnen)", - "username": "Benotzernumm" - }, - "description": "Wann d'Login Method 'E-Mail' ass, dannn ass de Benotzernumm d'E-Mail Adress. Wann d'Login-Method 'Telefon' ass, ass den Benotzernumm d'Telefonsnummer am Format '+ NNNNNNNNN'.", - "title": "August Kont ariichten" - }, "user_validate": { "data": { "login_method": "Login Method" diff --git a/homeassistant/components/august/translations/lv.json b/homeassistant/components/august/translations/lv.json deleted file mode 100644 index b2afeaf0874..00000000000 --- a/homeassistant/components/august/translations/lv.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "login_method": "Pieteik\u0161an\u0101s metode", - "password": "Parole" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/august/translations/nl.json b/homeassistant/components/august/translations/nl.json index 0ebd8ad3ba6..2d9a4202f74 100644 --- a/homeassistant/components/august/translations/nl.json +++ b/homeassistant/components/august/translations/nl.json @@ -17,16 +17,6 @@ "description": "Voer het wachtwoord in voor {username}.", "title": "Verifieer opnieuw een August-account" }, - "user": { - "data": { - "login_method": "Aanmeldmethode", - "password": "Wachtwoord", - "timeout": "Time-out (seconden)", - "username": "Gebruikersnaam" - }, - "description": "Als de aanmeldingsmethode 'e-mail' is, is gebruikersnaam het e-mailadres. Als de aanmeldingsmethode 'telefoon' is, is gebruikersnaam het telefoonnummer in de indeling '+ NNNNNNNNN'.", - "title": "Stel een augustus-account in" - }, "user_validate": { "data": { "login_method": "Inlogmethode", diff --git a/homeassistant/components/august/translations/no.json b/homeassistant/components/august/translations/no.json index d90e7f8080a..8ea4cd7141f 100644 --- a/homeassistant/components/august/translations/no.json +++ b/homeassistant/components/august/translations/no.json @@ -17,16 +17,6 @@ "description": "Skriv inn passordet for {username} .", "title": "Godkjenn en August-konto p\u00e5 nytt" }, - "user": { - "data": { - "login_method": "P\u00e5loggingsmetode", - "password": "Passord", - "timeout": "Tidsavbrudd (sekunder)", - "username": "Brukernavn" - }, - "description": "Hvis p\u00e5loggingsmetoden er 'e-post', er brukernavnet e-postadressen. Hvis p\u00e5loggingsmetoden er 'telefon', er brukernavn telefonnummeret i formatet '+ NNNNNNNNN'.", - "title": "Sett opp en August konto" - }, "user_validate": { "data": { "login_method": "P\u00e5loggingsmetode", diff --git a/homeassistant/components/august/translations/pl.json b/homeassistant/components/august/translations/pl.json index a5539bea93a..d7deafd228d 100644 --- a/homeassistant/components/august/translations/pl.json +++ b/homeassistant/components/august/translations/pl.json @@ -17,16 +17,6 @@ "description": "Wprowad\u017a has\u0142o dla {username}", "title": "Ponownie uwierzytelnij konto August" }, - "user": { - "data": { - "login_method": "Metoda logowania", - "password": "Has\u0142o", - "timeout": "Limit czasu (sekundy)", - "username": "Nazwa u\u017cytkownika" - }, - "description": "Je\u015bli metod\u0105 logowania jest 'e-mail', nazw\u0105 u\u017cytkownika b\u0119dzie adres e-mail. Je\u015bli metod\u0105 logowania jest 'telefon', nazw\u0105 u\u017cytkownika b\u0119dzie numer telefonu w formacie '+NNNNNNNNN'.", - "title": "Konfiguracja konta August" - }, "user_validate": { "data": { "login_method": "Metoda logowania", diff --git a/homeassistant/components/august/translations/pt-BR.json b/homeassistant/components/august/translations/pt-BR.json index efb4b3db35f..7186be6216c 100644 --- a/homeassistant/components/august/translations/pt-BR.json +++ b/homeassistant/components/august/translations/pt-BR.json @@ -1,13 +1,6 @@ { "config": { "step": { - "user": { - "data": { - "timeout": "Tempo limite (segundos)" - }, - "description": "Se o m\u00e9todo de login for 'email', Nome de usu\u00e1rio \u00e9 o endere\u00e7o de email. Se o m\u00e9todo de login for 'telefone', Nome de usu\u00e1rio ser\u00e1 o n\u00famero de telefone no formato '+NNNNNNNNN'.", - "title": "Configurar uma conta de August" - }, "validation": { "data": { "code": "C\u00f3digo de verifica\u00e7\u00e3o" diff --git a/homeassistant/components/august/translations/pt.json b/homeassistant/components/august/translations/pt.json index 7daa90fad2c..6c6765da70e 100644 --- a/homeassistant/components/august/translations/pt.json +++ b/homeassistant/components/august/translations/pt.json @@ -10,14 +10,6 @@ "unknown": "Erro inesperado" }, "step": { - "user": { - "data": { - "login_method": "M\u00e9todo de login", - "password": "Palavra-passe", - "username": "Nome de Utilizador" - }, - "description": "Se o m\u00e9todo de login for 'email', Nome do utilizador \u00e9 o endere\u00e7o de email. Se o m\u00e9todo de login for 'telefone', Nome do utilizador ser\u00e1 o n\u00famero de telefone no formato '+NNNNNNNNN'." - }, "validation": { "data": { "code": "C\u00f3digo de verifica\u00e7\u00e3o" diff --git a/homeassistant/components/august/translations/ru.json b/homeassistant/components/august/translations/ru.json index 0263ef6ee18..0f57924aef7 100644 --- a/homeassistant/components/august/translations/ru.json +++ b/homeassistant/components/august/translations/ru.json @@ -17,16 +17,6 @@ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" }, - "user": { - "data": { - "login_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, - "description": "\u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 '+NNNNNNNNN'.", - "title": "August" - }, "user_validate": { "data": { "login_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", diff --git a/homeassistant/components/august/translations/sl.json b/homeassistant/components/august/translations/sl.json index 5d78dac5ef1..41be45271c8 100644 --- a/homeassistant/components/august/translations/sl.json +++ b/homeassistant/components/august/translations/sl.json @@ -9,16 +9,6 @@ "unknown": "Nepri\u010dakovana napaka" }, "step": { - "user": { - "data": { - "login_method": "Na\u010din prijave", - "password": "Geslo", - "timeout": "\u010casovna omejitev (sekunde)", - "username": "Uporabni\u0161ko ime" - }, - "description": "\u010ce je metoda za prijavo 'e-po\u0161ta', je e-po\u0161tni naslov uporabni\u0161ko ime. V kolikor je na\u010din prijave \"telefon\", je uporabni\u0161ko ime telefonska \u0161tevilka v obliki \" +NNNNNNNNN\".", - "title": "Nastavite ra\u010dun August" - }, "validation": { "data": { "code": "Koda za preverjanje" diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json index 1ebdfab9fd2..762d5fd7640 100644 --- a/homeassistant/components/august/translations/sv.json +++ b/homeassistant/components/august/translations/sv.json @@ -14,16 +14,6 @@ "password": "L\u00f6senord" } }, - "user": { - "data": { - "login_method": "Inloggningsmetod", - "password": "L\u00f6senord", - "timeout": "Timeout (sekunder)", - "username": "Anv\u00e4ndarnamn" - }, - "description": "Om inloggningsmetoden \u00e4r \"e-post\" \u00e4r anv\u00e4ndarnamnet e-postadressen. Om inloggningsmetoden \u00e4r \"telefon\" \u00e4r anv\u00e4ndarnamnet telefonnumret i formatet \"+ NNNNNNNN\".", - "title": "St\u00e4ll in ett August-konto" - }, "user_validate": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/august/translations/tr.json b/homeassistant/components/august/translations/tr.json index ccb9e200c82..d3b32080466 100644 --- a/homeassistant/components/august/translations/tr.json +++ b/homeassistant/components/august/translations/tr.json @@ -10,15 +10,6 @@ "unknown": "Beklenmeyen hata" }, "step": { - "user": { - "data": { - "login_method": "Giri\u015f Y\u00f6ntemi", - "password": "Parola", - "timeout": "Zaman a\u015f\u0131m\u0131 (saniye)", - "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "description": "Giri\u015f Y\u00f6ntemi 'e-posta' ise, Kullan\u0131c\u0131 Ad\u0131 e-posta adresidir. Giri\u015f Y\u00f6ntemi 'telefon' ise, Kullan\u0131c\u0131 Ad\u0131 '+ NNNNNNNNN' bi\u00e7imindeki telefon numaras\u0131d\u0131r." - }, "validation": { "data": { "code": "Do\u011frulama kodu" diff --git a/homeassistant/components/august/translations/uk.json b/homeassistant/components/august/translations/uk.json index e06c5347d73..5f4729d02b2 100644 --- a/homeassistant/components/august/translations/uk.json +++ b/homeassistant/components/august/translations/uk.json @@ -10,16 +10,6 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { - "user": { - "data": { - "login_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", - "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - }, - "description": "\u042f\u043a\u0449\u043e \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0456\u043d\u043e\u043c \u0454 \u0430\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438. \u042f\u043a\u0449\u043e \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0456\u043d\u043e\u043c \u0454 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 '+ NNNNNNNNN'.", - "title": "August" - }, "validation": { "data": { "code": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f" diff --git a/homeassistant/components/august/translations/zh-Hans.json b/homeassistant/components/august/translations/zh-Hans.json deleted file mode 100644 index a5f4ff11f09..00000000000 --- a/homeassistant/components/august/translations/zh-Hans.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "username": "\u7528\u6237\u540d" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/august/translations/zh-Hant.json b/homeassistant/components/august/translations/zh-Hant.json index ab157e3da3c..17fd85a2d58 100644 --- a/homeassistant/components/august/translations/zh-Hant.json +++ b/homeassistant/components/august/translations/zh-Hant.json @@ -17,16 +17,6 @@ "description": "\u8f38\u5165{username} \u5bc6\u78bc", "title": "\u91cd\u65b0\u8a8d\u8b49 August \u5e33\u865f" }, - "user": { - "data": { - "login_method": "\u767b\u5165\u65b9\u5f0f", - "password": "\u5bc6\u78bc", - "timeout": "\u903e\u6642\uff08\u79d2\uff09", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, - "description": "\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u90f5\u4ef6\u300cemail\u300d\u3001\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u96fb\u8a71\u300cphone\u300d\u3001\u5247\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u5305\u542b\u570b\u78bc\u4e4b\u96fb\u8a71\u865f\u78bc\uff0c\u5982\u300c+NNNNNNNNN\u300d\u3002", - "title": "\u8a2d\u5b9a August \u5e33\u865f" - }, "user_validate": { "data": { "login_method": "\u767b\u5165\u65b9\u5f0f", diff --git a/homeassistant/components/azure_devops/translations/ca.json b/homeassistant/components/azure_devops/translations/ca.json index 92dbd2e3e40..b3eb6e4eb8e 100644 --- a/homeassistant/components/azure_devops/translations/ca.json +++ b/homeassistant/components/azure_devops/translations/ca.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "project_error": "No s'ha pogut obtenir la informaci\u00f3 del projecte." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/azure_devops/translations/et.json b/homeassistant/components/azure_devops/translations/et.json index 63ec0276d89..58931e3fd37 100644 --- a/homeassistant/components/azure_devops/translations/et.json +++ b/homeassistant/components/azure_devops/translations/et.json @@ -9,7 +9,7 @@ "invalid_auth": "Tuvastamise viga", "project_error": "Projekti teavet ei \u00f5nnestunud hankida." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/azure_devops/translations/no.json b/homeassistant/components/azure_devops/translations/no.json index 50ee7a7a2a9..ba4ff946595 100644 --- a/homeassistant/components/azure_devops/translations/no.json +++ b/homeassistant/components/azure_devops/translations/no.json @@ -9,7 +9,7 @@ "invalid_auth": "Ugyldig godkjenning", "project_error": "Kunne ikke f\u00e5 prosjektinformasjon." }, - "flow_title": "", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/azure_devops/translations/ru.json b/homeassistant/components/azure_devops/translations/ru.json index 4e59af2dd11..5e629b6d558 100644 --- a/homeassistant/components/azure_devops/translations/ru.json +++ b/homeassistant/components/azure_devops/translations/ru.json @@ -9,7 +9,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "project_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0435." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/azure_devops/translations/zh-Hant.json b/homeassistant/components/azure_devops/translations/zh-Hant.json index b77ce8c54a7..13f6fcbe276 100644 --- a/homeassistant/components/azure_devops/translations/zh-Hant.json +++ b/homeassistant/components/azure_devops/translations/zh-Hant.json @@ -9,7 +9,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "project_error": "\u7121\u6cd5\u53d6\u5f97\u5c08\u6848\u8cc7\u8a0a\u3002" }, - "flow_title": "Azure DevOps\uff1a{project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/blebox/translations/ca.json b/homeassistant/components/blebox/translations/ca.json index aba528fafb5..d2b25c7590a 100644 --- a/homeassistant/components/blebox/translations/ca.json +++ b/homeassistant/components/blebox/translations/ca.json @@ -9,7 +9,7 @@ "unknown": "Error inesperat", "unsupported_version": "El dispositiu BleBox t\u00e9 un firmware obsolet. Primer actualitza'l." }, - "flow_title": "Dispositiu BleBox: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/blebox/translations/et.json b/homeassistant/components/blebox/translations/et.json index 64ca64c1d80..913428a897e 100644 --- a/homeassistant/components/blebox/translations/et.json +++ b/homeassistant/components/blebox/translations/et.json @@ -9,7 +9,7 @@ "unknown": "Tundmatu viga", "unsupported_version": "BleBoxi seadmel on vananenud p\u00fcsivara. Esmalt v\u00e4rskenda seda." }, - "flow_title": "BleBoxi seade: {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/blebox/translations/no.json b/homeassistant/components/blebox/translations/no.json index cf46945950f..3ab5987eba7 100644 --- a/homeassistant/components/blebox/translations/no.json +++ b/homeassistant/components/blebox/translations/no.json @@ -9,7 +9,7 @@ "unknown": "Uventet feil", "unsupported_version": "BleBox-enheten har utdatert fastvare. Vennligst oppgrader den f\u00f8rst." }, - "flow_title": "BleBox-enhet: {name} ({host})", + "flow_title": "{name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/blebox/translations/ru.json b/homeassistant/components/blebox/translations/ru.json index 4fd361021eb..b230c4e9974 100644 --- a/homeassistant/components/blebox/translations/ru.json +++ b/homeassistant/components/blebox/translations/ru.json @@ -9,7 +9,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "unsupported_version": "\u041f\u0440\u043e\u0448\u0438\u0432\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0435\u0451." }, - "flow_title": "BleBox device: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/blebox/translations/zh-Hant.json b/homeassistant/components/blebox/translations/zh-Hant.json index a763442db7d..612596882f4 100644 --- a/homeassistant/components/blebox/translations/zh-Hant.json +++ b/homeassistant/components/blebox/translations/zh-Hant.json @@ -9,7 +9,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4", "unsupported_version": "BleBox \u88dd\u7f6e\u97cc\u9ad4\u904e\u820a\uff0c\u8acb\u5148\u9032\u884c\u66f4\u65b0\u3002" }, - "flow_title": "BleBox \u88dd\u7f6e\uff1a{name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/bond/translations/ca.json b/homeassistant/components/bond/translations/ca.json index 1d1df915630..f2ef0761e24 100644 --- a/homeassistant/components/bond/translations/ca.json +++ b/homeassistant/components/bond/translations/ca.json @@ -9,7 +9,7 @@ "old_firmware": "Hi ha un programari antic i no compatible al dispositiu Bond - actualitza'l abans de continuar", "unknown": "Error inesperat" }, - "flow_title": "Bond: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bond/translations/et.json b/homeassistant/components/bond/translations/et.json index 5e9a8e4493f..7ebf173f8a7 100644 --- a/homeassistant/components/bond/translations/et.json +++ b/homeassistant/components/bond/translations/et.json @@ -9,7 +9,7 @@ "old_firmware": "Bondi seadme ei toeta vana p\u00fcsivara - uuenda enne j\u00e4tkamist", "unknown": "Tundmatu viga" }, - "flow_title": "Bond: {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bond/translations/no.json b/homeassistant/components/bond/translations/no.json index c09b7a17635..da09fe246eb 100644 --- a/homeassistant/components/bond/translations/no.json +++ b/homeassistant/components/bond/translations/no.json @@ -9,7 +9,7 @@ "old_firmware": "Gammel fastvare som ikke st\u00f8ttes p\u00e5 Bond-enheten \u2013 vennligst oppgrader f\u00f8r du fortsetter", "unknown": "Uventet feil" }, - "flow_title": "Obligasjon: {name} ({host})", + "flow_title": "{name} ( {host} )", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bond/translations/ru.json b/homeassistant/components/bond/translations/ru.json index cdc37fc27f7..2fd0886d420 100644 --- a/homeassistant/components/bond/translations/ru.json +++ b/homeassistant/components/bond/translations/ru.json @@ -9,7 +9,7 @@ "old_firmware": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430 \u0438 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Bond: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bond/translations/zh-Hant.json b/homeassistant/components/bond/translations/zh-Hant.json index de54be7fff3..1c2a03a7bbe 100644 --- a/homeassistant/components/bond/translations/zh-Hant.json +++ b/homeassistant/components/bond/translations/zh-Hant.json @@ -9,7 +9,7 @@ "old_firmware": "Bond \u88dd\u7f6e\u4f7f\u7528\u4e0d\u652f\u63f4\u7684\u820a\u7248\u672c\u97cc\u9ad4 - \u8acb\u66f4\u65b0\u5f8c\u518d\u7e7c\u7e8c", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "Bond\uff1a{name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/brother/translations/ca.json b/homeassistant/components/brother/translations/ca.json index 7d90fd8510d..689495478bb 100644 --- a/homeassistant/components/brother/translations/ca.json +++ b/homeassistant/components/brother/translations/ca.json @@ -9,7 +9,7 @@ "snmp_error": "El servidor SNMP s'ha tancat o la impressora no \u00e9s compatible.", "wrong_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids." }, - "flow_title": "Impressora Brother: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/brother/translations/et.json b/homeassistant/components/brother/translations/et.json index 7b2b7c1b4a5..1115e5cb3ca 100644 --- a/homeassistant/components/brother/translations/et.json +++ b/homeassistant/components/brother/translations/et.json @@ -9,7 +9,7 @@ "snmp_error": "SNMP-server on v\u00e4lja l\u00fclitatud v\u00f5i printerit ei toetata.", "wrong_host": "Sobimatu hostinimi v\u00f5i IP-aadress." }, - "flow_title": "Brotheri printer: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/brother/translations/no.json b/homeassistant/components/brother/translations/no.json index 923d1afe68d..9d3618cad62 100644 --- a/homeassistant/components/brother/translations/no.json +++ b/homeassistant/components/brother/translations/no.json @@ -9,7 +9,7 @@ "snmp_error": "SNMP verten er skrudd av eller printeren er ikke st\u00f8ttet.", "wrong_host": "Ugyldig vertsnavn eller IP-adresse." }, - "flow_title": "Brother skriver: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/brother/translations/ru.json b/homeassistant/components/brother/translations/ru.json index 6c90cd374a8..6fd15e30ac3 100644 --- a/homeassistant/components/brother/translations/ru.json +++ b/homeassistant/components/brother/translations/ru.json @@ -9,7 +9,7 @@ "snmp_error": "\u0421\u0435\u0440\u0432\u0435\u0440 SNMP \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "wrong_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, - "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Brother: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json index 80555f52e8d..88bd481749f 100644 --- a/homeassistant/components/brother/translations/zh-Hant.json +++ b/homeassistant/components/brother/translations/zh-Hant.json @@ -9,7 +9,7 @@ "snmp_error": "SNMP \u4f3a\u670d\u5668\u70ba\u95dc\u9589\u72c0\u614b\u6216\u5370\u8868\u6a5f\u4e0d\u652f\u63f4\u3002", "wrong_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740" }, - "flow_title": "Brother \u5370\u8868\u6a5f\uff1a{model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/ca.json b/homeassistant/components/bsblan/translations/ca.json index e217787ba19..8a9e3f3e533 100644 --- a/homeassistant/components/bsblan/translations/ca.json +++ b/homeassistant/components/bsblan/translations/ca.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/et.json b/homeassistant/components/bsblan/translations/et.json index 22ff91e7e1b..e0243261271 100644 --- a/homeassistant/components/bsblan/translations/et.json +++ b/homeassistant/components/bsblan/translations/et.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/no.json b/homeassistant/components/bsblan/translations/no.json index 40981e2b77c..043477ed3f9 100644 --- a/homeassistant/components/bsblan/translations/no.json +++ b/homeassistant/components/bsblan/translations/no.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/ru.json b/homeassistant/components/bsblan/translations/ru.json index 8291a20d307..e52a249a493 100644 --- a/homeassistant/components/bsblan/translations/ru.json +++ b/homeassistant/components/bsblan/translations/ru.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/zh-Hant.json b/homeassistant/components/bsblan/translations/zh-Hant.json index ebe0ca62370..54a6a8067cf 100644 --- a/homeassistant/components/bsblan/translations/zh-Hant.json +++ b/homeassistant/components/bsblan/translations/zh-Hant.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "BSB-Lan\uff1a{name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/buienradar/translations/de.json b/homeassistant/components/buienradar/translations/de.json index 4fbb298d38c..72f1ebfed3c 100644 --- a/homeassistant/components/buienradar/translations/de.json +++ b/homeassistant/components/buienradar/translations/de.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "L\u00e4ndercode des Landes, in dem Kamerabilder angezeigt werden sollen.", + "delta": "Zeitintervall in Sekunden zwischen Kamerabildaktualisierungen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/canary/translations/ca.json b/homeassistant/components/canary/translations/ca.json index c4b80d7537a..399f1d8f286 100644 --- a/homeassistant/components/canary/translations/ca.json +++ b/homeassistant/components/canary/translations/ca.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/canary/translations/et.json b/homeassistant/components/canary/translations/et.json index 1d30b9efe9e..09f46ff20bb 100644 --- a/homeassistant/components/canary/translations/et.json +++ b/homeassistant/components/canary/translations/et.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "Canary {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/canary/translations/no.json b/homeassistant/components/canary/translations/no.json index 0c60c44d4ab..c092db02793 100644 --- a/homeassistant/components/canary/translations/no.json +++ b/homeassistant/components/canary/translations/no.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/canary/translations/ru.json b/homeassistant/components/canary/translations/ru.json index 51052d0d68d..6509ab99cd6 100644 --- a/homeassistant/components/canary/translations/ru.json +++ b/homeassistant/components/canary/translations/ru.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/canary/translations/zh-Hant.json b/homeassistant/components/canary/translations/zh-Hant.json index c53ffd83279..6c7dbea4daa 100644 --- a/homeassistant/components/canary/translations/zh-Hant.json +++ b/homeassistant/components/canary/translations/zh-Hant.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "Canary\uff1a{name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/cast/translations/bg.json b/homeassistant/components/cast/translations/bg.json index 92a840cc5af..0ab9d863eff 100644 --- a/homeassistant/components/cast/translations/bg.json +++ b/homeassistant/components/cast/translations/bg.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\u0412 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u043d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 Google Cast \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "single_instance_allowed": "\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 Google Cast." }, "step": { diff --git a/homeassistant/components/cast/translations/ca.json b/homeassistant/components/cast/translations/ca.json index 944c3c043d5..7d87fffb8eb 100644 --- a/homeassistant/components/cast/translations/ca.json +++ b/homeassistant/components/cast/translations/ca.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "No s'han trobat dispositius a la xarxa", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { @@ -39,14 +38,6 @@ }, "description": "Amfitrions coneguts - Llista, separada per comes, dels noms d'amfitri\u00f3 o adreces IP dels dispositius Cast. Utilitza-ho si el descobriment mDNS no funciona.", "title": "Configuraci\u00f3 de Google Cast" - }, - "options": { - "data": { - "ignore_cec": "Llista opcional que es passar\u00e0 a pychromecast.IGNORE_CEC.", - "known_hosts": "Llista opcional d'amfitrions coneguts per si el descobriment mDNS deixa de funcionar.", - "uuid": "Llista opcional d'UUIDs. No s'afegiran 'casts' que no siguin a la llista." - }, - "description": "Introdueix la configuraci\u00f3 de Google Cast." } } } diff --git a/homeassistant/components/cast/translations/cs.json b/homeassistant/components/cast/translations/cs.json index d3f0e37a132..f04465341e3 100644 --- a/homeassistant/components/cast/translations/cs.json +++ b/homeassistant/components/cast/translations/cs.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, "step": { diff --git a/homeassistant/components/cast/translations/da.json b/homeassistant/components/cast/translations/da.json index fe6ed03bb5a..6dc6f552c8c 100644 --- a/homeassistant/components/cast/translations/da.json +++ b/homeassistant/components/cast/translations/da.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Ingen Google Cast enheder kunne findes p\u00e5 netv\u00e6rket.", "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Google Cast" }, "step": { diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index e9821bbe937..2093306a508 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden.", "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich." }, "error": { @@ -25,13 +24,19 @@ "invalid_known_hosts": "Bekannte Hosts m\u00fcssen eine durch Kommata getrennte Liste von Hosts sein." }, "step": { - "options": { + "advanced_options": { "data": { - "ignore_cec": "Optionale Liste, die an pychromecast.IGNORE_CEC \u00fcbergeben wird.", - "known_hosts": "Optionale Liste bekannter Hosts, wenn die mDNS-Erkennung nicht funktioniert.", - "uuid": "Optionale Liste der UUIDs. Casts, die nicht aufgef\u00fchrt sind, werden nicht hinzugef\u00fcgt." + "ignore_cec": "CEC ignorieren", + "uuid": "Zul\u00e4ssige UUIDs" }, - "description": "Bitte die Google Cast-Konfiguration eingeben." + "title": "Erweiterte Google Cast-Konfiguration" + }, + "basic_options": { + "data": { + "known_hosts": "Bekannte Hosts" + }, + "description": "Bekannte Hosts - Eine durch Kommas getrennte Liste von Hostnamen oder IP-Adressen von Cast-Ger\u00e4ten, die verwendet wird, wenn die mDNS-Erkennung nicht funktioniert.", + "title": "Google Cast-Konfiguration" } } } diff --git a/homeassistant/components/cast/translations/en.json b/homeassistant/components/cast/translations/en.json index b61c5eee99e..32d22a147b3 100644 --- a/homeassistant/components/cast/translations/en.json +++ b/homeassistant/components/cast/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { @@ -39,14 +38,6 @@ }, "description": "Known Hosts - A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.", "title": "Google Cast configuration" - }, - "options": { - "data": { - "ignore_cec": "Optional list which will be passed to pychromecast.IGNORE_CEC.", - "known_hosts": "Optional list of known hosts if mDNS discovery is not working.", - "uuid": "Optional list of UUIDs. Casts not listed will not be added." - }, - "description": "Please enter the Google Cast configuration." } } } diff --git a/homeassistant/components/cast/translations/es-419.json b/homeassistant/components/cast/translations/es-419.json index ee30ef16b46..fd893b9680f 100644 --- a/homeassistant/components/cast/translations/es-419.json +++ b/homeassistant/components/cast/translations/es-419.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "No se encontraron dispositivos Google Cast en la red.", "single_instance_allowed": "S\u00f3lo es necesaria una \u00fanica configuraci\u00f3n de Google Cast." }, "step": { @@ -9,15 +8,5 @@ "description": "\u00bfDesea configurar Google Cast?" } } - }, - "options": { - "step": { - "options": { - "data": { - "ignore_cec": "Lista opcional que se pasar\u00e1 a pychromecast.IGNORE_CEC.", - "uuid": "Lista opcional de UUID. No se agregar\u00e1n los dispositivos Cast que no est\u00e9n listados." - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index 17b0ff4c2c4..358bb5af6eb 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "No se encontraron dispositivos en la red", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { @@ -23,16 +22,6 @@ "options": { "error": { "invalid_known_hosts": "Los hosts conocidos deben ser una lista de hosts separados por comas." - }, - "step": { - "options": { - "data": { - "ignore_cec": "Lista opcional que se pasar\u00e1 a pychromecast.IGNORE_CEC.", - "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona.", - "uuid": "Lista opcional de UUIDs. Los cast que no aparezcan en la lista no se a\u00f1adir\u00e1n." - }, - "description": "Introduce la configuraci\u00f3n de Google Cast." - } } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/et.json b/homeassistant/components/cast/translations/et.json index 48397e044f4..427be338fea 100644 --- a/homeassistant/components/cast/translations/et.json +++ b/homeassistant/components/cast/translations/et.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi Google Casti seadet.", "single_instance_allowed": "Vajalik on ainult \u00fcks Google Casti konfiguratsioon." }, "error": { @@ -39,14 +38,6 @@ }, "description": "Tuntud hostid - komadega eraldatud loend hostitud seadmete hostinimedest v\u00f5i IP-aadressidest. Kasuta seda juhul kui mDNS-i tuvastus ei t\u00f6\u00f6ta.", "title": "Google Casti s\u00e4tted" - }, - "options": { - "data": { - "ignore_cec": "Valikuline nimekiri mis edastatakse pychromecast.IGNORE_CEC-ile.", - "known_hosts": "Valikuline loend teadaolevatest hostidest kui mDNS-i tuvastamine ei t\u00f6\u00f6ta.", - "uuid": "Valikuline UUIDide loend. Loetlemata cast-e ei lisata." - }, - "description": "Sisesta Google Casti andmed." } } } diff --git a/homeassistant/components/cast/translations/fi.json b/homeassistant/components/cast/translations/fi.json index 730f3ebf9b3..74cf54852a8 100644 --- a/homeassistant/components/cast/translations/fi.json +++ b/homeassistant/components/cast/translations/fi.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Verkosta ei l\u00f6ydy Google Cast -laitteita.", "single_instance_allowed": "Vain yksi Google Cast -m\u00e4\u00e4ritys on tarpeen." }, "step": { diff --git a/homeassistant/components/cast/translations/fr.json b/homeassistant/components/cast/translations/fr.json index a907fbe4a76..b5274cae453 100644 --- a/homeassistant/components/cast/translations/fr.json +++ b/homeassistant/components/cast/translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Aucun appareil Google Cast trouv\u00e9 sur le r\u00e9seau.", "single_instance_allowed": "Une seule configuration de Google Cast est n\u00e9cessaire." }, "error": { @@ -39,14 +38,6 @@ }, "description": "H\u00f4tes connus - Une liste de noms d'h\u00f4te ou d'adresses IP s\u00e9par\u00e9s par des virgules des p\u00e9riph\u00e9riques de diffusion, \u00e0 utiliser si la d\u00e9couverte mDNS ne fonctionne pas.", "title": "Configuration de Google Cast" - }, - "options": { - "data": { - "ignore_cec": "Liste facultative qui sera transmise \u00e0 pychromecast.IGNORE_CEC.", - "known_hosts": "Liste facultative des h\u00f4tes connus si la d\u00e9couverte mDNS ne fonctionne pas.", - "uuid": "Liste facultative des UUID. Les moulages non r\u00e9pertori\u00e9s ne seront pas ajout\u00e9s." - }, - "description": "Veuillez saisir la configuration de Google Cast." } } } diff --git a/homeassistant/components/cast/translations/he.json b/homeassistant/components/cast/translations/he.json index 0bc4e834cb7..09c85008e10 100644 --- a/homeassistant/components/cast/translations/he.json +++ b/homeassistant/components/cast/translations/he.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9 Google Cast \u05d1\u05e8\u05e9\u05ea.", "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Google Cast \u05e0\u05d7\u05d5\u05e6\u05d4." }, "step": { diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json index 7e5625c925d..660e598a7cb 100644 --- a/homeassistant/components/cast/translations/hu.json +++ b/homeassistant/components/cast/translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { @@ -23,15 +22,6 @@ "options": { "error": { "invalid_known_hosts": "Az ismert hosztoknak vessz\u0151vel elv\u00e1lasztott hosztok list\u00e1j\u00e1nak kell lennie." - }, - "step": { - "options": { - "data": { - "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik.", - "uuid": "Az UUID-k opcion\u00e1lis list\u00e1ja. A felsorol\u00e1sban nem szerepl\u0151 szerepl\u0151g\u00e1rd\u00e1k nem ker\u00fclnek hozz\u00e1ad\u00e1sra." - }, - "description": "K\u00e9rj\u00fck, add meg a Google Cast konfigur\u00e1ci\u00f3t." - } } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/id.json b/homeassistant/components/cast/translations/id.json index d086b388252..bd7e0c936b1 100644 --- a/homeassistant/components/cast/translations/id.json +++ b/homeassistant/components/cast/translations/id.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "error": { @@ -23,16 +22,6 @@ "options": { "error": { "invalid_known_hosts": "Host yang diketahui harus berupa daftar host yang dipisahkan koma." - }, - "step": { - "options": { - "data": { - "ignore_cec": "Daftar opsional yang akan diteruskan ke pychromecast.IGNORE_CEC.", - "known_hosts": "Daftar opsional host yang diketahui jika penemuan mDNS tidak berfungsi.", - "uuid": "Daftar opsional UUID. Cast yang tidak tercantum tidak akan ditambahkan." - }, - "description": "Masukkan konfigurasi Google Cast." - } } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/it.json b/homeassistant/components/cast/translations/it.json index c0ff9144a2f..d96bb9763c6 100644 --- a/homeassistant/components/cast/translations/it.json +++ b/homeassistant/components/cast/translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Nessun dispositivo trovato sulla rete", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { @@ -39,14 +38,6 @@ }, "description": "Host conosciuti: un elenco separato da virgole di nomi host o indirizzi IP di dispositivi di trasmissione, da utilizzare se l'individuazione di mDNS non funziona.", "title": "Configurazione di Google Cast" - }, - "options": { - "data": { - "ignore_cec": "Elenco opzionale che sar\u00e0 passato a pychromecast.IGNORE_CEC.", - "known_hosts": "Elenco facoltativo di host noti se l'individuazione di mDNS non funziona.", - "uuid": "Elenco opzionale di UUID. I cast non elencati non saranno aggiunti." - }, - "description": "Inserisci la configurazione di Google Cast." } } } diff --git a/homeassistant/components/cast/translations/ja.json b/homeassistant/components/cast/translations/ja.json index fa1d4031562..b669a6e65b8 100644 --- a/homeassistant/components/cast/translations/ja.json +++ b/homeassistant/components/cast/translations/ja.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306bGoogle Cast\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" - }, "step": { "confirm": { "description": "Google Cast\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/cast/translations/ko.json b/homeassistant/components/cast/translations/ko.json index b0bfd3271c9..2f1ee52675f 100644 --- a/homeassistant/components/cast/translations/ko.json +++ b/homeassistant/components/cast/translations/ko.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "error": { @@ -23,16 +22,6 @@ "options": { "error": { "invalid_known_hosts": "\uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\ub294 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \ud638\uc2a4\ud2b8 \ubaa9\ub85d\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4." - }, - "step": { - "options": { - "data": { - "ignore_cec": "pychromecast.IGNORE_CEC\uc5d0 \uc804\ub2ec\ub420 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4.", - "known_hosts": "mDNS \uac80\uc0c9\uc774 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0 \uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\uc758 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4.", - "uuid": "UUID\uc758 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4. \ubaa9\ub85d\uc5d0 \uc5c6\ub294 \uce90\uc2a4\ud2b8\ub294 \ucd94\uac00\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." - }, - "description": "Google Cast \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694." - } } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/lb.json b/homeassistant/components/cast/translations/lb.json index 8f572aa48ce..6b1454a6b57 100644 --- a/homeassistant/components/cast/translations/lb.json +++ b/homeassistant/components/cast/translations/lb.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Keng Apparater am Netzwierk fonnt.", "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." }, "step": { diff --git a/homeassistant/components/cast/translations/nl.json b/homeassistant/components/cast/translations/nl.json index aaf662c91f3..3928b227e5d 100644 --- a/homeassistant/components/cast/translations/nl.json +++ b/homeassistant/components/cast/translations/nl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Geen apparaten gevonden op het netwerk", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "error": { @@ -39,14 +38,6 @@ }, "description": "Bekende hosts - Een door komma's gescheiden lijst met hostnamen of IP-adressen van cast-apparaten, te gebruiken als mDNS-detectie niet werkt.", "title": "Google Cast configuratie" - }, - "options": { - "data": { - "ignore_cec": "Optionele lijst die zal worden doorgegeven aan pychromecast.IGNORE_CEC.", - "known_hosts": "Optionele lijst van bekende hosts indien mDNS discovery niet werkt.", - "uuid": "Optionele lijst van UUID's. Casts die niet in de lijst staan, worden niet toegevoegd." - }, - "description": "Voer de Google Cast configuratie in." } } } diff --git a/homeassistant/components/cast/translations/nn.json b/homeassistant/components/cast/translations/nn.json index 44d26792812..3afcfc6bc2e 100644 --- a/homeassistant/components/cast/translations/nn.json +++ b/homeassistant/components/cast/translations/nn.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Klar", "single_instance_allowed": "Du treng berre \u00e5 sette opp \u00e9in Google Cast-konfigurasjon." }, "step": { diff --git a/homeassistant/components/cast/translations/no.json b/homeassistant/components/cast/translations/no.json index 315926a84be..60fa7fae3f1 100644 --- a/homeassistant/components/cast/translations/no.json +++ b/homeassistant/components/cast/translations/no.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { @@ -39,14 +38,6 @@ }, "description": "Kjente verter - En kommaseparert liste over vertsnavn eller IP-adresser til cast-enheter, bruk hvis mDNS discovery ikke fungerer.", "title": "Google Cast-konfigurasjon" - }, - "options": { - "data": { - "ignore_cec": "Valgfri liste som sendes til pychromecast.IGNORE_CEC.", - "known_hosts": "Valgfri liste over kjente verter hvis mDNS-oppdagelse ikke fungerer.", - "uuid": "Valgfri liste over UUIDer. Medvirkende som ikke er oppf\u00f8rt, blir ikke lagt til." - }, - "description": "Angi Google Cast-konfigurasjonen." } } } diff --git a/homeassistant/components/cast/translations/pl.json b/homeassistant/components/cast/translations/pl.json index 5802bda502a..0fb732d704b 100644 --- a/homeassistant/components/cast/translations/pl.json +++ b/homeassistant/components/cast/translations/pl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { @@ -23,16 +22,6 @@ "options": { "error": { "invalid_known_hosts": "Znane hosty musz\u0105 by\u0107 list\u0105 host\u00f3w oddzielonych przecinkami." - }, - "step": { - "options": { - "data": { - "ignore_cec": "Opcjonalna lista, kt\u00f3ra zostanie przekazana do pychromecast.IGNORE_CEC.", - "known_hosts": "Opcjonalna lista znanych host\u00f3w, je\u015bli wykrywanie mDNS nie dzia\u0142a.", - "uuid": "Opcjonalna lista identyfikator\u00f3w UUID. Casty nie wymienione na li\u015bcie nie zostan\u0105 dodane." - }, - "description": "Wprowad\u017a konfiguracj\u0119 Google Cast." - } } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/pt-BR.json b/homeassistant/components/cast/translations/pt-BR.json index 000971f9e4c..8abd2dac5e5 100644 --- a/homeassistant/components/cast/translations/pt-BR.json +++ b/homeassistant/components/cast/translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo Google Cast encontrado na rede.", "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Google Cast \u00e9 necess\u00e1ria." }, "step": { diff --git a/homeassistant/components/cast/translations/pt.json b/homeassistant/components/cast/translations/pt.json index 2a5b62a9de1..bb29c923128 100644 --- a/homeassistant/components/cast/translations/pt.json +++ b/homeassistant/components/cast/translations/pt.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo Google Cast descoberto na rede.", "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Google Cast \u00e9 necess\u00e1ria." }, "step": { @@ -12,12 +11,5 @@ "description": "Deseja configurar o Google Cast?" } } - }, - "options": { - "step": { - "options": { - "description": "Por favor introduza a configura\u00e7\u00e3o do Google Cast" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/ro.json b/homeassistant/components/cast/translations/ro.json index 6e93a0fdb1a..1eb46021d98 100644 --- a/homeassistant/components/cast/translations/ro.json +++ b/homeassistant/components/cast/translations/ro.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Nu s-au g\u0103sit dispozitive Google Cast \u00een re\u021bea.", "single_instance_allowed": "Este necesar\u0103 o singur\u0103 configura\u021bie a serviciului Google Cast." }, "step": { diff --git a/homeassistant/components/cast/translations/ru.json b/homeassistant/components/cast/translations/ru.json index cb432acbf64..84fc0835de0 100644 --- a/homeassistant/components/cast/translations/ru.json +++ b/homeassistant/components/cast/translations/ru.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \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 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { @@ -39,14 +38,6 @@ }, "description": "\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b \u2014 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u043c\u0435\u043d \u0445\u043e\u0441\u0442\u043e\u0432 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 mDNS \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Google Cast" - }, - "options": { - "data": { - "ignore_cec": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d \u0432 pychromecast.IGNORE_CEC.", - "known_hosts": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0445\u043e\u0441\u0442\u043e\u0432, \u0435\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 mDNS \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", - "uuid": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a UUID. \u041d\u0435 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u043d\u044b\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043d\u0435 \u0431\u0443\u0434\u0443\u0442." - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Google Cast." } } } diff --git a/homeassistant/components/cast/translations/sl.json b/homeassistant/components/cast/translations/sl.json index c4d2ba98006..15e2b33ab30 100644 --- a/homeassistant/components/cast/translations/sl.json +++ b/homeassistant/components/cast/translations/sl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "V omre\u017eju niso najdene naprave Google Cast.", "single_instance_allowed": "Potrebna je samo ena konfiguracija Google Cast-a." }, "step": { diff --git a/homeassistant/components/cast/translations/sv.json b/homeassistant/components/cast/translations/sv.json index 982b52b65dd..0d5fb586cc8 100644 --- a/homeassistant/components/cast/translations/sv.json +++ b/homeassistant/components/cast/translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Inga Google Cast-enheter hittades i n\u00e4tverket.", "single_instance_allowed": "Endast en enda konfiguration av Google Cast \u00e4r n\u00f6dv\u00e4ndig." }, "step": { diff --git a/homeassistant/components/cast/translations/th.json b/homeassistant/components/cast/translations/th.json index 64a5eaa3085..f0b06a06dc9 100644 --- a/homeassistant/components/cast/translations/th.json +++ b/homeassistant/components/cast/translations/th.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "no_devices_found": "\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e2d\u0e38\u0e1b\u0e01\u0e23\u0e13\u0e4c Google Cast \u0e1a\u0e19\u0e40\u0e04\u0e23\u0e37\u0e2d\u0e02\u0e48\u0e32\u0e22" - }, "step": { "confirm": { "description": "\u0e04\u0e38\u0e13\u0e15\u0e49\u0e2d\u0e07\u0e01\u0e32\u0e23\u0e15\u0e31\u0e49\u0e07\u0e04\u0e48\u0e32 Google Cast \u0e2b\u0e23\u0e37\u0e2d\u0e44\u0e21\u0e48?" diff --git a/homeassistant/components/cast/translations/uk.json b/homeassistant/components/cast/translations/uk.json index 292861e9129..5f8d69f5f29 100644 --- a/homeassistant/components/cast/translations/uk.json +++ b/homeassistant/components/cast/translations/uk.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." }, "step": { diff --git a/homeassistant/components/cast/translations/vi.json b/homeassistant/components/cast/translations/vi.json index f65f3c58ebe..175f6d22886 100644 --- a/homeassistant/components/cast/translations/vi.json +++ b/homeassistant/components/cast/translations/vi.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Kh\u00f4ng t\u00ecm th\u1ea5y thi\u1ebft b\u1ecb Google Cast n\u00e0o tr\u00ean m\u1ea1ng.", "single_instance_allowed": "Ch\u1ec9 c\u1ea7n m\u1ed9t c\u1ea5u h\u00ecnh duy nh\u1ea5t c\u1ee7a Google Cast l\u00e0 \u0111\u1ee7." }, "step": { diff --git a/homeassistant/components/cast/translations/zh-Hans.json b/homeassistant/components/cast/translations/zh-Hans.json index 0feaac56440..073919a1282 100644 --- a/homeassistant/components/cast/translations/zh-Hans.json +++ b/homeassistant/components/cast/translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Google Cast \u8bbe\u5907\u3002", "single_instance_allowed": "Google Cast \u53ea\u9700\u8981\u914d\u7f6e\u4e00\u6b21\u3002" }, "step": { @@ -13,12 +12,5 @@ "description": "\u60a8\u60f3\u8981\u914d\u7f6e Google Cast \u5417\uff1f" } } - }, - "options": { - "step": { - "options": { - "description": "\u8bf7\u786e\u8ba4Goole Cast\u7684\u914d\u7f6e" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json index 947fce7a44b..7d3def31eb4 100644 --- a/homeassistant/components/cast/translations/zh-Hant.json +++ b/homeassistant/components/cast/translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { @@ -39,14 +38,6 @@ }, "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u63a2\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", "title": "Google Cast \u8a2d\u5b9a" - }, - "options": { - "data": { - "ignore_cec": "\u9078\u9805\u5217\u8868\u5c07\u50b3\u905e\u81f3 pychromecast.IGNORE_CEC\u3002", - "known_hosts": "\u5047\u5982 mDNS \u63a2\u7d22\u7121\u6cd5\u4f5c\u7528\uff0c\u5247\u70ba\u5df2\u77e5\u4e3b\u6a5f\u7684\u9078\u9805\u5217\u8868\u3002", - "uuid": "UUID \u9078\u9805\u5217\u8868\u3002\u672a\u5217\u51fa\u7684 Cast \u88dd\u7f6e\u5c07\u4e0d\u6703\u9032\u884c\u65b0\u589e\u3002" - }, - "description": "\u8acb\u8f38\u5165 Google Cast \u8a2d\u5b9a\u3002" } } } diff --git a/homeassistant/components/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json index 3f215b63234..e7b04018934 100644 --- a/homeassistant/components/climacell/translations/ca.json +++ b/homeassistant/components/climacell/translations/ca.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Tipus de previsi\u00f3", "timestep": "Minuts entre previsions NowCast" }, "description": "Si decideixes activar l'entitat de predicci\u00f3 \"nowcast\", podr\u00e0s configurar l'interval en minuts entre cada previsi\u00f3. El nombre de previsions proporcionades dep\u00e8n d'aquest interval de minuts.", diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json index e53b96d8e73..8e269db785b 100644 --- a/homeassistant/components/climacell/translations/de.json +++ b/homeassistant/components/climacell/translations/de.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Vorhersage Arten", "timestep": "Minuten zwischen den Kurzvorhersagen" }, "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.", diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json index 45ed5d8a722..91f38277ae3 100644 --- a/homeassistant/components/climacell/translations/el.json +++ b/homeassistant/components/climacell/translations/el.json @@ -19,7 +19,6 @@ "step": { "init": { "data": { - "forecast_types": "\u0395\u03af\u03b4\u03bf\u03c2/\u03b7 \u0394\u03b5\u03bb\u03c4\u03af\u03bf\u03c5/\u03c9\u03bd", "timestep": "\u039b\u03b5\u03c0\u03c4\u03ac \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd NowCast" }, "description": "\u0395\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd 'nowcast', \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03ba\u03ac\u03b8\u03b5 \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b5\u03be\u03b1\u03c1\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd.", diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json index c126cf170b1..3e5cd436ba8 100644 --- a/homeassistant/components/climacell/translations/en.json +++ b/homeassistant/components/climacell/translations/en.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Forecast Type(s)", "timestep": "Min. Between NowCast Forecasts" }, "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json index ec3bfd15967..4e709f03ad1 100644 --- a/homeassistant/components/climacell/translations/es.json +++ b/homeassistant/components/climacell/translations/es.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Tipo(s) de pron\u00f3stico", "timestep": "Min. Entre pron\u00f3sticos de NowCast" }, "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.", diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json index 4e9cec722ef..46ac184fa3c 100644 --- a/homeassistant/components/climacell/translations/et.json +++ b/homeassistant/components/climacell/translations/et.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Prognoosi t\u00fc\u00fcp (t\u00fc\u00fcbid)", "timestep": "Minuteid NowCasti prognooside vahel" }, "description": "Kui otsustad lubada \"nowcast\" prognoosi\u00fcksuse, saad seadistada minutite arvu iga prognoosi vahel. Esitatavate prognooside arv s\u00f5ltub prognooside vahel valitud minutite arvust.", diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json index c0e8d5b88a4..89e3f43be56 100644 --- a/homeassistant/components/climacell/translations/fr.json +++ b/homeassistant/components/climacell/translations/fr.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Type(s) de pr\u00e9vision", "timestep": "Min. Entre les pr\u00e9visions NowCast" }, "description": "Si vous choisissez d'activer l'entit\u00e9 de pr\u00e9vision \u00abnowcast\u00bb, vous pouvez configurer le nombre de minutes entre chaque pr\u00e9vision. Le nombre de pr\u00e9visions fournies d\u00e9pend du nombre de minutes choisies entre les pr\u00e9visions.", diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json index b9f8c4ea981..88b377261bb 100644 --- a/homeassistant/components/climacell/translations/id.json +++ b/homeassistant/components/climacell/translations/id.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Jenis Prakiraan", "timestep": "Jarak Interval Prakiraan NowCast dalam Menit" }, "description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan 'nowcast', Anda dapat mengonfigurasi jarak interval prakiraan dalam menit. Jumlah prakiraan yang diberikan tergantung pada nilai interval yang dipilih.", diff --git a/homeassistant/components/climacell/translations/it.json b/homeassistant/components/climacell/translations/it.json index bbd8e33d305..bd1bdd88238 100644 --- a/homeassistant/components/climacell/translations/it.json +++ b/homeassistant/components/climacell/translations/it.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Tipo(i) di previsione", "timestep": "Minuti tra le previsioni di NowCast" }, "description": "Se scegli di abilitare l'entit\u00e0 di previsione `nowcast`, puoi configurare il numero di minuti tra ogni previsione. Il numero di previsioni fornite dipende dal numero di minuti scelti tra le previsioni.", diff --git a/homeassistant/components/climacell/translations/ko.json b/homeassistant/components/climacell/translations/ko.json index 901fd429b1a..b5936bbc7d7 100644 --- a/homeassistant/components/climacell/translations/ko.json +++ b/homeassistant/components/climacell/translations/ko.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "\uc77c\uae30\uc608\ubcf4 \uc720\ud615", "timestep": "\ub2e8\uae30\uc608\uce21 \uc77c\uae30\uc608\ubcf4 \uac04 \ucd5c\uc18c \uc2dc\uac04" }, "description": "`nowcast` \uc77c\uae30\uc608\ubcf4 \uad6c\uc131\uc694\uc18c\ub97c \uc0ac\uc6a9\ud558\ub3c4\ub85d \uc120\ud0dd\ud55c \uacbd\uc6b0 \uac01 \uc77c\uae30\uc608\ubcf4 \uc0ac\uc774\uc758 \uc2dc\uac04(\ubd84)\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc81c\uacf5\ub41c \uc77c\uae30\uc608\ubcf4 \ud69f\uc218\ub294 \uc608\uce21 \uac04 \uc120\ud0dd\ud55c \uc2dc\uac04(\ubd84)\uc5d0 \ub530\ub77c \ub2ec\ub77c\uc9d1\ub2c8\ub2e4.", diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json index 925300c089d..a8754e81943 100644 --- a/homeassistant/components/climacell/translations/nl.json +++ b/homeassistant/components/climacell/translations/nl.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Voorspellingstype(n)", "timestep": "Min. Tussen NowCast-voorspellingen" }, "description": "Als u ervoor kiest om de `nowcast` voorspellingsentiteit in te schakelen, kan u het aantal minuten tussen elke voorspelling configureren. Het aantal voorspellingen hangt af van het aantal gekozen minuten tussen de voorspellingen.", diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json index 2aad7900607..7a821bcccbb 100644 --- a/homeassistant/components/climacell/translations/no.json +++ b/homeassistant/components/climacell/translations/no.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Prognosetype(r)", "timestep": "Min. mellom NowCast prognoser" }, "description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselenheten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.", diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json index 6c8bad0f57a..dc08cb9b1bf 100644 --- a/homeassistant/components/climacell/translations/pl.json +++ b/homeassistant/components/climacell/translations/pl.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "Typ(y) prognozy", "timestep": "Minuty pomi\u0119dzy prognozami" }, "description": "Je\u015bli zdecydujesz si\u0119 w\u0142\u0105czy\u0107 encj\u0119 prognozy \u201enowcast\u201d, mo\u017cesz skonfigurowa\u0107 liczb\u0119 minut mi\u0119dzy ka\u017cd\u0105 prognoz\u0105. Liczba dostarczonych prognoz zale\u017cy od liczby minut wybranych mi\u0119dzy prognozami.", diff --git a/homeassistant/components/climacell/translations/ru.json b/homeassistant/components/climacell/translations/ru.json index 7e40c619112..0f1a80b5e09 100644 --- a/homeassistant/components/climacell/translations/ru.json +++ b/homeassistant/components/climacell/translations/ru.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "\u0422\u0438\u043f(\u044b) \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430", "timestep": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" }, "description": "\u0415\u0441\u043b\u0438 \u0412\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 'nowcast', \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430.", diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json index 710759b954c..64b8e90b6ea 100644 --- a/homeassistant/components/climacell/translations/zh-Hant.json +++ b/homeassistant/components/climacell/translations/zh-Hant.json @@ -23,7 +23,6 @@ "step": { "init": { "data": { - "forecast_types": "\u9810\u5831\u985e\u578b", "timestep": "NowCast \u9810\u5831\u9593\u9694\u5206\u9418" }, "description": "\u5047\u5982\u9078\u64c7\u958b\u555f `nowcast` \u9810\u5831\u5be6\u9ad4\u3001\u5c07\u53ef\u4ee5\u8a2d\u5b9a\u9810\u5831\u983b\u7387\u9593\u9694\u5206\u9418\u6578\u3002\u6839\u64da\u6240\u8f38\u5165\u7684\u9593\u9694\u6642\u9593\u5c07\u6c7a\u5b9a\u9810\u5831\u7684\u6578\u76ee\u3002", diff --git a/homeassistant/components/cloudflare/translations/ca.json b/homeassistant/components/cloudflare/translations/ca.json index edd31662e56..d0ffdcd5429 100644 --- a/homeassistant/components/cloudflare/translations/ca.json +++ b/homeassistant/components/cloudflare/translations/ca.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_zone": "Zona inv\u00e0lida" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/cloudflare/translations/et.json b/homeassistant/components/cloudflare/translations/et.json index 1f4d91c71d7..1688372fa1e 100644 --- a/homeassistant/components/cloudflare/translations/et.json +++ b/homeassistant/components/cloudflare/translations/et.json @@ -9,7 +9,7 @@ "invalid_auth": "Tuvastamise viga", "invalid_zone": "Sobimatu ala" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/cloudflare/translations/no.json b/homeassistant/components/cloudflare/translations/no.json index 33e4ca61f78..4c99154a4e3 100644 --- a/homeassistant/components/cloudflare/translations/no.json +++ b/homeassistant/components/cloudflare/translations/no.json @@ -9,7 +9,7 @@ "invalid_auth": "Ugyldig godkjenning", "invalid_zone": "Ugyldig sone" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/cloudflare/translations/ru.json b/homeassistant/components/cloudflare/translations/ru.json index 7c397faa37e..2b8eb0b140a 100644 --- a/homeassistant/components/cloudflare/translations/ru.json +++ b/homeassistant/components/cloudflare/translations/ru.json @@ -9,7 +9,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_zone": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0437\u043e\u043d\u0430" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/cloudflare/translations/zh-Hant.json b/homeassistant/components/cloudflare/translations/zh-Hant.json index d9a05269748..da11b44ea8e 100644 --- a/homeassistant/components/cloudflare/translations/zh-Hant.json +++ b/homeassistant/components/cloudflare/translations/zh-Hant.json @@ -9,7 +9,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_zone": "\u5340\u57df\u7121\u6548" }, - "flow_title": "Cloudflare\uff1a{name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index 60d91a83db8..2f839b209eb 100644 --- a/homeassistant/components/deconz/translations/ca.json +++ b/homeassistant/components/deconz/translations/ca.json @@ -11,7 +11,7 @@ "error": { "no_key": "No s'ha pogut obtenir una clau API" }, - "flow_title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee ({host})", + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb la passarel\u00b7la deCONZ proporcionada pel complement {addon}?", diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json index e52b54166a1..6be6a7fbdc6 100644 --- a/homeassistant/components/deconz/translations/et.json +++ b/homeassistant/components/deconz/translations/et.json @@ -11,7 +11,7 @@ "error": { "no_key": "API v\u00f5tit ei leitud" }, - "flow_title": "deCONZ Zigbee l\u00fc\u00fcs ( {host} )", + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse deCONZ-l\u00fc\u00fcsiga, mida pakub lisandmoodul {addon} ?", diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index f27e7235f40..06c03b8b585 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -11,7 +11,7 @@ "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel" }, - "flow_title": "", + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "Vil du konfigurere Home Assistant til \u00e5 koble til deCONZ gateway levert av tillegget {addon} ?", diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index de97d799381..412c12198f3 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -11,7 +11,7 @@ "error": { "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." }, - "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", + "flow_title": "{host}", "step": { "hassio_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 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index a80afaf4695..b93cb320993 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -11,7 +11,7 @@ "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" }, - "flow_title": "deCONZ Zigbee \u9598\u9053\u5668\uff08{host}\uff09", + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 deCONZ \u9598\u9053\u5668\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", diff --git a/homeassistant/components/denonavr/translations/ca.json b/homeassistant/components/denonavr/translations/ca.json index 3f0c846e10f..d74705086a2 100644 --- a/homeassistant/components/denonavr/translations/ca.json +++ b/homeassistant/components/denonavr/translations/ca.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "No s'ha pogut descobrir un receptor de xarxa AVR de Denon" }, - "flow_title": "Receptor de xarxa AVR de Denon: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Confirma l'addici\u00f3 del receptor", diff --git a/homeassistant/components/denonavr/translations/et.json b/homeassistant/components/denonavr/translations/et.json index edba2158e69..5dc9f3cc771 100644 --- a/homeassistant/components/denonavr/translations/et.json +++ b/homeassistant/components/denonavr/translations/et.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Denon AVR Network Receiver'i avastamine nurjus" }, - "flow_title": "Denon AVR v\u00f5rguvastuv\u00f5tja: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Palun kinnita vastuv\u00f5tja lisamine", diff --git a/homeassistant/components/denonavr/translations/no.json b/homeassistant/components/denonavr/translations/no.json index c2cac347e77..646e24d6031 100644 --- a/homeassistant/components/denonavr/translations/no.json +++ b/homeassistant/components/denonavr/translations/no.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Kunne ikke oppdage en Denon AVR Network Receiver" }, - "flow_title": "Denon AVR nettverksmottaker: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Bekreft at du legger til mottakeren", diff --git a/homeassistant/components/denonavr/translations/ru.json b/homeassistant/components/denonavr/translations/ru.json index 6a3397023d3..c1fb25a9889 100644 --- a/homeassistant/components/denonavr/translations/ru.json +++ b/homeassistant/components/denonavr/translations/ru.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0440\u0435\u0441\u0438\u0432\u0435\u0440 Denon." }, - "flow_title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440\u0430", diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index 96bf7b00f92..a8ee7f87fd8 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "\u7121\u6cd5\u627e\u5230 Denon AVR \u7db2\u8def\u63a5\u6536\u5668" }, - "flow_title": "Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff1a{name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u8acb\u78ba\u8a8d\u65b0\u589e\u63a5\u6536\u5668", diff --git a/homeassistant/components/devolo_home_control/translations/ca.json b/homeassistant/components/devolo_home_control/translations/ca.json index 57ca0b7c209..a41c1f78c15 100644 --- a/homeassistant/components/devolo_home_control/translations/ca.json +++ b/homeassistant/components/devolo_home_control/translations/ca.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "URL de Home Control", "mydevolo_url": "URL de mydevolo", "password": "Contrasenya", "username": "Correu electr\u00f2nic / ID de devolo" diff --git a/homeassistant/components/devolo_home_control/translations/cs.json b/homeassistant/components/devolo_home_control/translations/cs.json index f41c2dc218f..44906c51207 100644 --- a/homeassistant/components/devolo_home_control/translations/cs.json +++ b/homeassistant/components/devolo_home_control/translations/cs.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Heslo", "username": "E-mail / Devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/de.json b/homeassistant/components/devolo_home_control/translations/de.json index 251d058b42e..c34ecdb5c34 100644 --- a/homeassistant/components/devolo_home_control/translations/de.json +++ b/homeassistant/components/devolo_home_control/translations/de.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Passwort", "username": "E-Mail / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/en.json b/homeassistant/components/devolo_home_control/translations/en.json index e358e47ef0b..d1b8645072f 100644 --- a/homeassistant/components/devolo_home_control/translations/en.json +++ b/homeassistant/components/devolo_home_control/translations/en.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Password", "username": "Email / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/es.json b/homeassistant/components/devolo_home_control/translations/es.json index 713f5a53d73..fe862c1c01d 100644 --- a/homeassistant/components/devolo_home_control/translations/es.json +++ b/homeassistant/components/devolo_home_control/translations/es.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "URL de Home Control", "mydevolo_url": "URL de mydevolo", "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico / ID de devolo" diff --git a/homeassistant/components/devolo_home_control/translations/et.json b/homeassistant/components/devolo_home_control/translations/et.json index f781e4b4042..75d332456e5 100644 --- a/homeassistant/components/devolo_home_control/translations/et.json +++ b/homeassistant/components/devolo_home_control/translations/et.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control'i URL", "mydevolo_url": "mydevolo URL", "password": "Salas\u00f5na", "username": "E-post / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/fi.json b/homeassistant/components/devolo_home_control/translations/fi.json index 51dc72c408a..2bf76d9168d 100644 --- a/homeassistant/components/devolo_home_control/translations/fi.json +++ b/homeassistant/components/devolo_home_control/translations/fi.json @@ -3,7 +3,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Salasana" } diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index 13354e9da76..d0f806c042e 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "URL Home Control", "mydevolo_url": "URL mydevolo", "password": "Mot de passe", "username": "Adresse e-mail / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/hu.json b/homeassistant/components/devolo_home_control/translations/hu.json index 45b07f0adcb..4fa10a2a088 100644 --- a/homeassistant/components/devolo_home_control/translations/hu.json +++ b/homeassistant/components/devolo_home_control/translations/hu.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Jelsz\u00f3", "username": "E-mail / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/id.json b/homeassistant/components/devolo_home_control/translations/id.json index 8b7ce0171d5..31f0f87dc00 100644 --- a/homeassistant/components/devolo_home_control/translations/id.json +++ b/homeassistant/components/devolo_home_control/translations/id.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "URL Home Control", "mydevolo_url": "URL mydevolo", "password": "Kata Sandi", "username": "Email/ID devolo" diff --git a/homeassistant/components/devolo_home_control/translations/it.json b/homeassistant/components/devolo_home_control/translations/it.json index a0cba314ea6..bb19fde73a9 100644 --- a/homeassistant/components/devolo_home_control/translations/it.json +++ b/homeassistant/components/devolo_home_control/translations/it.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "URL di Home Control", "mydevolo_url": "URL di mydevolo", "password": "Password", "username": "E-mail / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/ko.json b/homeassistant/components/devolo_home_control/translations/ko.json index f21122bff70..9c9a21182cc 100644 --- a/homeassistant/components/devolo_home_control/translations/ko.json +++ b/homeassistant/components/devolo_home_control/translations/ko.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL \uc8fc\uc18c", "mydevolo_url": "mydevolo URL \uc8fc\uc18c", "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc774\uba54\uc77c / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/lb.json b/homeassistant/components/devolo_home_control/translations/lb.json index 3943dbd1d5b..56f8362fca7 100644 --- a/homeassistant/components/devolo_home_control/translations/lb.json +++ b/homeassistant/components/devolo_home_control/translations/lb.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Passwuert", "username": "E-Mail / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/nl.json b/homeassistant/components/devolo_home_control/translations/nl.json index 0ae5696a23a..0ca81ba7911 100644 --- a/homeassistant/components/devolo_home_control/translations/nl.json +++ b/homeassistant/components/devolo_home_control/translations/nl.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Wachtwoord", "username": "E-mail adres / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json index 3076e4679e0..984d279257e 100644 --- a/homeassistant/components/devolo_home_control/translations/no.json +++ b/homeassistant/components/devolo_home_control/translations/no.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Passord", "username": "E-post / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/pl.json b/homeassistant/components/devolo_home_control/translations/pl.json index e07f41deb6d..388ea692ad4 100644 --- a/homeassistant/components/devolo_home_control/translations/pl.json +++ b/homeassistant/components/devolo_home_control/translations/pl.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "URL Home Control", "mydevolo_url": "URL mydevolo", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika/identyfikator devolo" diff --git a/homeassistant/components/devolo_home_control/translations/pt.json b/homeassistant/components/devolo_home_control/translations/pt.json index ca6b9a6542c..2215d148b7b 100644 --- a/homeassistant/components/devolo_home_control/translations/pt.json +++ b/homeassistant/components/devolo_home_control/translations/pt.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control [VOID]", "mydevolo_url": "mydevolo [VOID]", "password": "Palavra-passe", "username": "Email / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/ru.json b/homeassistant/components/devolo_home_control/translations/ru.json index 66293556e7c..7334ba2ad38 100644 --- a/homeassistant/components/devolo_home_control/translations/ru.json +++ b/homeassistant/components/devolo_home_control/translations/ru.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL-\u0430\u0434\u0440\u0435\u0441", "mydevolo_url": "mydevolo URL-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/sv.json b/homeassistant/components/devolo_home_control/translations/sv.json index 48cd7428a78..4479e25b250 100644 --- a/homeassistant/components/devolo_home_control/translations/sv.json +++ b/homeassistant/components/devolo_home_control/translations/sv.json @@ -3,7 +3,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "L\u00f6senord", "username": "E-postadress / devolo-ID" diff --git a/homeassistant/components/devolo_home_control/translations/uk.json b/homeassistant/components/devolo_home_control/translations/uk.json index d230d1918f5..d9546a36eb1 100644 --- a/homeassistant/components/devolo_home_control/translations/uk.json +++ b/homeassistant/components/devolo_home_control/translations/uk.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL-\u0430\u0434\u0440\u0435\u0441\u0430", "mydevolo_url": "mydevolo URL-\u0430\u0434\u0440\u0435\u0441\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/zh-Hant.json b/homeassistant/components/devolo_home_control/translations/zh-Hant.json index b855480da9e..48aa0a9be2e 100644 --- a/homeassistant/components/devolo_home_control/translations/zh-Hant.json +++ b/homeassistant/components/devolo_home_control/translations/zh-Hant.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "home_control_url": "Home Control \u7db2\u5740", "mydevolo_url": "mydevolo \u7db2\u5740", "password": "\u5bc6\u78bc", "username": "\u96fb\u5b50\u90f5\u4ef6 / devolo ID" diff --git a/homeassistant/components/directv/translations/ca.json b/homeassistant/components/directv/translations/ca.json index 57db4ee0030..db5e9386d05 100644 --- a/homeassistant/components/directv/translations/ca.json +++ b/homeassistant/components/directv/translations/ca.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Vols configurar {name}?" diff --git a/homeassistant/components/directv/translations/en.json b/homeassistant/components/directv/translations/en.json index 118c693c891..9e921d98112 100644 --- a/homeassistant/components/directv/translations/en.json +++ b/homeassistant/components/directv/translations/en.json @@ -10,7 +10,6 @@ "flow_title": "{name}", "step": { "ssdp_confirm": { - "data": {}, "description": "Do you want to set up {name}?" }, "user": { diff --git a/homeassistant/components/directv/translations/et.json b/homeassistant/components/directv/translations/et.json index 67c0f4c1046..45d41149438 100644 --- a/homeassistant/components/directv/translations/et.json +++ b/homeassistant/components/directv/translations/et.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Kas seadistada {name}?" diff --git a/homeassistant/components/directv/translations/no.json b/homeassistant/components/directv/translations/no.json index e93d3dadf49..a8c9492d6ba 100644 --- a/homeassistant/components/directv/translations/no.json +++ b/homeassistant/components/directv/translations/no.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Vil du sette opp {name} ?" diff --git a/homeassistant/components/directv/translations/ru.json b/homeassistant/components/directv/translations/ru.json index a3a340e5ce5..50995f26590 100644 --- a/homeassistant/components/directv/translations/ru.json +++ b/homeassistant/components/directv/translations/ru.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_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 {name}?" diff --git a/homeassistant/components/directv/translations/zh-Hant.json b/homeassistant/components/directv/translations/zh-Hant.json index d38bbb90528..c9f3fda773e 100644 --- a/homeassistant/components/directv/translations/zh-Hant.json +++ b/homeassistant/components/directv/translations/zh-Hant.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "DirecTV\uff1a{name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" diff --git a/homeassistant/components/doorbird/translations/ca.json b/homeassistant/components/doorbird/translations/ca.json index 2adc6227ab4..880360234b1 100644 --- a/homeassistant/components/doorbird/translations/ca.json +++ b/homeassistant/components/doorbird/translations/ca.json @@ -10,7 +10,7 @@ "invalid_auth": "[%key::common::config_flow::error::invalid_auth%]", "unknown": "[%key::common::config_flow::error::unknown%]" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/translations/et.json b/homeassistant/components/doorbird/translations/et.json index 2d967a09cda..08b163c0ca2 100644 --- a/homeassistant/components/doorbird/translations/et.json +++ b/homeassistant/components/doorbird/translations/et.json @@ -10,7 +10,7 @@ "invalid_auth": "Tuvastamine nurjus", "unknown": "Tundmatu viga" }, - "flow_title": "", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/translations/no.json b/homeassistant/components/doorbird/translations/no.json index b2a8928dc4d..356c86a8b54 100644 --- a/homeassistant/components/doorbird/translations/no.json +++ b/homeassistant/components/doorbird/translations/no.json @@ -10,7 +10,7 @@ "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "{name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/translations/ru.json b/homeassistant/components/doorbird/translations/ru.json index 4d5695a3ab2..df156bb640a 100644 --- a/homeassistant/components/doorbird/translations/ru.json +++ b/homeassistant/components/doorbird/translations/ru.json @@ -10,7 +10,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/translations/zh-Hant.json b/homeassistant/components/doorbird/translations/zh-Hant.json index b475a474ed9..020267b4921 100644 --- a/homeassistant/components/doorbird/translations/zh-Hant.json +++ b/homeassistant/components/doorbird/translations/zh-Hant.json @@ -10,7 +10,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/ca.json b/homeassistant/components/elgato/translations/ca.json index 2302b833481..354e67e00b0 100644 --- a/homeassistant/components/elgato/translations/ca.json +++ b/homeassistant/components/elgato/translations/ca.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "Elgato Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/et.json b/homeassistant/components/elgato/translations/et.json index da01933c787..7f50ffc4c98 100644 --- a/homeassistant/components/elgato/translations/et.json +++ b/homeassistant/components/elgato/translations/et.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "Elgato Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/no.json b/homeassistant/components/elgato/translations/no.json index 8059138e366..0e3c4abdf6e 100644 --- a/homeassistant/components/elgato/translations/no.json +++ b/homeassistant/components/elgato/translations/no.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "Elgato Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/ru.json b/homeassistant/components/elgato/translations/ru.json index e3af9572232..6641785b75f 100644 --- a/homeassistant/components/elgato/translations/ru.json +++ b/homeassistant/components/elgato/translations/ru.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "Elgato Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json index f1180a719ba..9d6bd9ab761 100644 --- a/homeassistant/components/elgato/translations/zh-Hant.json +++ b/homeassistant/components/elgato/translations/zh-Hant.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "Elgato \u7167\u660e\uff1a{serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/emonitor/translations/ca.json b/homeassistant/components/emonitor/translations/ca.json index b6fd1f99c84..4ab24587c15 100644 --- a/homeassistant/components/emonitor/translations/ca.json +++ b/homeassistant/components/emonitor/translations/ca.json @@ -7,7 +7,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Vols configurar {name} ({host})?", diff --git a/homeassistant/components/emonitor/translations/et.json b/homeassistant/components/emonitor/translations/et.json index bea6607a9ca..d1051aed9e8 100644 --- a/homeassistant/components/emonitor/translations/et.json +++ b/homeassistant/components/emonitor/translations/et.json @@ -7,7 +7,7 @@ "cannot_connect": "\u00dchendamine nurjus", "unknown": "Tundmatu viga" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Kas soovid seadistada {name}({host})?", diff --git a/homeassistant/components/emonitor/translations/no.json b/homeassistant/components/emonitor/translations/no.json index 866602d854b..5b559af8f17 100644 --- a/homeassistant/components/emonitor/translations/no.json +++ b/homeassistant/components/emonitor/translations/no.json @@ -7,7 +7,7 @@ "cannot_connect": "Tilkobling mislyktes", "unknown": "Uventet feil" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Vil du konfigurere {name} ({host})?", diff --git a/homeassistant/components/emonitor/translations/ru.json b/homeassistant/components/emonitor/translations/ru.json index e9ae6b12e86..682b4cdde8a 100644 --- a/homeassistant/components/emonitor/translations/ru.json +++ b/homeassistant/components/emonitor/translations/ru.json @@ -7,7 +7,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", diff --git a/homeassistant/components/emonitor/translations/zh-Hant.json b/homeassistant/components/emonitor/translations/zh-Hant.json index 1a7dc36fc5a..94fda8f8d58 100644 --- a/homeassistant/components/emonitor/translations/zh-Hant.json +++ b/homeassistant/components/emonitor/translations/zh-Hant.json @@ -7,7 +7,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", diff --git a/homeassistant/components/enphase_envoy/translations/ca.json b/homeassistant/components/enphase_envoy/translations/ca.json index fad9e8f4a18..dad1f8903ba 100644 --- a/homeassistant/components/enphase_envoy/translations/ca.json +++ b/homeassistant/components/enphase_envoy/translations/ca.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/enphase_envoy/translations/et.json b/homeassistant/components/enphase_envoy/translations/et.json index d4a0fb6dfb3..a692ed3f79e 100644 --- a/homeassistant/components/enphase_envoy/translations/et.json +++ b/homeassistant/components/enphase_envoy/translations/et.json @@ -9,7 +9,7 @@ "invalid_auth": "Tuvastamise viga", "unknown": "Tundmatu viga" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/enphase_envoy/translations/no.json b/homeassistant/components/enphase_envoy/translations/no.json index aee2b0f711a..9422d97056a 100644 --- a/homeassistant/components/enphase_envoy/translations/no.json +++ b/homeassistant/components/enphase_envoy/translations/no.json @@ -9,7 +9,7 @@ "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, - "flow_title": "Utsending {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/enphase_envoy/translations/ru.json b/homeassistant/components/enphase_envoy/translations/ru.json index b04d0ac5093..b6e025f63e0 100644 --- a/homeassistant/components/enphase_envoy/translations/ru.json +++ b/homeassistant/components/enphase_envoy/translations/ru.json @@ -9,7 +9,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/enphase_envoy/translations/zh-Hant.json b/homeassistant/components/enphase_envoy/translations/zh-Hant.json index 6fd6d4d038a..c568b749e50 100644 --- a/homeassistant/components/enphase_envoy/translations/zh-Hant.json +++ b/homeassistant/components/enphase_envoy/translations/zh-Hant.json @@ -9,7 +9,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/epson/translations/ca.json b/homeassistant/components/epson/translations/ca.json index 51fbbe1e273..eae6b1329d5 100644 --- a/homeassistant/components/epson/translations/ca.json +++ b/homeassistant/components/epson/translations/ca.json @@ -8,8 +8,7 @@ "user": { "data": { "host": "Amfitri\u00f3", - "name": "Nom", - "port": "Port" + "name": "Nom" } } } diff --git a/homeassistant/components/epson/translations/cs.json b/homeassistant/components/epson/translations/cs.json index 31b0e41118c..7a27355056b 100644 --- a/homeassistant/components/epson/translations/cs.json +++ b/homeassistant/components/epson/translations/cs.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "Hostitel", - "name": "Jm\u00e9no", - "port": "Port" + "name": "Jm\u00e9no" } } } diff --git a/homeassistant/components/epson/translations/de.json b/homeassistant/components/epson/translations/de.json index a91e3831cdb..2d53861fb75 100644 --- a/homeassistant/components/epson/translations/de.json +++ b/homeassistant/components/epson/translations/de.json @@ -1,14 +1,14 @@ { "config": { "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "powered_off": "Ist der Projektor eingeschaltet? Du musst den Projektor f\u00fcr die Erstkonfiguration einschalten." }, "step": { "user": { "data": { "host": "Host", - "name": "Name", - "port": "Port" + "name": "Name" } } } diff --git a/homeassistant/components/epson/translations/en.json b/homeassistant/components/epson/translations/en.json index 2c477f65de4..931bbcf557e 100644 --- a/homeassistant/components/epson/translations/en.json +++ b/homeassistant/components/epson/translations/en.json @@ -8,8 +8,7 @@ "user": { "data": { "host": "Host", - "name": "Name", - "port": "Port" + "name": "Name" } } } diff --git a/homeassistant/components/epson/translations/es.json b/homeassistant/components/epson/translations/es.json index e1d40ef981b..77837bb25ce 100644 --- a/homeassistant/components/epson/translations/es.json +++ b/homeassistant/components/epson/translations/es.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "Host", - "name": "Nombre", - "port": "Puerto" + "name": "Nombre" } } } diff --git a/homeassistant/components/epson/translations/et.json b/homeassistant/components/epson/translations/et.json index a0e3ec395f5..755e5810c25 100644 --- a/homeassistant/components/epson/translations/et.json +++ b/homeassistant/components/epson/translations/et.json @@ -8,8 +8,7 @@ "user": { "data": { "host": "Host", - "name": "Nimi", - "port": "Port" + "name": "Nimi" } } } diff --git a/homeassistant/components/epson/translations/fr.json b/homeassistant/components/epson/translations/fr.json index 3bbdd3063f5..51a18284e73 100644 --- a/homeassistant/components/epson/translations/fr.json +++ b/homeassistant/components/epson/translations/fr.json @@ -8,8 +8,7 @@ "user": { "data": { "host": "H\u00f4te", - "name": "Nom", - "port": "Port" + "name": "Nom" } } } diff --git a/homeassistant/components/epson/translations/hu.json b/homeassistant/components/epson/translations/hu.json index f2a380903ec..4f70feb6ec1 100644 --- a/homeassistant/components/epson/translations/hu.json +++ b/homeassistant/components/epson/translations/hu.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "Hoszt", - "name": "N\u00e9v", - "port": "Port" + "name": "N\u00e9v" } } } diff --git a/homeassistant/components/epson/translations/id.json b/homeassistant/components/epson/translations/id.json index ba2d36424f9..fd6c2bc2491 100644 --- a/homeassistant/components/epson/translations/id.json +++ b/homeassistant/components/epson/translations/id.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "Host", - "name": "Nama", - "port": "Port" + "name": "Nama" } } } diff --git a/homeassistant/components/epson/translations/it.json b/homeassistant/components/epson/translations/it.json index fe72abc8739..88a296466e9 100644 --- a/homeassistant/components/epson/translations/it.json +++ b/homeassistant/components/epson/translations/it.json @@ -8,8 +8,7 @@ "user": { "data": { "host": "Host", - "name": "Nome", - "port": "Porta" + "name": "Nome" } } } diff --git a/homeassistant/components/epson/translations/ka.json b/homeassistant/components/epson/translations/ka.json index b339899ea5f..ec686412a8f 100644 --- a/homeassistant/components/epson/translations/ka.json +++ b/homeassistant/components/epson/translations/ka.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "\u10f0\u10dd\u10e1\u10e2\u10d8", - "name": "\u10e1\u10d0\u10ee\u10d4\u10da\u10d8", - "port": "\u10de\u10dd\u10e0\u10e2\u10d8" + "name": "\u10e1\u10d0\u10ee\u10d4\u10da\u10d8" } } } diff --git a/homeassistant/components/epson/translations/ko.json b/homeassistant/components/epson/translations/ko.json index 1ee9afdcf75..15666044f5b 100644 --- a/homeassistant/components/epson/translations/ko.json +++ b/homeassistant/components/epson/translations/ko.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "\ud638\uc2a4\ud2b8", - "name": "\uc774\ub984", - "port": "\ud3ec\ud2b8" + "name": "\uc774\ub984" } } } diff --git a/homeassistant/components/epson/translations/lb.json b/homeassistant/components/epson/translations/lb.json index e8d9f52998f..2a46ad28dd5 100644 --- a/homeassistant/components/epson/translations/lb.json +++ b/homeassistant/components/epson/translations/lb.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "Host", - "name": "Numm", - "port": "Port" + "name": "Numm" } } } diff --git a/homeassistant/components/epson/translations/nl.json b/homeassistant/components/epson/translations/nl.json index d7521c945f2..d2ffe84c7be 100644 --- a/homeassistant/components/epson/translations/nl.json +++ b/homeassistant/components/epson/translations/nl.json @@ -8,8 +8,7 @@ "user": { "data": { "host": "Host", - "name": "Naam", - "port": "Poort" + "name": "Naam" } } } diff --git a/homeassistant/components/epson/translations/no.json b/homeassistant/components/epson/translations/no.json index fc4bf7dcf36..882b12801d4 100644 --- a/homeassistant/components/epson/translations/no.json +++ b/homeassistant/components/epson/translations/no.json @@ -8,8 +8,7 @@ "user": { "data": { "host": "Vert", - "name": "Navn", - "port": "Port" + "name": "Navn" } } } diff --git a/homeassistant/components/epson/translations/pl.json b/homeassistant/components/epson/translations/pl.json index 7a3ea98a0e9..98acbf5ef4b 100644 --- a/homeassistant/components/epson/translations/pl.json +++ b/homeassistant/components/epson/translations/pl.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "Nazwa hosta lub adres IP", - "name": "Nazwa", - "port": "Port" + "name": "Nazwa" } } } diff --git a/homeassistant/components/epson/translations/pt.json b/homeassistant/components/epson/translations/pt.json index 352e98916f1..38336a1d5de 100644 --- a/homeassistant/components/epson/translations/pt.json +++ b/homeassistant/components/epson/translations/pt.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "Servidor", - "name": "Nome", - "port": "Porta" + "name": "Nome" } } } diff --git a/homeassistant/components/epson/translations/ru.json b/homeassistant/components/epson/translations/ru.json index 47209d311a5..e800f033c3e 100644 --- a/homeassistant/components/epson/translations/ru.json +++ b/homeassistant/components/epson/translations/ru.json @@ -8,8 +8,7 @@ "user": { "data": { "host": "\u0425\u043e\u0441\u0442", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "port": "\u041f\u043e\u0440\u0442" + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" } } } diff --git a/homeassistant/components/epson/translations/tr.json b/homeassistant/components/epson/translations/tr.json index 9ffd77fc50f..cb0a09cb26a 100644 --- a/homeassistant/components/epson/translations/tr.json +++ b/homeassistant/components/epson/translations/tr.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "Ana Bilgisayar", - "name": "\u0130sim", - "port": "Port" + "name": "\u0130sim" } } } diff --git a/homeassistant/components/epson/translations/uk.json b/homeassistant/components/epson/translations/uk.json index 65566a8f4aa..e37fdc0f25c 100644 --- a/homeassistant/components/epson/translations/uk.json +++ b/homeassistant/components/epson/translations/uk.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "\u0425\u043e\u0441\u0442", - "name": "\u041d\u0430\u0437\u0432\u0430", - "port": "\u041f\u043e\u0440\u0442" + "name": "\u041d\u0430\u0437\u0432\u0430" } } } diff --git a/homeassistant/components/epson/translations/zh-Hant.json b/homeassistant/components/epson/translations/zh-Hant.json index 25ae09cb4b4..4831db6c564 100644 --- a/homeassistant/components/epson/translations/zh-Hant.json +++ b/homeassistant/components/epson/translations/zh-Hant.json @@ -8,8 +8,7 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "name": "\u540d\u7a31", - "port": "\u901a\u8a0a\u57e0" + "name": "\u540d\u7a31" } } } diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index 25374cbbd00..d0c59194528 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "resolve_error": "No s'ha pogut trobar l'adre\u00e7a de l'ESP. Si l'error persisteix, configura una adre\u00e7a IP est\u00e0tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/translations/et.json b/homeassistant/components/esphome/translations/et.json index 18f69b7207b..4f018931141 100644 --- a/homeassistant/components/esphome/translations/et.json +++ b/homeassistant/components/esphome/translations/et.json @@ -9,7 +9,7 @@ "invalid_auth": "Tuvastamise viga", "resolve_error": "ESP aadressi ei \u00f5nnestu lahendada. Kui see viga p\u00fcsib, m\u00e4\u00e4ra staatiline IP-aadress: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index d3501c496ef..14b92500f41 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -9,7 +9,7 @@ "invalid_auth": "Ugyldig godkjenning", "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, vennligst [sett en statisk IP-adresse](https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)" }, - "flow_title": "", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index 4277a057a86..5987a7db13b 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -9,7 +9,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips." }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 6e9e43eae02..6ea440c02df 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -9,7 +9,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "resolve_error": "\u7121\u6cd5\u89e3\u6790 ESP \u4f4d\u5740\uff0c\u5047\u5982\u6b64\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u53c3\u8003\u8aaa\u660e\u8a2d\u5b9a\u70ba\u975c\u614b\u56fa\u5b9a IP \uff1a https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome\uff1a{name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/ezviz/translations/de.json b/homeassistant/components/ezviz/translations/de.json index 0286f942487..184255bcbcc 100644 --- a/homeassistant/components/ezviz/translations/de.json +++ b/homeassistant/components/ezviz/translations/de.json @@ -9,12 +9,15 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse" }, + "flow_title": "{serial}", "step": { "confirm": { "data": { "password": "Passwort", "username": "Benutzername" - } + }, + "description": "RTSP-Anmeldeinformationen f\u00fcr Ezviz-Kamera {serial} mit IP {ip_address} eingeben", + "title": "Entdeckte Ezviz-Kamera" }, "user": { "data": { @@ -29,7 +32,9 @@ "password": "Passwort", "url": "URL", "username": "Benutzername" - } + }, + "description": "URL Region manuell festlegen", + "title": "Verbinden mit benutzerdefinierter Ezviz-URL" } } }, diff --git a/homeassistant/components/flume/translations/de.json b/homeassistant/components/flume/translations/de.json index e229427ea6f..2d1a67d9a74 100644 --- a/homeassistant/components/flume/translations/de.json +++ b/homeassistant/components/flume/translations/de.json @@ -13,7 +13,9 @@ "reauth_confirm": { "data": { "password": "Passwort" - } + }, + "description": "Das Passwort f\u00fcr {username} ist nicht mehr g\u00fcltig.", + "title": "Authentifizieren Sie Ihr Flume-Konto erneut" }, "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/ca.json b/homeassistant/components/forked_daapd/translations/ca.json index fb778199efa..f84b0376dd6 100644 --- a/homeassistant/components/forked_daapd/translations/ca.json +++ b/homeassistant/components/forked_daapd/translations/ca.json @@ -12,7 +12,7 @@ "wrong_password": "Contrasenya incorrecta.", "wrong_server_type": "La integraci\u00f3 forked-daapd necessita un servidor forked-daapd amb versi\u00f3 >= 27.0." }, - "flow_title": "Servidor forked-daapd: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/et.json b/homeassistant/components/forked_daapd/translations/et.json index 8e2096e821c..a9413cf0cea 100644 --- a/homeassistant/components/forked_daapd/translations/et.json +++ b/homeassistant/components/forked_daapd/translations/et.json @@ -12,7 +12,7 @@ "wrong_password": "Vale salas\u00f5na.", "wrong_server_type": "Forked-daapd sidumine n\u00f5uab forked-daapd serveri versioon >= 27.0." }, - "flow_title": "forked-daapd server: {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/no.json b/homeassistant/components/forked_daapd/translations/no.json index da260c9a019..4461101cbc2 100644 --- a/homeassistant/components/forked_daapd/translations/no.json +++ b/homeassistant/components/forked_daapd/translations/no.json @@ -12,7 +12,7 @@ "wrong_password": "Feil passord.", "wrong_server_type": "Forked-daapd integrasjon krever en gaffel-daapd server med versjon \"= 27.0." }, - "flow_title": "", + "flow_title": "{name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/ru.json b/homeassistant/components/forked_daapd/translations/ru.json index 58d574b4054..3850c895353 100644 --- a/homeassistant/components/forked_daapd/translations/ru.json +++ b/homeassistant/components/forked_daapd/translations/ru.json @@ -12,7 +12,7 @@ "wrong_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", "wrong_server_type": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd \u0432\u0435\u0440\u0441\u0438\u0438 27.0 \u0438\u043b\u0438 \u0432\u044b\u0448\u0435." }, - "flow_title": "\u0421\u0435\u0440\u0432\u0435\u0440 forked-daapd: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/zh-Hant.json b/homeassistant/components/forked_daapd/translations/zh-Hant.json index 17839b60748..9d91fb93033 100644 --- a/homeassistant/components/forked_daapd/translations/zh-Hant.json +++ b/homeassistant/components/forked_daapd/translations/zh-Hant.json @@ -12,7 +12,7 @@ "wrong_password": "\u5bc6\u78bc\u932f\u8aa4\u3002", "wrong_server_type": "forked-daapd \u6574\u5408\u9700\u8981\u7248\u6b21 >= 27.0 \u7248\u4e4b forked-daapd \u4f3a\u670d\u5668\u3002" }, - "flow_title": "forked-daapd \u4f3a\u670d\u5668\uff1a{name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json index 1b55ba3e23d..bfd095d4dc0 100644 --- a/homeassistant/components/fritz/translations/ca.json +++ b/homeassistant/components/fritz/translations/ca.json @@ -11,7 +11,7 @@ "connection_error": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, - "flow_title": "FRITZ!Box Tools: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index 037c9eb07f1..8fd24fa43dc 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -18,7 +18,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Entdeckte FRITZ! Box: {name} \n\n Richten Sie FRITZ! Box Tools ein, um {name} zu kontrollieren", + "description": "Entdeckte FRITZ! Box: {name} \n\nRichte deine FRITZ! Box Tools ein, um {name} zu kontrollieren", "title": "FRITZ! Box Tools einrichten" }, "reauth_confirm": { @@ -26,7 +26,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Aktualisieren Sie die Anmeldeinformationen von FRITZ! Box Tools f\u00fcr: {host} . \n\n FRITZ! Box Tools kann sich nicht bei Ihrer FRITZ! Box anmelden.", + "description": "Aktualisiere die Anmeldeinformationen von FRITZ! Box Tools f\u00fcr: {host} . \n\nFRITZ! Box Tools kann sich nicht bei deiner FRITZ! Box anmelden.", "title": "Aktualisieren der FRITZ! Box Tools - Anmeldeinformationen" }, "start_config": { diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json index 1ab5b27ffd7..bb72dde74b8 100644 --- a/homeassistant/components/fritz/translations/et.json +++ b/homeassistant/components/fritz/translations/et.json @@ -11,7 +11,7 @@ "connection_error": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus" }, - "flow_title": "FRITZ!Box t\u00f6\u00f6riistad: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json index e3b642a1594..44bb6d297cb 100644 --- a/homeassistant/components/fritz/translations/no.json +++ b/homeassistant/components/fritz/translations/no.json @@ -11,7 +11,7 @@ "connection_error": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "FRITZ!Box Verkt\u00f8y: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json index b50c42c4bfc..d6921c900e6 100644 --- a/homeassistant/components/fritz/translations/ru.json +++ b/homeassistant/components/fritz/translations/ru.json @@ -11,7 +11,7 @@ "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, - "flow_title": "FRITZ!Box Tools: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json index 29872e14868..370894a5e00 100644 --- a/homeassistant/components/fritz/translations/zh-Hant.json +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -11,7 +11,7 @@ "connection_error": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "FRITZ!Box Tools\uff1a{name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/ca.json b/homeassistant/components/fritzbox/translations/ca.json index 9324f91ef18..efd81ddff84 100644 --- a/homeassistant/components/fritzbox/translations/ca.json +++ b/homeassistant/components/fritzbox/translations/ca.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, - "flow_title": "AVM FRITZ!SmartHome: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/et.json b/homeassistant/components/fritzbox/translations/et.json index 96c77903f97..849dc7fadee 100644 --- a/homeassistant/components/fritzbox/translations/et.json +++ b/homeassistant/components/fritzbox/translations/et.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Tuvastamise viga" }, - "flow_title": "AVM FRITZ! SmartHome: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json index 9a64c5b8506..5ec0cc1acdc 100644 --- a/homeassistant/components/fritzbox/translations/no.json +++ b/homeassistant/components/fritzbox/translations/no.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "AVM FRITZ! SmartHome: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json index 5ca83042497..51e9aedc632 100644 --- a/homeassistant/components/fritzbox/translations/ru.json +++ b/homeassistant/components/fritzbox/translations/ru.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, - "flow_title": "AVM FRITZ!SmartHome: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index d27d78b8962..b90b87aaee7 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "AVM FRITZ!SmartHome\uff1a{name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ca.json b/homeassistant/components/fritzbox_callmonitor/translations/ca.json index 808b642f4ff..e98a1c345b8 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ca.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ca.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, - "flow_title": "Sensor de trucades d'AVM FRITZ!Box: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/et.json b/homeassistant/components/fritzbox_callmonitor/translations/et.json index 7770f31ae0e..4f9205efc89 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/et.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/et.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Vigane autentimine" }, - "flow_title": "AVM FRITZ! K\u00f5nekontroll: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/no.json b/homeassistant/components/fritzbox_callmonitor/translations/no.json index 12883b0140d..98b1c31c804 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/no.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/no.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "AVM FRITZ! Box monitor: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ru.json b/homeassistant/components/fritzbox_callmonitor/translations/ru.json index 38448ac8c59..608d6e1ee64 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ru.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ru.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, - "flow_title": "AVM FRITZ!Box call monitor: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json index 3e7da079b18..1f8127c1928 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "AVM FRITZ!Box \u901a\u8a71\u76e3\u63a7\u5668\uff1a{name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/growatt_server/translations/ca.json b/homeassistant/components/growatt_server/translations/ca.json new file mode 100644 index 00000000000..19c0eecdafb --- /dev/null +++ b/homeassistant/components/growatt_server/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "No s'ha trobat cap planta en aquest compte" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "plant": { + "data": { + "plant_id": "Planta" + }, + "title": "Selecciona la teva planta" + }, + "user": { + "data": { + "name": "Nom", + "password": "Nom", + "username": "Nom d'usuari" + }, + "title": "Introdueix la teva informaci\u00f3 de Growatt" + } + } + }, + "title": "Servidor Growatt" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/de.json b/homeassistant/components/growatt_server/translations/de.json new file mode 100644 index 00000000000..f58a513e038 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "no_plants": "Es wurden keine Pflanzen auf diesem Konto gefunden" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "plant": { + "data": { + "plant_id": "Pflanze" + }, + "title": "W\u00e4hle deine Pflanze aus" + }, + "user": { + "data": { + "name": "Name", + "username": "Nutzername" + }, + "title": "Gib deine Growatt-Informationen ein" + } + } + }, + "title": "Growatt Server" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/et.json b/homeassistant/components/growatt_server/translations/et.json new file mode 100644 index 00000000000..c371234e6da --- /dev/null +++ b/homeassistant/components/growatt_server/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "Kontolt ei leitud \u00fchtegi taime" + }, + "error": { + "invalid_auth": "Autentimine nurjus" + }, + "step": { + "plant": { + "data": { + "plant_id": "Taim" + }, + "title": "Vali oma taim" + }, + "user": { + "data": { + "name": "Nimi", + "password": "Nimi", + "username": "Kasutajanimi" + }, + "title": "Sisesta oma Growatti teave" + } + } + }, + "title": "Growatt Server" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/nl.json b/homeassistant/components/growatt_server/translations/nl.json new file mode 100644 index 00000000000..b4e27643cdb --- /dev/null +++ b/homeassistant/components/growatt_server/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "Er zijn geen planten gevonden op dit account" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "plant": { + "data": { + "plant_id": "Plant" + }, + "title": "Kies uw plant" + }, + "user": { + "data": { + "name": "Naam", + "password": "Naam", + "username": "Gebruikersnaam" + }, + "title": "Vul uw Growatt gegevens in" + } + } + }, + "title": "Growatt Server" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/no.json b/homeassistant/components/growatt_server/translations/no.json new file mode 100644 index 00000000000..03f2d6ee82d --- /dev/null +++ b/homeassistant/components/growatt_server/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "Ingen planter er funnet p\u00e5 denne kontoen" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "plant": { + "data": { + "plant_id": "Plante" + }, + "title": "Velg din plante" + }, + "user": { + "data": { + "name": "Navn", + "password": "Navn", + "username": "Brukernavn" + }, + "title": "Skriv inn Growatt-informasjonen din" + } + } + }, + "title": "Growatt Server" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/ru.json b/homeassistant/components/growatt_server/translations/ru.json new file mode 100644 index 00000000000..8db89da175b --- /dev/null +++ b/homeassistant/components/growatt_server/translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "\u0412 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0440\u0430\u0441\u0442\u0435\u043d\u0438\u044f." + }, + "error": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "plant": { + "data": { + "plant_id": "\u0420\u0430\u0441\u0442\u0435\u043d\u0438\u0435" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0430\u0441\u0442\u0435\u043d\u0438\u0435" + }, + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Growatt." + } + } + }, + "title": "\u0421\u0435\u0440\u0432\u0435\u0440 Growatt" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/zh-Hant.json b/homeassistant/components/growatt_server/translations/zh-Hant.json new file mode 100644 index 00000000000..47efaddf3fa --- /dev/null +++ b/homeassistant/components/growatt_server/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u690d\u7269" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "plant": { + "data": { + "plant_id": "\u690d\u7269" + }, + "title": "\u9078\u64c7\u690d\u7269" + }, + "user": { + "data": { + "name": "\u540d\u7a31", + "password": "\u540d\u7a31", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u8f38\u5165 Growatt \u8cc7\u8a0a" + } + } + }, + "title": "Growatt \u4f3a\u670d\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ca.json b/homeassistant/components/guardian/translations/ca.json index 476bf49ee5a..0831975511e 100644 --- a/homeassistant/components/guardian/translations/ca.json +++ b/homeassistant/components/guardian/translations/ca.json @@ -6,6 +6,9 @@ "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { + "discovery_confirm": { + "description": "Vols configurar aquest dispositiu Guardian?" + }, "user": { "data": { "ip_address": "Adre\u00e7a IP", diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index 310f550bcc1..52932cce02b 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -15,6 +15,9 @@ "port": "Port" }, "description": "Configure a local Elexa Guardian device." + }, + "zeroconf_confirm": { + "description": "Do you want to set up this Guardian device?" } } } diff --git a/homeassistant/components/guardian/translations/et.json b/homeassistant/components/guardian/translations/et.json index 42c425ec85f..56aec0e00c7 100644 --- a/homeassistant/components/guardian/translations/et.json +++ b/homeassistant/components/guardian/translations/et.json @@ -6,6 +6,9 @@ "cannot_connect": "\u00dchendamine nurjus" }, "step": { + "discovery_confirm": { + "description": "Kas soovid seadistada seda Guardian'i seadet?" + }, "user": { "data": { "ip_address": "IP aadress", diff --git a/homeassistant/components/guardian/translations/nl.json b/homeassistant/components/guardian/translations/nl.json index a33cb9357a9..409c3db9bed 100644 --- a/homeassistant/components/guardian/translations/nl.json +++ b/homeassistant/components/guardian/translations/nl.json @@ -6,6 +6,9 @@ "cannot_connect": "Kan geen verbinding maken" }, "step": { + "discovery_confirm": { + "description": "Wil je dit Guardian apparaat instellen?" + }, "user": { "data": { "ip_address": "IP-adres", diff --git a/homeassistant/components/guardian/translations/no.json b/homeassistant/components/guardian/translations/no.json index 18410f382ca..28313fa8520 100644 --- a/homeassistant/components/guardian/translations/no.json +++ b/homeassistant/components/guardian/translations/no.json @@ -6,6 +6,9 @@ "cannot_connect": "Tilkobling mislyktes" }, "step": { + "discovery_confirm": { + "description": "Vil du konfigurere denne Guardian-enheten?" + }, "user": { "data": { "ip_address": "IP adresse", diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json index 20e1710f120..1095a568d78 100644 --- a/homeassistant/components/guardian/translations/ru.json +++ b/homeassistant/components/guardian/translations/ru.json @@ -6,6 +6,9 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Guardian?" + }, "user": { "data": { "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index e2a8c03dbbf..dc3bde8ec1c 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -6,6 +6,9 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Guardian \u88dd\u7f6e\uff1f" + }, "user": { "data": { "ip_address": "IP \u4f4d\u5740", diff --git a/homeassistant/components/harmony/translations/ca.json b/homeassistant/components/harmony/translations/ca.json index 7257ddb450d..e5a15705bac 100644 --- a/homeassistant/components/harmony/translations/ca.json +++ b/homeassistant/components/harmony/translations/ca.json @@ -7,7 +7,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "Vols configurar {name} ({host})?", diff --git a/homeassistant/components/harmony/translations/et.json b/homeassistant/components/harmony/translations/et.json index 86128243014..ef4c0c5ce69 100644 --- a/homeassistant/components/harmony/translations/et.json +++ b/homeassistant/components/harmony/translations/et.json @@ -7,7 +7,7 @@ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti", "unknown": "Ootamatu t\u00f5rge" }, - "flow_title": "", + "flow_title": "{name}", "step": { "link": { "description": "Kas soovid seadistada {name}({host})?", diff --git a/homeassistant/components/harmony/translations/no.json b/homeassistant/components/harmony/translations/no.json index aeeb5ae3c84..072837d600e 100644 --- a/homeassistant/components/harmony/translations/no.json +++ b/homeassistant/components/harmony/translations/no.json @@ -7,7 +7,7 @@ "cannot_connect": "Tilkobling mislyktes", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "{name}", "step": { "link": { "description": "Vil du konfigurere {name} ({host})?", diff --git a/homeassistant/components/harmony/translations/ru.json b/homeassistant/components/harmony/translations/ru.json index 5dfcff7091d..8bf00fe8637 100644 --- a/homeassistant/components/harmony/translations/ru.json +++ b/homeassistant/components/harmony/translations/ru.json @@ -7,7 +7,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", diff --git a/homeassistant/components/harmony/translations/zh-Hant.json b/homeassistant/components/harmony/translations/zh-Hant.json index cf835421fc1..0ea95aecd67 100644 --- a/homeassistant/components/harmony/translations/zh-Hant.json +++ b/homeassistant/components/harmony/translations/zh-Hant.json @@ -7,7 +7,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "\u7f85\u6280 Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", diff --git a/homeassistant/components/hassio/translations/af.json b/homeassistant/components/hassio/translations/af.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/af.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/bg.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index d2e712c230d..7813d970d0e 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -14,6 +14,5 @@ "update_channel": "Canal d'actualitzaci\u00f3", "version_api": "Versi\u00f3 d'APIs" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/cs.json b/homeassistant/components/hassio/translations/cs.json index eb64ed58baa..97f844a8c81 100644 --- a/homeassistant/components/hassio/translations/cs.json +++ b/homeassistant/components/hassio/translations/cs.json @@ -14,6 +14,5 @@ "update_channel": "Kan\u00e1l aktualizac\u00ed", "version_api": "Verze API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/cy.json b/homeassistant/components/hassio/translations/cy.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/cy.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/da.json b/homeassistant/components/hassio/translations/da.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/da.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/de.json b/homeassistant/components/hassio/translations/de.json index 19538e0b3e1..99747512e97 100644 --- a/homeassistant/components/hassio/translations/de.json +++ b/homeassistant/components/hassio/translations/de.json @@ -14,6 +14,5 @@ "update_channel": "Update-Channel", "version_api": "Versions-API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/el.json b/homeassistant/components/hassio/translations/el.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/el.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 16911be4110..bb5f8e6f01a 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -14,6 +14,5 @@ "update_channel": "Update Channel", "version_api": "Version API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/es-419.json b/homeassistant/components/hassio/translations/es-419.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/es-419.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index f3bdf14c446..da3730fa45b 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -14,6 +14,5 @@ "update_channel": "Canal de actualizaci\u00f3n", "version_api": "Versi\u00f3n del API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/et.json b/homeassistant/components/hassio/translations/et.json index 9d3ef08afbe..4449c058498 100644 --- a/homeassistant/components/hassio/translations/et.json +++ b/homeassistant/components/hassio/translations/et.json @@ -14,6 +14,5 @@ "update_channel": "V\u00e4rskenduskanal", "version_api": "API versioon" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/eu.json b/homeassistant/components/hassio/translations/eu.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/eu.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/fa.json b/homeassistant/components/hassio/translations/fa.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/fa.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/fi.json b/homeassistant/components/hassio/translations/fi.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/fi.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/fr.json b/homeassistant/components/hassio/translations/fr.json index e4fe8a63bba..6e20e37a2b9 100644 --- a/homeassistant/components/hassio/translations/fr.json +++ b/homeassistant/components/hassio/translations/fr.json @@ -14,6 +14,5 @@ "update_channel": "Mise \u00e0 jour", "version_api": "Version API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/he.json b/homeassistant/components/hassio/translations/he.json deleted file mode 100644 index 80c1a0c48ee..00000000000 --- a/homeassistant/components/hassio/translations/he.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/hr.json b/homeassistant/components/hassio/translations/hr.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/hr.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index e0fc98408d4..ae7a51a8ec8 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -13,6 +13,5 @@ "update_channel": "Friss\u00edt\u00e9si csatorna", "version_api": "API verzi\u00f3" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/hy.json b/homeassistant/components/hassio/translations/hy.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/hy.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/id.json b/homeassistant/components/hassio/translations/id.json index b95ffb35d81..b87e1b47c44 100644 --- a/homeassistant/components/hassio/translations/id.json +++ b/homeassistant/components/hassio/translations/id.json @@ -14,6 +14,5 @@ "update_channel": "Kanal Pembaruan", "version_api": "API Versi" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/is.json b/homeassistant/components/hassio/translations/is.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/is.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index 86d573cba40..44499d3f002 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -14,6 +14,5 @@ "update_channel": "Canale di aggiornamento", "version_api": "Versione API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/ja.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ko.json b/homeassistant/components/hassio/translations/ko.json index aba9a665f70..23280115649 100644 --- a/homeassistant/components/hassio/translations/ko.json +++ b/homeassistant/components/hassio/translations/ko.json @@ -14,6 +14,5 @@ "update_channel": "\uc5c5\ub370\uc774\ud2b8 \ucc44\ub110", "version_api": "\ubc84\uc804 API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/lb.json b/homeassistant/components/hassio/translations/lb.json index c0d0f42ed94..55f5c0e9b3a 100644 --- a/homeassistant/components/hassio/translations/lb.json +++ b/homeassistant/components/hassio/translations/lb.json @@ -14,6 +14,5 @@ "update_channel": "Aktualis\u00e9ierungs Kanal", "version_api": "API Versioun" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/lt.json b/homeassistant/components/hassio/translations/lt.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/lt.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/lv.json b/homeassistant/components/hassio/translations/lv.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/lv.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/nl.json b/homeassistant/components/hassio/translations/nl.json index 7224857a10c..e5541ff1d00 100644 --- a/homeassistant/components/hassio/translations/nl.json +++ b/homeassistant/components/hassio/translations/nl.json @@ -14,6 +14,5 @@ "update_channel": "Update kanaal", "version_api": "API Versie" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/nn.json b/homeassistant/components/hassio/translations/nn.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/nn.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json index 9f0c5ba89b2..30ff8b903a9 100644 --- a/homeassistant/components/hassio/translations/no.json +++ b/homeassistant/components/hassio/translations/no.json @@ -14,6 +14,5 @@ "update_channel": "Oppdater kanal", "version_api": "Versjon API" } - }, - "title": "Home Assistant veileder" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pl.json b/homeassistant/components/hassio/translations/pl.json index 5266d640d7c..2f6b5cab1dc 100644 --- a/homeassistant/components/hassio/translations/pl.json +++ b/homeassistant/components/hassio/translations/pl.json @@ -14,6 +14,5 @@ "update_channel": "Kana\u0142 aktualizacji", "version_api": "Wersja API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pt-BR.json b/homeassistant/components/hassio/translations/pt-BR.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/pt-BR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pt.json b/homeassistant/components/hassio/translations/pt.json index 326560409e4..b05d2d02ecf 100644 --- a/homeassistant/components/hassio/translations/pt.json +++ b/homeassistant/components/hassio/translations/pt.json @@ -12,6 +12,5 @@ "supervisor_version": "Vers\u00e3o do Supervisor", "supported": "Suportado" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ro.json b/homeassistant/components/hassio/translations/ro.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/ro.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index 56c3522ba3c..f9572edcd6f 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -14,6 +14,5 @@ "update_channel": "\u041a\u0430\u043d\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439", "version_api": "\u0412\u0435\u0440\u0441\u0438\u044f API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/sk.json b/homeassistant/components/hassio/translations/sk.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/sk.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/sl.json b/homeassistant/components/hassio/translations/sl.json index cfc71ce0832..35aafe322bb 100644 --- a/homeassistant/components/hassio/translations/sl.json +++ b/homeassistant/components/hassio/translations/sl.json @@ -13,6 +13,5 @@ "update_channel": "Posodobi kanal", "version_api": "API razli\u010dica" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/sv.json b/homeassistant/components/hassio/translations/sv.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/sv.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/th.json b/homeassistant/components/hassio/translations/th.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/th.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/tr.json b/homeassistant/components/hassio/translations/tr.json index 06a8d3fd661..16504c32372 100644 --- a/homeassistant/components/hassio/translations/tr.json +++ b/homeassistant/components/hassio/translations/tr.json @@ -14,6 +14,5 @@ "update_channel": "Kanal\u0131 G\u00fcncelle", "version_api": "S\u00fcr\u00fcm API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/uk.json b/homeassistant/components/hassio/translations/uk.json index d25ad6e7979..05f39e905b4 100644 --- a/homeassistant/components/hassio/translations/uk.json +++ b/homeassistant/components/hassio/translations/uk.json @@ -14,6 +14,5 @@ "update_channel": "\u041a\u0430\u043d\u0430\u043b \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c", "version_api": "\u0412\u0435\u0440\u0441\u0456\u044f API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/vi.json b/homeassistant/components/hassio/translations/vi.json deleted file mode 100644 index 91588c5529a..00000000000 --- a/homeassistant/components/hassio/translations/vi.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Home Assistant Supervisor" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/zh-Hans.json b/homeassistant/components/hassio/translations/zh-Hans.json index a48cbeb95a8..dd5ad2b4eae 100644 --- a/homeassistant/components/hassio/translations/zh-Hans.json +++ b/homeassistant/components/hassio/translations/zh-Hans.json @@ -14,6 +14,5 @@ "update_channel": "\u66f4\u65b0\u901a\u9053", "version_api": "API \u7248\u672c" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/zh-Hant.json b/homeassistant/components/hassio/translations/zh-Hant.json index b8b3a1e7b93..91c7f64e39c 100644 --- a/homeassistant/components/hassio/translations/zh-Hant.json +++ b/homeassistant/components/hassio/translations/zh-Hant.json @@ -14,6 +14,5 @@ "update_channel": "\u66f4\u65b0\u983b\u9053", "version_api": "\u7248\u672c API" } - }, - "title": "Home Assistant Supervisor" + } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json index 97e3d088af4..4e227cedadd 100644 --- a/homeassistant/components/homeassistant/translations/ca.json +++ b/homeassistant/components/homeassistant/translations/ca.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "Arquitectura de la CPU", - "chassis": "Xass\u00eds", "dev": "Desenvolupador", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "Tipus d'instal\u00b7laci\u00f3", "os_name": "Fam\u00edlia del sistema operatiu", "os_version": "Versi\u00f3 del sistema operatiu", "python_version": "Versi\u00f3 de Python", - "supervisor": "Supervisor", "timezone": "Zona hor\u00e0ria", "version": "Versi\u00f3", "virtualenv": "Entorn virtual" diff --git a/homeassistant/components/homeassistant/translations/cs.json b/homeassistant/components/homeassistant/translations/cs.json index 3b6414b58ad..0b60fb374bb 100644 --- a/homeassistant/components/homeassistant/translations/cs.json +++ b/homeassistant/components/homeassistant/translations/cs.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "Architektura procesoru", - "chassis": "\u0160asi", "dev": "V\u00fdvoj", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "Typ instalace", "os_name": "Rodina opera\u010dn\u00edch syst\u00e9m\u016f", "os_version": "Verze opera\u010dn\u00edho syst\u00e9mu", "python_version": "Verze Pythonu", - "supervisor": "Supervisor", "timezone": "\u010casov\u00e9 p\u00e1smo", "version": "Verze", "virtualenv": "Virtu\u00e1ln\u00ed prost\u0159ed\u00ed" diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json index e24568ff212..426fab01031 100644 --- a/homeassistant/components/homeassistant/translations/de.json +++ b/homeassistant/components/homeassistant/translations/de.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "CPU-Architektur", - "chassis": "Chassis", "dev": "Entwicklung", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "Installationstyp", "os_name": "Betriebssystemfamilie", "os_version": "Betriebssystem-Version", "python_version": "Python-Version", - "supervisor": "Supervisor", "timezone": "Zeitzone", "version": "Version", "virtualenv": "Virtuelle Umgebung" diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 22538ad6536..897b577c33c 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "CPU Architecture", - "chassis": "Chassis", "dev": "Development", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "Installation Type", "os_name": "Operating System Family", "os_version": "Operating System Version", "python_version": "Python Version", - "supervisor": "Supervisor", "timezone": "Timezone", "version": "Version", "virtualenv": "Virtual Environment" diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index 1829d16d510..562a7335617 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "Arquitectura de CPU", - "chassis": "Chasis", "dev": "Desarrollo", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "SO Home Assistant", "installation_type": "Tipo de instalaci\u00f3n", "os_name": "Nombre del Sistema Operativo", "os_version": "Versi\u00f3n del Sistema Operativo", "python_version": "Versi\u00f3n de Python", - "supervisor": "Supervisor", "timezone": "Zona horaria", "version": "Versi\u00f3n", "virtualenv": "Entorno virtual" diff --git a/homeassistant/components/homeassistant/translations/et.json b/homeassistant/components/homeassistant/translations/et.json index 22e3ab1e00d..fd53bf02877 100644 --- a/homeassistant/components/homeassistant/translations/et.json +++ b/homeassistant/components/homeassistant/translations/et.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "Protsessori arhitektuur", - "chassis": "Korpus", "dev": "Arendus", "docker": "Docker", - "docker_version": "Docker", "hassio": "Haldur", - "host_os": "Home Assistant OS", "installation_type": "Paigalduse t\u00fc\u00fcp", "os_name": "Operatsioonis\u00fcsteemi j\u00e4rk", "os_version": "Operatsioonis\u00fcsteemi versioon", "python_version": "Pythoni versioon", - "supervisor": "Haldur", "timezone": "Ajav\u00f6\u00f6nd", "version": "Versioon", "virtualenv": "Virtuaalne keskkond" diff --git a/homeassistant/components/homeassistant/translations/fr.json b/homeassistant/components/homeassistant/translations/fr.json index 8d76ff76b79..6b7d4f93559 100644 --- a/homeassistant/components/homeassistant/translations/fr.json +++ b/homeassistant/components/homeassistant/translations/fr.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "Architecture du processeur", - "chassis": "Ch\u00e2ssis", "dev": "D\u00e9veloppement", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "Type d'installation", "os_name": "Famille du syst\u00e8me d'exploitation", "os_version": "Version du syst\u00e8me d'exploitation", "python_version": "Version de Python", - "supervisor": "Supervisor", "timezone": "Fuseau horaire", "version": "Version", "virtualenv": "Environnement virtuel" diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index f6bfe03321e..9eddeeba112 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "Processzor architekt\u00fara", - "chassis": "Kivitel", "dev": "Fejleszt\u00e9s", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "Telep\u00edt\u00e9s t\u00edpusa", "os_name": "Oper\u00e1ci\u00f3s rendszer csal\u00e1d", "os_version": "Oper\u00e1ci\u00f3s rendszer verzi\u00f3ja", "python_version": "Python verzi\u00f3", - "supervisor": "Supervisor", "timezone": "Id\u0151z\u00f3na", "version": "Verzi\u00f3", "virtualenv": "Virtu\u00e1lis k\u00f6rnyezet" diff --git a/homeassistant/components/homeassistant/translations/id.json b/homeassistant/components/homeassistant/translations/id.json index 2ee86bba815..8f3d9484c27 100644 --- a/homeassistant/components/homeassistant/translations/id.json +++ b/homeassistant/components/homeassistant/translations/id.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "Arsitektur CPU", - "chassis": "Kerangka", "dev": "Pengembangan", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "Jenis Instalasi", "os_name": "Keluarga Sistem Operasi", "os_version": "Versi Sistem Operasi", "python_version": "Versi Python", - "supervisor": "Supervisor", "timezone": "Zona Waktu", "version": "Versi", "virtualenv": "Lingkungan Virtual" diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index 66b8f8a1d14..2d8d73597d3 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "Architettura della CPU", - "chassis": "Telaio", "dev": "Sviluppo", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Sistema Operativo di Home Assistant", "installation_type": "Tipo di installazione", "os_name": "Famiglia del Sistema Operativo", "os_version": "Versione del Sistema Operativo", "python_version": "Versione Python", - "supervisor": "Supervisor", "timezone": "Fuso orario", "version": "Versione", "virtualenv": "Ambiente virtuale" diff --git a/homeassistant/components/homeassistant/translations/ka.json b/homeassistant/components/homeassistant/translations/ka.json index 4b5dec2fd30..27f744335e6 100644 --- a/homeassistant/components/homeassistant/translations/ka.json +++ b/homeassistant/components/homeassistant/translations/ka.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "\u10de\u10e0\u10dd\u10ea\u10d4\u10e1\u10dd\u10e0\u10d8\u10e1 \u10d0\u10e0\u10e5\u10d8\u10e2\u10d4\u10e5\u10e2\u10e3\u10e0\u10d0", - "chassis": "\u10e8\u10d0\u10e1\u10d8", "dev": "\u10e8\u10d4\u10db\u10e3\u10e8\u10d0\u10d5\u10d4\u10d1\u10d0", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant \u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8", "installation_type": "\u10d8\u10dc\u10e1\u10e2\u10d0\u10da\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8", "os_name": "\u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d8\u10e1 \u10dd\u10ef\u10d0\u10ee\u10d8", "os_version": "\u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d8\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0", "python_version": "Python-\u10d8\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0", - "supervisor": "Supervisor", "timezone": "\u1c93\u10e0\u10dd\u10d8\u10e1 \u10e1\u10d0\u10e0\u10e2\u10e7\u10d4\u10da\u10d8", "version": "\u10d5\u10d4\u10e0\u10e1\u10d8\u10d0", "virtualenv": "\u10d5\u10d8\u10e0\u10e2\u10e3\u10d0\u10da\u10e3\u10e0\u10d8 \u10d2\u10d0\u10e0\u10d4\u10db\u10dd" diff --git a/homeassistant/components/homeassistant/translations/ko.json b/homeassistant/components/homeassistant/translations/ko.json index 801d63fd449..1a9b1aef5c8 100644 --- a/homeassistant/components/homeassistant/translations/ko.json +++ b/homeassistant/components/homeassistant/translations/ko.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "CPU \uc544\ud0a4\ud14d\ucc98", - "chassis": "\uc100\uc2dc", "dev": "\uac1c\ubc1c\uc790 \ubaa8\ub4dc", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "\uc124\uce58 \uc720\ud615", "os_name": "\uc6b4\uc601 \uccb4\uc81c \uc81c\ud488\uad70", "os_version": "\uc6b4\uc601 \uccb4\uc81c \ubc84\uc804", "python_version": "Python \ubc84\uc804", - "supervisor": "Supervisor", "timezone": "\uc2dc\uac04\ub300", "version": "\ubc84\uc804", "virtualenv": "\uac00\uc0c1 \ud658\uacbd" diff --git a/homeassistant/components/homeassistant/translations/lb.json b/homeassistant/components/homeassistant/translations/lb.json index 07cfe8c4c83..51e0800c654 100644 --- a/homeassistant/components/homeassistant/translations/lb.json +++ b/homeassistant/components/homeassistant/translations/lb.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "CPU Architektur", - "chassis": "Chassis", "dev": "Entw\u00e9cklung", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor API", - "host_os": "Home Assistant OS", "installation_type": "Typ vun Installatioun", "os_name": "Betribssystem Famille", "os_version": "Betribssystem Versioun", "python_version": "Python Versioun", - "supervisor": "Supervisor", "timezone": "Z\u00e4itzon", "version": "Versioun", "virtualenv": "Virtuellen Environnement" diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index 6dba5eec8b9..8c76ffa39be 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "CPU-architectuur", - "chassis": "Chassis", "dev": "Ontwikkeling", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "Type installatie", "os_name": "Besturingssysteem", "os_version": "Versie van het besturingssysteem", "python_version": "Python-versie", - "supervisor": "Supervisor", "timezone": "Tijdzone", "version": "Versie", "virtualenv": "Virtuele omgeving" diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json index 3cf39e2cf7d..325bb53db15 100644 --- a/homeassistant/components/homeassistant/translations/no.json +++ b/homeassistant/components/homeassistant/translations/no.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "CPU-arkitektur", - "chassis": "Kabinett", "dev": "Utvikling", "docker": "", - "docker_version": "", "hassio": "Supervisor", - "host_os": "", "installation_type": "Installasjonstype", "os_name": "Familie for operativsystem", "os_version": "Operativsystemversjon", "python_version": "Python versjon", - "supervisor": "", "timezone": "Tidssone", "version": "Versjon", "virtualenv": "Virtuelt milj\u00f8" diff --git a/homeassistant/components/homeassistant/translations/pl.json b/homeassistant/components/homeassistant/translations/pl.json index 44d35cb582c..ea91096d0c2 100644 --- a/homeassistant/components/homeassistant/translations/pl.json +++ b/homeassistant/components/homeassistant/translations/pl.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "Architektura procesora", - "chassis": "Wersja komputera", "dev": "Wersja deweloperska", "docker": "Docker", - "docker_version": "Wersja Dockera", "hassio": "Supervisor", - "host_os": "System operacyjny HA", "installation_type": "Typ instalacji", "os_name": "Rodzina systemu operacyjnego", "os_version": "Wersja systemu operacyjnego", "python_version": "Wersja Pythona", - "supervisor": "Supervisor", "timezone": "Strefa czasowa", "version": "Wersja", "virtualenv": "\u015arodowisko wirtualne" diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json index c16c2c3baa4..13fd384d6a2 100644 --- a/homeassistant/components/homeassistant/translations/pt.json +++ b/homeassistant/components/homeassistant/translations/pt.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "Arquitetura do Processador", - "chassis": "Chassis", "dev": "Desenvolvimento", "docker": "", - "docker_version": "", "hassio": "Supervisor", - "host_os": "Sistema Operativo do Home Assistant", "installation_type": "Tipo de Instala\u00e7\u00e3o", "os_name": "Nome do Sistema Operativo", "os_version": "Vers\u00e3o do Sistema Operativo", "python_version": "Vers\u00e3o Python", - "supervisor": "Supervisor", "timezone": "Fuso hor\u00e1rio", "version": "Vers\u00e3o", "virtualenv": "Ambiente Virtual" diff --git a/homeassistant/components/homeassistant/translations/ru.json b/homeassistant/components/homeassistant/translations/ru.json index 651400c5fe5..c479fa41f43 100644 --- a/homeassistant/components/homeassistant/translations/ru.json +++ b/homeassistant/components/homeassistant/translations/ru.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f", - "chassis": "\u0428\u0430\u0441\u0441\u0438", "dev": "\u0421\u0440\u0435\u0434\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "\u0422\u0438\u043f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438", "os_name": "\u0421\u0435\u043c\u0435\u0439\u0441\u0442\u0432\u043e \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c", "os_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b", "python_version": "\u0412\u0435\u0440\u0441\u0438\u044f Python", - "supervisor": "Supervisor", "timezone": "\u0427\u0430\u0441\u043e\u0432\u043e\u0439 \u043f\u043e\u044f\u0441", "version": "\u0412\u0435\u0440\u0441\u0438\u044f", "virtualenv": "\u0412\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435" diff --git a/homeassistant/components/homeassistant/translations/sl.json b/homeassistant/components/homeassistant/translations/sl.json index 64e972f5a44..ff641d76792 100644 --- a/homeassistant/components/homeassistant/translations/sl.json +++ b/homeassistant/components/homeassistant/translations/sl.json @@ -4,7 +4,6 @@ "arch": "Arhitektura CPU", "dev": "Razvoj", "docker": "Docker", - "docker_version": "Docker", "hassio": "Nadzornik", "installation_type": "Vrsta namestitve", "os_version": "Razli\u010dica operacijskega sistema", diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json index c2b7ca1b10c..9c57c8f2cee 100644 --- a/homeassistant/components/homeassistant/translations/tr.json +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "CPU Mimarisi", - "chassis": "Ana G\u00f6vde", "dev": "Geli\u015ftirme", "docker": "Konteyner", - "docker_version": "Konteyner", "hassio": "S\u00fcperviz\u00f6r", - "host_os": "Home Assistant OS", "installation_type": "Kurulum T\u00fcr\u00fc", "os_name": "\u0130\u015fletim Sistemi Ailesi", "os_version": "\u0130\u015fletim Sistemi S\u00fcr\u00fcm\u00fc", "python_version": "Python S\u00fcr\u00fcm\u00fc", - "supervisor": "S\u00fcperviz\u00f6r", "timezone": "Saat dilimi", "version": "S\u00fcr\u00fcm", "virtualenv": "Sanal Ortam" diff --git a/homeassistant/components/homeassistant/translations/uk.json b/homeassistant/components/homeassistant/translations/uk.json index 19e07c8f822..b35506b6138 100644 --- a/homeassistant/components/homeassistant/translations/uk.json +++ b/homeassistant/components/homeassistant/translations/uk.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0456\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f", - "chassis": "\u0428\u0430\u0441\u0456", "dev": "\u0421\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0435 \u0440\u043e\u0437\u0440\u043e\u0431\u043a\u0438", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "\u0422\u0438\u043f \u0456\u043d\u0441\u0442\u0430\u043b\u044f\u0446\u0456\u0457", "os_name": "\u0421\u0456\u043c\u0435\u0439\u0441\u0442\u0432\u043e \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0438\u0445 \u0441\u0438\u0441\u0442\u0435\u043c", "os_version": "\u0412\u0435\u0440\u0441\u0456\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438", "python_version": "\u0412\u0435\u0440\u0441\u0456\u044f Python", - "supervisor": "Supervisor", "timezone": "\u0427\u0430\u0441\u043e\u0432\u0438\u0439 \u043f\u043e\u044f\u0441", "version": "\u0412\u0435\u0440\u0441\u0456\u044f", "virtualenv": "\u0412\u0456\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u0435 \u043e\u0442\u043e\u0447\u0435\u043d\u043d\u044f" diff --git a/homeassistant/components/homeassistant/translations/zh-Hans.json b/homeassistant/components/homeassistant/translations/zh-Hans.json index 6d6e1f2eed8..617866926b8 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hans.json +++ b/homeassistant/components/homeassistant/translations/zh-Hans.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "CPU \u67b6\u6784", - "chassis": "\u673a\u7bb1", "dev": "\u5f00\u53d1\u7248", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "\u5b89\u88c5\u7c7b\u578b", "os_name": "\u64cd\u4f5c\u7cfb\u7edf\u7cfb\u5217", "os_version": "\u64cd\u4f5c\u7cfb\u7edf\u7248\u672c", "python_version": "Python \u7248\u672c", - "supervisor": "Supervisor", "timezone": "\u65f6\u533a", "version": "\u7248\u672c", "virtualenv": "\u865a\u62df\u73af\u5883" diff --git a/homeassistant/components/homeassistant/translations/zh-Hant.json b/homeassistant/components/homeassistant/translations/zh-Hant.json index eba7a8034db..36f4fb70e24 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hant.json +++ b/homeassistant/components/homeassistant/translations/zh-Hant.json @@ -2,17 +2,13 @@ "system_health": { "info": { "arch": "CPU \u67b6\u69cb", - "chassis": "Chassis", "dev": "\u958b\u767c\u7248", "docker": "Docker", - "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS", "installation_type": "\u5b89\u88dd\u985e\u578b", "os_name": "\u4f5c\u696d\u7cfb\u7d71\u5bb6\u65cf", "os_version": "\u4f5c\u696d\u7cfb\u7d71\u7248\u672c", "python_version": "Python \u7248\u672c", - "supervisor": "Supervisor", "timezone": "\u6642\u5340", "version": "\u7248\u672c", "virtualenv": "\u865b\u64ec\u74b0\u5883" diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index 8093cb1792f..e836d81ac05 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -4,29 +4,13 @@ "port_name_in_use": "Ja hi ha un enlla\u00e7 o accessori configurat amb aquest nom o port." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Entitat" - }, - "description": "Escull l'entitat que vulguis incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat.", - "title": "Selecciona l'entitat a incloure" - }, - "bridge_mode": { - "data": { - "include_domains": "Dominis a incloure" - }, - "description": "Escull els dominis que vulguis incloure. S'inclouran totes les entitats del domini que siguin compatibles.", - "title": "Selecciona els dominis a incloure" - }, "pairing": { "description": "Per completar la vinculaci\u00f3, segueix les instruccions a \"Configuraci\u00f3 de l'enlla\u00e7 HomeKit\" sota \"Notificacions\".", "title": "Vinculaci\u00f3 HomeKit" }, "user": { "data": { - "auto_start": "Autoarrencada (desactiva-ho si fas servir Z-Wave o algun altre sistema d'inici lent)", - "include_domains": "Dominis a incloure", - "mode": "Mode" + "include_domains": "Dominis a incloure" }, "description": "Selecciona els dominis a incloure. S'inclouran totes les entitats del domini compatibles. Es crear\u00e0 una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", "title": "Selecciona els dominis a incloure" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Inici autom\u00e0tic (desactiva-ho si crides el servei homekit.start manualment)", - "safe_mode": "Mode segur (habilita-ho nom\u00e9s si falla la vinculaci\u00f3)" + "auto_start": "Inici autom\u00e0tic (desactiva-ho si crides el servei homekit.start manualment)" }, "description": "Aquests par\u00e0metres nom\u00e9s s'han d'ajustar si HomeKit no \u00e9s funcional.", "title": "Configuraci\u00f3 avan\u00e7ada" diff --git a/homeassistant/components/homekit/translations/cs.json b/homeassistant/components/homekit/translations/cs.json index cdfaed9183c..60b365ff684 100644 --- a/homeassistant/components/homekit/translations/cs.json +++ b/homeassistant/components/homekit/translations/cs.json @@ -4,18 +4,12 @@ "port_name_in_use": "P\u0159\u00edslu\u0161enstv\u00ed nebo p\u0159emost\u011bn\u00ed se stejn\u00fdm n\u00e1zvem nebo portem je ji\u017e nastaveno." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Entita" - } - }, "pairing": { "title": "P\u00e1rov\u00e1n\u00ed s HomeKit" }, "user": { "data": { - "include_domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty", - "mode": "Re\u017eim" + "include_domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty" }, "title": "Vyberte dom\u00e9ny, kter\u00e9 chcete zahrnout" } @@ -24,9 +18,6 @@ "options": { "step": { "advanced": { - "data": { - "safe_mode": "Nouzov\u00fd re\u017eim (povolit pouze v p\u0159\u00edpad\u011b, \u017ee p\u00e1rov\u00e1n\u00ed sel\u017ee)" - }, "description": "Tato nastaven\u00ed je t\u0159eba upravit pouze v p\u0159\u00edpad\u011b, \u017ee HomeKit nen\u00ed funk\u010dn\u00ed.", "title": "Pokro\u010dil\u00e9 nastaven\u00ed" }, diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index b1f5b23c264..09a6b059ea7 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -4,29 +4,13 @@ "port_name_in_use": "Eine HomeKit Bridge mit demselben Namen oder Port ist bereits vorhanden." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Entit\u00e4t" - }, - "description": "W\u00e4hle die Entit\u00e4t aus, die aufgenommen werden soll. Im Zubeh\u00f6rmodus ist nur eine einzelne Entit\u00e4t enthalten.", - "title": "W\u00e4hle die Entit\u00e4t aus, die aufgenommen werden soll" - }, - "bridge_mode": { - "data": { - "include_domains": "Einzubeziehende Domains" - }, - "description": "W\u00e4hle die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Ger\u00e4te innerhalb der Domain werden aufgenommen.", - "title": "W\u00e4hle die Domains aus, die aufgenommen werden sollen" - }, "pairing": { "description": "Um die Kopplung abzuschlie\u00dfen, folgen Sie den Anweisungen in \"Benachrichtigungen\" unter \"HomeKit-Kopplung\".", "title": "HomeKit verbinden" }, "user": { "data": { - "auto_start": "Autostart (deaktivieren, wenn Z-Wave oder ein anderes verz\u00f6gertes Startsystem verwendet wird)", - "include_domains": "Einzubeziehende Domains", - "mode": "Modus" + "include_domains": "Einzubeziehende Domains" }, "description": "W\u00e4hlen Sie die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Entit\u00e4ten in der Domain werden aufgenommen. F\u00fcr jeden TV-Mediaplayer und jede Kamera wird eine separate HomeKit-Instanz im Zubeh\u00f6rmodus erstellt.", "title": "HomeKit aktivieren" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (deaktivieren, wenn du den homekit.start-Dienst manuell aufrufst)", - "safe_mode": "Abgesicherter Modus (nur aktivieren, wenn das Pairing fehlschl\u00e4gt)" + "auto_start": "Autostart (deaktivieren, wenn du den homekit.start-Dienst manuell aufrufst)" }, "description": "Diese Einstellungen m\u00fcssen nur angepasst werden, wenn HomeKit nicht funktioniert.", "title": "Erweiterte Konfiguration" diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index a48b6fdee24..aa78c3e4adc 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -4,29 +4,13 @@ "port_name_in_use": "An accessory or bridge with the same name or port is already configured." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Entity" - }, - "description": "Choose the entity to be included. In accessory mode, only a single entity is included.", - "title": "Select entity to be included" - }, - "bridge_mode": { - "data": { - "include_domains": "Domains to include" - }, - "description": "Choose the domains to be included. All supported entities in the domain will be included.", - "title": "Select domains to be included" - }, "pairing": { "description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d.", "title": "Pair HomeKit" }, "user": { "data": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", - "include_domains": "Domains to include", - "mode": "Mode" + "include_domains": "Domains to include" }, "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.", "title": "Select domains to be included" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", - "safe_mode": "Safe Mode (enable only if pairing fails)" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" }, "description": "These settings only need to be adjusted if HomeKit is not functional.", "title": "Advanced Configuration" diff --git a/homeassistant/components/homekit/translations/es-419.json b/homeassistant/components/homekit/translations/es-419.json index 45e42250177..2b670f13c7e 100644 --- a/homeassistant/components/homekit/translations/es-419.json +++ b/homeassistant/components/homekit/translations/es-419.json @@ -10,7 +10,6 @@ }, "user": { "data": { - "auto_start": "Inicio autom\u00e1tico (deshabilitar si se usa Z-Wave u otro sistema de inicio diferido)", "include_domains": "Dominios para incluir" }, "description": "Un HomeKit Bridge le permitir\u00e1 acceder a sus entidades de Home Assistant en HomeKit. Los puentes HomeKit est\u00e1n limitados a 150 accesorios por instancia, incluido el puente mismo. Si desea unir m\u00e1s de la cantidad m\u00e1xima de accesorios, se recomienda que use m\u00faltiples puentes HomeKit para diferentes dominios. La configuraci\u00f3n detallada de la entidad solo est\u00e1 disponible a trav\u00e9s de YAML para el puente primario.", @@ -22,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Inicio autom\u00e1tico (deshabilitar si se usa Z-Wave u otro sistema de inicio diferido)", - "safe_mode": "Modo seguro (habil\u00edtelo solo si falla el emparejamiento)" + "auto_start": "Inicio autom\u00e1tico (deshabilitar si se usa Z-Wave u otro sistema de inicio diferido)" }, "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", "title": "Configuraci\u00f3n avanzada" diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 694b7dcdb6c..e713391eb9e 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -4,29 +4,13 @@ "port_name_in_use": "Ya est\u00e1 configurada una pasarela con el mismo nombre o puerto." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Entidad" - }, - "description": "Elija la entidad que desea incluir. En el modo accesorio, s\u00f3lo se incluye una \u00fanica entidad.", - "title": "Seleccione la entidad a incluir" - }, - "bridge_mode": { - "data": { - "include_domains": "Dominios a incluir" - }, - "description": "Elija los dominios que se van a incluir. Se incluir\u00e1n todas las entidades admitidas en el dominio.", - "title": "Selecciona los dominios a incluir" - }, "pairing": { "description": "Tan pronto como la pasarela {name} est\u00e9 lista, la vinculaci\u00f3n estar\u00e1 disponible en \"Notificaciones\" como \"configuraci\u00f3n de pasarela Homekit\"", "title": "Vincular pasarela Homekit" }, "user": { "data": { - "auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)", - "include_domains": "Dominios para incluir", - "mode": "Modo" + "include_domains": "Dominios para incluir" }, "description": "Una pasarela Homekit permitir\u00e1 a Homekit acceder a sus entidades de Home Assistant. La pasarela Homekit est\u00e1 limitada a 150 accesorios por instancia incluyendo la propia pasarela. Si desea enlazar m\u00e1s del m\u00e1ximo n\u00famero de accesorios, se recomienda que use multiples pasarelas Homekit para diferentes dominios. Configuraci\u00f3n detallada de la entidad solo est\u00e1 disponible via YAML para la pasarela primaria.", "title": "Activar pasarela Homekit" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)", - "safe_mode": "Modo seguro (habil\u00edtelo solo si falla el emparejamiento)" + "auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)" }, "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", "title": "Configuraci\u00f3n avanzada" diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index 38d063d9bf3..5814ad2069b 100644 --- a/homeassistant/components/homekit/translations/et.json +++ b/homeassistant/components/homekit/translations/et.json @@ -4,29 +4,13 @@ "port_name_in_use": "Sama nime v\u00f5i pordiga tarvik v\u00f5i sild on juba konfigureeritud." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Olem" - }, - "description": "Vali kaasatav olem. Lisare\u017eiimis on kaasatav ainult \u00fcks olem.", - "title": "Vali kaasatav olem" - }, - "bridge_mode": { - "data": { - "include_domains": "Kaasatavad domeenid" - }, - "description": "Vali kaasatavad domeenid. Kaasatakse k\u00f5ik domeenis toetatud olemid.", - "title": "Vali kaasatavad domeenid" - }, "pairing": { "description": "Sidumise l\u00f5puleviimiseks j\u00e4rgi jaotises \"HomeKiti sidumine\" toodud juhiseid alajaotises \"Teatised\".", "title": "HomeKiti sidumine" }, "user": { "data": { - "auto_start": "Autostart (keela, kui kasutad Z-Wave'i v\u00f5i muud viivitatud k\u00e4ivituss\u00fcsteemi)", - "include_domains": "Kaasatavad domeenid", - "mode": "Re\u017eiim" + "include_domains": "Kaasatavad domeenid" }, "description": "Vali kaasatavad domeenid. Kaasatakse k\u00f5ik domeenis toetatud olemid. Iga telemeedia pleieri ja kaamera jaoks luuakse eraldi HomeKiti eksemplar tarvikure\u017eiimis.", "title": "Vali kaasatavad domeenid" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (keela kui kasutad homekit.start teenust k\u00e4sitsi)", - "safe_mode": "Turvare\u017eiim (luba ainult siis, kui sidumine nurjub)" + "auto_start": "Autostart (keela kui kasutad homekit.start teenust k\u00e4sitsi)" }, "description": "Neid s\u00e4tteid tuleb muuta ainult siis kui HomeKit ei t\u00f6\u00f6ta.", "title": "T\u00e4psem seadistamine" diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index dae09002c54..018e93e18c9 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -4,29 +4,13 @@ "port_name_in_use": "Une passerelle avec le m\u00eame nom ou port est d\u00e9j\u00e0 configur\u00e9e." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Entit\u00e9" - }, - "description": "Choisissez l'entit\u00e9 \u00e0 inclure. En mode accessoire, une seule entit\u00e9 est incluse.", - "title": "S\u00e9lectionnez l'entit\u00e9 \u00e0 inclure" - }, - "bridge_mode": { - "data": { - "include_domains": "Domaines \u00e0 inclure" - }, - "description": "Choisissez les domaines \u00e0 inclure. Toutes les entit\u00e9s prises en charge dans le domaine seront incluses.", - "title": "S\u00e9lectionnez les domaines \u00e0 inclure" - }, "pairing": { "description": "Pour compl\u00e9ter l'appariement, suivez les instructions dans les \"Notifications\" sous \"Appariement HomeKit\".", "title": "Appairage de la Passerelle Homekit" }, "user": { "data": { - "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", - "include_domains": "Domaines \u00e0 inclure", - "mode": "Mode" + "include_domains": "Domaines \u00e0 inclure" }, "description": "Choisissez les domaines \u00e0 inclure. Toutes les entit\u00e9s prises en charge dans le domaine seront incluses. Une instance HomeKit distincte en mode accessoire sera cr\u00e9\u00e9e pour chaque lecteur multim\u00e9dia TV et cam\u00e9ra.", "title": "S\u00e9lectionnez les domaines \u00e0 inclure" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", - "safe_mode": "Mode sans \u00e9chec (activez uniquement si le jumelage \u00e9choue)" + "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)" }, "description": "Ces param\u00e8tres ne doivent \u00eatre ajust\u00e9s que si le pont HomeKit n'est pas fonctionnel.", "title": "Configuration avanc\u00e9e" diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json index 7cc2577cb31..1afc0183a0d 100644 --- a/homeassistant/components/homekit/translations/hu.json +++ b/homeassistant/components/homekit/translations/hu.json @@ -1,19 +1,12 @@ { "config": { "step": { - "accessory_mode": { - "data": { - "entity_id": "Entit\u00e1s" - }, - "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt entit\u00e1st" - }, "pairing": { "title": "HomeKit p\u00e1ros\u00edt\u00e1s" }, "user": { "data": { - "include_domains": "Felvenni k\u00edv\u00e1nt domainek", - "mode": "M\u00f3d" + "include_domains": "Felvenni k\u00edv\u00e1nt domainek" }, "title": "Felvenni k\u00edv\u00e1nt domainek kiv\u00e1laszt\u00e1sa" } diff --git a/homeassistant/components/homekit/translations/id.json b/homeassistant/components/homekit/translations/id.json index 588631a5215..ecb35196228 100644 --- a/homeassistant/components/homekit/translations/id.json +++ b/homeassistant/components/homekit/translations/id.json @@ -4,29 +4,13 @@ "port_name_in_use": "Aksesori atau bridge dengan nama atau port yang sama telah dikonfigurasi." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Entitas" - }, - "description": "Pilih entitas yang akan disertakan. Dalam mode aksesori, hanya satu entitas yang disertakan.", - "title": "Pilih entitas yang akan disertakan" - }, - "bridge_mode": { - "data": { - "include_domains": "Domain yang disertakan" - }, - "description": "Pilih domain yang akan disertakan. Semua entitas yang didukung di domain akan disertakan.", - "title": "Pilih domain yang akan disertakan" - }, "pairing": { "description": "Untuk menyelesaikan pemasangan ikuti petunjuk di \"Notifikasi\" di bawah \"Pemasangan HomeKit\".", "title": "Pasangkan HomeKit" }, "user": { "data": { - "auto_start": "Mulai otomatis (nonaktifkan jika menggunakan Z-Wave atau sistem mulai tertunda lainnya)", - "include_domains": "Domain yang disertakan", - "mode": "Mode" + "include_domains": "Domain yang disertakan" }, "description": "Pilih domain yang akan disertakan. Semua entitas yang didukung di domain akan disertakan. Instans HomeKit terpisah dalam mode aksesori akan dibuat untuk setiap pemutar media TV dan kamera.", "title": "Pilih domain yang akan disertakan" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Mulai otomatis (nonaktifkan jika Anda memanggil layanan homekit.start secara manual)", - "safe_mode": "Mode Aman (aktifkan hanya jika pemasangan gagal)" + "auto_start": "Mulai otomatis (nonaktifkan jika Anda memanggil layanan homekit.start secara manual)" }, "description": "Pengaturan ini hanya perlu disesuaikan jika HomeKit tidak berfungsi.", "title": "Konfigurasi Tingkat Lanjut" diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index c61aececec7..0fb983f1a20 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -4,29 +4,13 @@ "port_name_in_use": "Un accessorio o un bridge con lo stesso nome o porta \u00e8 gi\u00e0 configurato." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Entit\u00e0" - }, - "description": "Scegli l'entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa solo una singola entit\u00e0.", - "title": "Seleziona l'entit\u00e0 da includere" - }, - "bridge_mode": { - "data": { - "include_domains": "Domini da includere" - }, - "description": "Scegli i domini da includere. Verranno incluse tutte le entit\u00e0 supportate nel dominio.", - "title": "Seleziona i domini da includere" - }, "pairing": { "description": "Per completare l'associazione, seguire le istruzioni in \"Notifiche\" sotto \"Associazione HomeKit\".", "title": "Associa HomeKit" }, "user": { "data": { - "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)", - "include_domains": "Domini da includere", - "mode": "Modalit\u00e0" + "include_domains": "Domini da includere" }, "description": "Scegli i domini da includere. Verranno incluse tutte le entit\u00e0 supportate nel dominio. Verr\u00e0 creata un'istanza HomeKit separata in modalit\u00e0 accessorio per ogni lettore multimediale TV e telecamera.", "title": "Seleziona i domini da includere" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Avvio automatico (disabilitare se stai chiamando manualmente il servizio homekit.start)", - "safe_mode": "Modalit\u00e0 provvisoria (attivare solo in caso di errore di associazione)" + "auto_start": "Avvio automatico (disabilitare se stai chiamando manualmente il servizio homekit.start)" }, "description": "Queste impostazioni devono essere regolate solo se HomeKit non funziona.", "title": "Configurazione Avanzata" diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json index b9b04aec0a7..e93a55db971 100644 --- a/homeassistant/components/homekit/translations/ko.json +++ b/homeassistant/components/homekit/translations/ko.json @@ -4,29 +4,13 @@ "port_name_in_use": "\uc774\ub984\uc774\ub098 \ud3ec\ud2b8\uac00 \uac19\uc740 \ube0c\ub9ac\uc9c0 \ub610\ub294 \uc561\uc138\uc11c\ub9ac\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "\uad6c\uc131\uc694\uc18c" - }, - "description": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \ud3ec\ud568\ub429\ub2c8\ub2e4.", - "title": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c \uc120\ud0dd\ud558\uae30" - }, - "bridge_mode": { - "data": { - "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778" - }, - "description": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc9c0\uc6d0\ub418\ub294 \ub3c4\uba54\uc778\uc758 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4.", - "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" - }, "pairing": { "description": "\"\uc54c\ub9bc\"\uc5d0\uc11c \"HomeKit Pairing\"\uc5d0 \uc788\ub294 \uc548\ub0b4\uc5d0 \ub530\ub77c \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", "title": "HomeKit \ud398\uc5b4\ub9c1\ud558\uae30" }, "user": { "data": { - "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (Z-Wave \ub610\ub294 \uae30\ud0c0 \uc9c0\uc5f0\ub41c \uc2dc\uc791 \uc2dc\uc2a4\ud15c\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", - "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778", - "mode": "\ubaa8\ub4dc" + "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778" }, "description": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ub3c4\uba54\uc778\uc5d0\uc11c \uc9c0\uc6d0\ub418\ub294 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \uac01 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \uce74\uba54\ub77c\uc5d0 \ub300\ud574 \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc758 \uac1c\ubcc4 HomeKit \uc778\uc2a4\ud134\uc2a4\uac00 \uc0dd\uc131\ub429\ub2c8\ub2e4.", "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (homekit.start \uc11c\ube44\uc2a4\ub97c \uc218\ub3d9\uc73c\ub85c \ud638\ucd9c\ud558\ub824\uba74 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", - "safe_mode": "\uc548\uc804 \ubaa8\ub4dc (\ud398\uc5b4\ub9c1\uc774 \uc2e4\ud328\ud55c \uacbd\uc6b0\uc5d0\ub9cc \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)" + "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (homekit.start \uc11c\ube44\uc2a4\ub97c \uc218\ub3d9\uc73c\ub85c \ud638\ucd9c\ud558\ub824\uba74 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)" }, "description": "\uc774 \uc124\uc815\uc740 HomeKit\uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "\uace0\uae09 \uad6c\uc131" diff --git a/homeassistant/components/homekit/translations/lb.json b/homeassistant/components/homekit/translations/lb.json index 97c1bc23aa8..6261d4b5ed4 100644 --- a/homeassistant/components/homekit/translations/lb.json +++ b/homeassistant/components/homekit/translations/lb.json @@ -10,7 +10,6 @@ }, "user": { "data": { - "auto_start": "Autostart (d\u00e9aktiv\u00e9ier falls Z-Wave oder een aanere verz\u00f6gerte Start System benotzt g\u00ebtt)", "include_domains": "Domaine d\u00e9i solle abegraff ginn." }, "description": "HomeKit Integratioun erlaabt et Home Assistant Entit\u00e9iten am HomeKit z'acc\u00e9d\u00e9ieren. HomeKit Bridges sinn op 150 Accessoire limit\u00e9iert, mat der Bridge selwer. Falls d'Bridge m\u00e9i w\u00e9i d\u00e9i max. Unzuel vun Accessoire soll \u00ebnnerst\u00ebtzen ass et recommand\u00e9iert verschidden HomeKit Bridges fir verschidden Domaine anzesetzen. Eng detaill\u00e9iert Konfiguratioun ass n\u00ebmme via YAML fir d\u00e9i prim\u00e4r Bridge verf\u00fcgbar.", @@ -22,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (d\u00e9aktiv\u00e9ier falls Z-Wave oder een aanere verz\u00f6gerte Start System benotzt g\u00ebtt)", - "safe_mode": "Safe Mode (n\u00ebmmen aktiv\u00e9ieren wann Kopplung net geht)" + "auto_start": "Autostart (d\u00e9aktiv\u00e9ier falls Z-Wave oder een aanere verz\u00f6gerte Start System benotzt g\u00ebtt)" }, "description": "D\u00ebs Astellungen brauche n\u00ebmmen ajust\u00e9iert ze ginn falls HomeKit net funktion\u00e9iert.", "title": "Erweidert Konfiguratioun" diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 154f271e1a3..c2751e9eb6b 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -4,29 +4,13 @@ "port_name_in_use": "Er is al een bridge of apparaat met dezelfde naam of poort geconfigureerd." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Entiteit" - }, - "description": "Kies de entiteit die moet worden opgenomen. In de accessoiremodus wordt slechts \u00e9\u00e9n entiteit opgenomen.", - "title": "Selecteer de entiteit die u wilt opnemen" - }, - "bridge_mode": { - "data": { - "include_domains": "Domeinen om op te nemen" - }, - "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen.", - "title": "Selecteer domeinen die u wilt opnemen" - }, "pairing": { "description": "Volg de instructies in \"Meldingen\" onder \"HomeKit-koppeling\" om het koppelen te voltooien.", "title": "Koppel HomeKit" }, "user": { "data": { - "auto_start": "Automatisch starten (uitschakelen als u Z-Wave of een ander vertraagd startsysteem gebruikt)", - "include_domains": "Domeinen om op te nemen", - "mode": "Mode" + "include_domains": "Domeinen om op te nemen" }, "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen. Voor elke tv-mediaspeler en camera wordt een afzonderlijke HomeKit-instantie in accessoiremodus aangemaakt.", "title": "Selecteer domeinen die u wilt opnemen" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (deactiveer als je de homekit.start service handmatig aanroept)", - "safe_mode": "Veilige modus (alleen inschakelen als het koppelen mislukt)" + "auto_start": "Autostart (deactiveer als je de homekit.start service handmatig aanroept)" }, "description": "Deze instellingen hoeven alleen te worden aangepast als HomeKit niet functioneert.", "title": "Geavanceerde configuratie" diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index e18f9224c68..7de5494c56a 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -4,29 +4,13 @@ "port_name_in_use": "Et tilbeh\u00f8r eller bro med samme navn eller port er allerede konfigurert." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Enhet" - }, - "description": "Velg enheten som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert.", - "title": "Velg enhet som skal inkluderes" - }, - "bridge_mode": { - "data": { - "include_domains": "Domener \u00e5 inkludere" - }, - "description": "Velg domenene som skal inkluderes. Alle st\u00f8ttede enheter i domenet vil bli inkludert.", - "title": "Velg domener som skal inkluderes" - }, "pairing": { "description": "For \u00e5 fullf\u00f8re sammenkoblingen ved \u00e5 f\u00f8lge instruksjonene i \"Varsler\" under \"Sammenkobling av HomeKit\".", "title": "Koble sammen HomeKit" }, "user": { "data": { - "auto_start": "Autostart (deaktiver hvis du bruker Z-Wave eller annet forsinket startsystem)", - "include_domains": "Domener \u00e5 inkludere", - "mode": "Modus" + "include_domains": "Domener \u00e5 inkludere" }, "description": "Velg domenene som skal inkluderes. Alle st\u00f8ttede enheter i domenet vil bli inkludert. Det opprettes en egen HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediaspiller og kamera.", "title": "Velg domener som skal inkluderes" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (deaktiver hvis du ringer til homekit.start-tjenesten manuelt)", - "safe_mode": "Sikker modus (aktiver bare hvis sammenkoblingen mislykkes)" + "auto_start": "Autostart (deaktiver hvis du ringer til homekit.start-tjenesten manuelt)" }, "description": "Disse innstillingene m\u00e5 bare justeres hvis HomeKit ikke fungerer.", "title": "Avansert konfigurasjon" diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index bcd088762ca..2cd687ebc47 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -4,29 +4,13 @@ "port_name_in_use": "Akcesorium lub mostek o tej samej nazwie lub adresie IP jest ju\u017c skonfigurowany" }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Encja" - }, - "description": "Wybierz uwzgl\u0119dniane encje. W trybie akcesori\u00f3w uwzgl\u0119dniana jest tylko jedna encja.", - "title": "Wybierz uwzgl\u0119dniane encje" - }, - "bridge_mode": { - "data": { - "include_domains": "Domeny do uwzgl\u0119dnienia" - }, - "description": "Wybierz uwzgl\u0119dniane domeny. Wszystkie obs\u0142ugiwane encje w domenie zostan\u0105 uwzgl\u0119dnione.", - "title": "Wybierz uwzgl\u0119dniane domeny" - }, "pairing": { "description": "Aby doko\u0144czy\u0107 parowanie, post\u0119puj wg instrukcji \u201eParowanie HomeKit\u201d w \u201ePowiadomieniach\u201d.", "title": "Parowanie z HomeKit" }, "user": { "data": { - "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli u\u017cywasz Z-Wave lub innej integracji op\u00f3\u017aniaj\u0105cej start systemu)", - "include_domains": "Domeny do uwzgl\u0119dnienia", - "mode": "Tryb" + "include_domains": "Domeny do uwzgl\u0119dnienia" }, "description": "Wybierz domeny do uwzgl\u0119dnienia. Wszystkie wspierane encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione. W trybie akcesorium, oddzielna instancja HomeKit zostanie utworzona dla ka\u017cdego tv media playera oraz kamery.", "title": "Wybierz uwzgl\u0119dniane domeny" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli r\u0119cznie uruchamiasz us\u0142ug\u0119 homekit.start)", - "safe_mode": "Tryb awaryjny (w\u0142\u0105cz tylko wtedy, gdy parowanie nie powiedzie si\u0119)" + "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli r\u0119cznie uruchamiasz us\u0142ug\u0119 homekit.start)" }, "description": "Te ustawienia nale\u017cy dostosowa\u0107 tylko wtedy, gdy HomeKit nie dzia\u0142a.", "title": "Konfiguracja zaawansowana" diff --git a/homeassistant/components/homekit/translations/pt.json b/homeassistant/components/homekit/translations/pt.json index b5da3fdfc97..920beaa98df 100644 --- a/homeassistant/components/homekit/translations/pt.json +++ b/homeassistant/components/homekit/translations/pt.json @@ -15,6 +15,9 @@ "options": { "step": { "advanced": { + "data": { + "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]" + }, "title": "Configura\u00e7\u00e3o avan\u00e7ada" }, "cameras": { diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index 81199b2971c..6b85983073a 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -4,29 +4,13 @@ "port_name_in_use": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0438\u043b\u0438 \u043f\u043e\u0440\u0442\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "\u041e\u0431\u044a\u0435\u043a\u0442" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442.", - "title": "\u0412\u044b\u0431\u043e\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" - }, - "bridge_mode": { - "data": { - "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u044b. \u0411\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0437 \u0434\u043e\u043c\u0435\u043d\u0430.", - "title": "\u0412\u044b\u0431\u043e\u0440 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" - }, "pairing": { "description": "\u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u0441\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u043c \u0432 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0438 \"HomeKit Pairing\".", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 HomeKit" }, "user": { "data": { - "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", - "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b", - "mode": "\u0420\u0435\u0436\u0438\u043c" + "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u044b. \u0411\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0437 \u0434\u043e\u043c\u0435\u043d\u0430. \u0414\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430.", "title": "\u0412\u044b\u0431\u043e\u0440 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0435 \u0441\u043b\u0443\u0436\u0431\u0443 homekit.start)", - "safe_mode": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0431\u043e\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f)" + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0435 \u0441\u043b\u0443\u0436\u0431\u0443 homekit.start)" }, "description": "\u042d\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 HomeKit \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", "title": "\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" diff --git a/homeassistant/components/homekit/translations/sl.json b/homeassistant/components/homekit/translations/sl.json index caeba3a9b6c..dcfc2496011 100644 --- a/homeassistant/components/homekit/translations/sl.json +++ b/homeassistant/components/homekit/translations/sl.json @@ -10,7 +10,6 @@ }, "user": { "data": { - "auto_start": "Samodejni zagon (onemogo\u010di, \u010de uporabljate Z-Wave ali drug sistem z zakasnjenim zagonom)", "include_domains": "Domene, ki jih \u017eelite vklju\u010diti" }, "description": "HomeKit most vam bo omogo\u010dil dostop do Home Assistant entitet v HomeKit-u. HomeKit mostovi so omejeni na 150 entitet na primerek, vklju\u010dno z mostom. \u010ce \u017eelite premostiti dovoljeno \u0161tevilo dodatkov, priporo\u010damo, da uporabite ve\u010d mostov HomeKit za razli\u010dne domene. Podrobna konfiguracija entitete je na voljo samo prek YAML za primarni most.", @@ -22,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Samodejni zagon (onemogo\u010dite, \u010de uporabljate Z-wave ali kakteri drug sistem z zakasnjenim zagonom)", - "safe_mode": "Varni na\u010din (omogo\u010dite samo, \u010de seznanjanje ne uspe)" + "auto_start": "Samodejni zagon (onemogo\u010dite, \u010de uporabljate Z-wave ali kakteri drug sistem z zakasnjenim zagonom)" }, "description": "Te nastavitve je treba prilagoditi le, \u010de most HomeKit ni funkcionalen.", "title": "Napredna konfiguracija" diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json index 1e2fcae04b5..0bc23c456ff 100644 --- a/homeassistant/components/homekit/translations/sv.json +++ b/homeassistant/components/homekit/translations/sv.json @@ -1,11 +1,6 @@ { "config": { "step": { - "bridge_mode": { - "data": { - "include_domains": "Dom\u00e4ner att inkludera" - } - }, "pairing": { "title": "Para HomeKit" }, diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json index f9391fd0686..6b196c25da5 100644 --- a/homeassistant/components/homekit/translations/tr.json +++ b/homeassistant/components/homekit/translations/tr.json @@ -4,38 +4,14 @@ "port_name_in_use": "Ayn\u0131 ada veya ba\u011flant\u0131 noktas\u0131na sahip bir aksesuar veya k\u00f6pr\u00fc zaten yap\u0131land\u0131r\u0131lm\u0131\u015f." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Varl\u0131k" - }, - "description": "Dahil edilecek varl\u0131\u011f\u0131 se\u00e7in. Aksesuar modunda, yaln\u0131zca tek bir varl\u0131k dahildir.", - "title": "Dahil edilecek varl\u0131\u011f\u0131 se\u00e7in" - }, - "bridge_mode": { - "data": { - "include_domains": "\u0130\u00e7erecek etki alanlar\u0131" - }, - "description": "Dahil edilecek alanlar\u0131 se\u00e7in. Etki alan\u0131ndaki t\u00fcm desteklenen varl\u0131klar dahil edilecektir.", - "title": "Dahil edilecek etki alanlar\u0131n\u0131 se\u00e7in" - }, "pairing": { "description": "{name} haz\u0131r olur olmaz e\u015fle\u015ftirme, \"Bildirimler\" i\u00e7inde \"HomeKit K\u00f6pr\u00fc Kurulumu\" olarak mevcut olacakt\u0131r.", "title": "HomeKit'i E\u015fle\u015ftir" - }, - "user": { - "data": { - "mode": "Mod" - } } } }, "options": { "step": { - "advanced": { - "data": { - "safe_mode": "G\u00fcvenli Mod (yaln\u0131zca e\u015fle\u015ftirme ba\u015far\u0131s\u0131z olursa etkinle\u015ftirin)" - } - }, "cameras": { "data": { "camera_copy": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen kameralar" diff --git a/homeassistant/components/homekit/translations/uk.json b/homeassistant/components/homekit/translations/uk.json index 876b200bdf8..0210380ad38 100644 --- a/homeassistant/components/homekit/translations/uk.json +++ b/homeassistant/components/homekit/translations/uk.json @@ -10,7 +10,6 @@ }, "user": { "data": { - "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 Z-Wave \u0430\u0431\u043e \u0456\u043d\u0448\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u0432\u0456\u0434\u043a\u043b\u0430\u0434\u0435\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0443)", "include_domains": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438 \u0434\u043e\u043c\u0435\u043d\u0438" }, "description": "\u0426\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0434\u043e\u0437\u0432\u043e\u043b\u044f\u0454 \u043e\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u043e\u0431'\u0454\u043a\u0442\u0456\u0432 Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u0438\u0439 150 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u0435\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0447\u0438 \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u042f\u043a\u0449\u043e \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0431\u0456\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 HomeKit Bridge \u0434\u043b\u044f \u0440\u0456\u0437\u043d\u0438\u0445 \u0434\u043e\u043c\u0435\u043d\u0456\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u043a\u043e\u0436\u043d\u043e\u0433\u043e \u043e\u0431'\u0454\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0435 \u0442\u0456\u043b\u044c\u043a\u0438 \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u043c\u043e\u0441\u0442\u0430.", @@ -22,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 Z-Wave \u0430\u0431\u043e \u0456\u043d\u0448\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u0432\u0456\u0434\u043a\u043b\u0430\u0434\u0435\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0443)", - "safe_mode": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c (\u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0442\u0456\u043b\u044c\u043a\u0438 \u0432 \u0440\u0430\u0437\u0456 \u0437\u0431\u043e\u044e \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f)" + "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]" }, "description": "\u0426\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0456, \u043b\u0438\u0448\u0435 \u044f\u043a\u0449\u043e HomeKit \u043d\u0435 \u043f\u0440\u0430\u0446\u044e\u0454.", "title": "\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" diff --git a/homeassistant/components/homekit/translations/zh-Hans.json b/homeassistant/components/homekit/translations/zh-Hans.json index d11234f4c6d..e85c492d3cd 100644 --- a/homeassistant/components/homekit/translations/zh-Hans.json +++ b/homeassistant/components/homekit/translations/zh-Hans.json @@ -10,7 +10,6 @@ }, "user": { "data": { - "auto_start": "\u81ea\u52a8\u542f\u52a8\uff08\u5982\u679c\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u8fdf\u542f\u52a8\u7cfb\u7edf\uff0c\u8bf7\u7981\u7528\u6b64\u9879\uff09", "include_domains": "\u8981\u5305\u542b\u7684\u57df" }, "description": "HomeKit \u96c6\u6210\u53ef\u4ee5\u8ba9\u60a8\u901a\u8fc7 HomeKit \u8bbf\u95ee Home Assistant \u4e2d\u7684\u5b9e\u4f53\u3002\u5728\u6865\u63a5\u6a21\u5f0f\u4e2d\uff0c\u6bcf\u4e2a\u6865\u63a5\u5668\u5b9e\u4f8b\u6700\u591a\u53ef\u6a21\u62df 150 \u4e2a\u914d\u4ef6\uff0c\u5305\u62ec\u6865\u63a5\u5668\u672c\u8eab\u3002\u5982\u679c\u60a8\u5e0c\u671b\u6865\u63a5\u7684\u914d\u4ef6\u591a\u4e8e\u6b64\u6570\u91cf\uff0c\u5efa\u8bae\u4e3a\u4e0d\u540c\u7684\u57df\u4f7f\u7528\u591a\u4e2a HomeKit \u6865\u63a5\u5668\u3002\u8be6\u7ec6\u7684\u5b9e\u4f53\u914d\u7f6e\u4ec5\u53ef\u7528\u4e8e\u4e3b\u6865\u63a5\u5668\uff0c\u4e14\u987b\u901a\u8fc7 YAML \u914d\u7f6e\u3002", @@ -22,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "\u81ea\u52a8\u542f\u52a8\uff08\u5982\u679c\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u8fdf\u542f\u52a8\u7cfb\u7edf\uff0c\u8bf7\u7981\u7528\u6b64\u9879\uff09", - "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u4ec5\u5728\u914d\u5bf9\u5931\u8d25\u65f6\u542f\u7528\uff09" + "auto_start": "[%key_id:43661779%]" }, "description": "\u8fd9\u4e9b\u8bbe\u7f6e\u53ea\u6709\u5f53 HomeKit \u529f\u80fd\u4e0d\u6b63\u5e38\u65f6\u624d\u9700\u8981\u8c03\u6574\u3002", "title": "\u9ad8\u7ea7\u914d\u7f6e" diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index 09f9220c20f..5ef479ff0dd 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -4,29 +4,13 @@ "port_name_in_use": "\u4f7f\u7528\u76f8\u540c\u540d\u7a31\u6216\u901a\u8a0a\u57e0\u7684\u914d\u4ef6\u6216 Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" }, "step": { - "accessory_mode": { - "data": { - "entity_id": "\u5be6\u9ad4" - }, - "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\uff0c\u50c5\u80fd\u5305\u542b\u55ae\u4e00\u5be6\u9ad4\u3002", - "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4" - }, - "bridge_mode": { - "data": { - "include_domains": "\u5305\u542b\u7db2\u57df" - }, - "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df\u3002\u6240\u6709\u7db2\u57df\u5167\u652f\u63f4\u7684\u5be6\u9ad4\u90fd\u6703\u5305\u542b\u3002", - "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df" - }, "pairing": { "description": "\u6b32\u5b8c\u6210\u914d\u5c0d\u3001\u8acb\u8ddf\u96a8\u300c\u901a\u77e5\u300d\u5167\u7684\u300cHomekit \u914d\u5c0d\u300d\u6307\u5f15\u3002", "title": "\u914d\u5c0d HomeKit" }, "user": { "data": { - "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", - "include_domains": "\u5305\u542b\u7db2\u57df", - "mode": "\u6a21\u5f0f" + "include_domains": "\u5305\u542b\u7db2\u57df" }, "description": "\u9078\u64c7\u6240\u8981\u5305\u542b\u7684\u7db2\u57df\uff0c\u6240\u6709\u8a72\u7db2\u57df\u5167\u652f\u63f4\u7684\u5be6\u9ad4\u90fd\u5c07\u6703\u88ab\u5305\u542b\u3002 \u5176\u4ed6 Homekit \u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\u5be6\u4f8b\uff0c\u5c07\u6703\u4ee5\u914d\u4ef6\u6a21\u5f0f\u65b0\u589e\u3002", "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u624b\u52d5\u4f7f\u7528 homekit.start \u670d\u52d9\u6642\u3001\u8acb\u95dc\u9589\uff09", - "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u50c5\u65bc\u914d\u5c0d\u5931\u6557\u6642\u4f7f\u7528\uff09" + "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u624b\u52d5\u4f7f\u7528 homekit.start \u670d\u52d9\u6642\u3001\u8acb\u95dc\u9589\uff09" }, "description": "\u50c5\u65bc Homekit \u7121\u6cd5\u6b63\u5e38\u4f7f\u7528\u6642\uff0c\u8abf\u6574\u6b64\u4e9b\u8a2d\u5b9a\u3002", "title": "\u9032\u968e\u8a2d\u5b9a" diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json index 0ce5c2b2ba3..e46e083f1bd 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -17,7 +17,7 @@ "unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.", "unknown_error": "El dispositiu ha em\u00e8s un error desconegut. Vinculaci\u00f3 fallida." }, - "flow_title": "{name} a trav\u00e9s de HomeKit Accessory Protocol", + "flow_title": "{name}", "step": { "busy_error": { "description": "Atura la vinculaci\u00f3 a tots els controladors o prova de reiniciar el dispositiu, despr\u00e9s, segueix amb la vinculaci\u00f3.", diff --git a/homeassistant/components/homekit_controller/translations/et.json b/homeassistant/components/homekit_controller/translations/et.json index 6df49751478..6a35fd22943 100644 --- a/homeassistant/components/homekit_controller/translations/et.json +++ b/homeassistant/components/homekit_controller/translations/et.json @@ -17,7 +17,7 @@ "unable_to_pair": "Ei saa siduda, proovi uuesti.", "unknown_error": "Seade teatas tundmatust t\u00f5rkest. Sidumine nurjus." }, - "flow_title": "{name} HomeKitAccessory Protocol abil", + "flow_title": "{name}", "step": { "busy_error": { "description": "Katkesta sidumine k\u00f5igis kontrollerites v\u00f5i proovi seade taask\u00e4ivitada ja j\u00e4tka sidumist.", diff --git a/homeassistant/components/homekit_controller/translations/no.json b/homeassistant/components/homekit_controller/translations/no.json index b0680ea5ba0..81e94e6a117 100644 --- a/homeassistant/components/homekit_controller/translations/no.json +++ b/homeassistant/components/homekit_controller/translations/no.json @@ -17,7 +17,7 @@ "unable_to_pair": "Kunne ikke koble til, vennligst pr\u00f8v igjen.", "unknown_error": "Enheten rapporterte en ukjent feil. Sammenkobling mislyktes." }, - "flow_title": "{name} via HomeKit tilbeh\u00f8rsprotokoll", + "flow_title": "{name}", "step": { "busy_error": { "description": "Avbryt sammenkobling p\u00e5 alle kontrollere, eller pr\u00f8v \u00e5 starte enheten p\u00e5 nytt, og fortsett deretter med \u00e5 fortsette sammenkoblingen.", diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json index 16e4c611efe..2bb6eefbe7b 100644 --- a/homeassistant/components/homekit_controller/translations/ru.json +++ b/homeassistant/components/homekit_controller/translations/ru.json @@ -17,7 +17,7 @@ "unable_to_pair": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\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.", "unknown_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u043e\u043e\u0431\u0449\u0438\u043b\u043e \u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435. \u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c." }, - "flow_title": "{name} \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u0432 HomeKit", + "flow_title": "{name}", "step": { "busy_error": { "description": "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0432\u0441\u0435\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430\u0445 \u0438\u043b\u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index 6490904a32e..dd0082d8d11 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -17,7 +17,7 @@ "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" }, - "flow_title": "{name} \u4f7f\u7528 HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a", + "flow_title": "{name}", "step": { "busy_error": { "description": "\u53d6\u6d88\u6240\u6709\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u6216\u8005\u91cd\u555f\u88dd\u7f6e\u3001\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index 73c5bc9b8e1..92966ca7eeb 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -15,7 +15,7 @@ "response_error": "S'ha produ\u00eft un error desconegut del dispositiu", "unknown": "Error inesperat" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 61b8a2e50d1..c94791ee592 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -34,6 +34,7 @@ "data": { "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", + "track_new_devices": "Track new devices", "track_wired_clients": "Track wired network clients" } } diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json index 3c674c0344c..8cd61842767 100644 --- a/homeassistant/components/huawei_lte/translations/et.json +++ b/homeassistant/components/huawei_lte/translations/et.json @@ -15,7 +15,7 @@ "response_error": "Seade andis tuvastamatu t\u00f5rke", "unknown": "Ootamatu t\u00f5rge" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index 4a9966c9339..a328858c57f 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -15,7 +15,7 @@ "response_error": "Ukjent feil fra enheten", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index d679f8d2861..34a00f0523c 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -15,7 +15,7 @@ "response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index bc929fdcbba..906a4fdc011 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -15,7 +15,7 @@ "response_error": "\u4f86\u81ea\u88dd\u7f6e\u672a\u77e5\u932f\u8aa4", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "\u83ef\u70ba LTE\uff1a{name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huisbaasje/translations/ca.json b/homeassistant/components/huisbaasje/translations/ca.json index 3ee45b4c38b..9677f944330 100644 --- a/homeassistant/components/huisbaasje/translations/ca.json +++ b/homeassistant/components/huisbaasje/translations/ca.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "connection_exception": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unauthenticated_exception": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/cs.json b/homeassistant/components/huisbaasje/translations/cs.json index 8c89c265fe5..dc27752e935 100644 --- a/homeassistant/components/huisbaasje/translations/cs.json +++ b/homeassistant/components/huisbaasje/translations/cs.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "connection_exception": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "unauthenticated_exception": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/de.json b/homeassistant/components/huisbaasje/translations/de.json index 5f8b8ef4c1a..0eee2778d05 100644 --- a/homeassistant/components/huisbaasje/translations/de.json +++ b/homeassistant/components/huisbaasje/translations/de.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "connection_exception": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unauthenticated_exception": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/en.json b/homeassistant/components/huisbaasje/translations/en.json index 42bb4b59196..cb0e7bed7ea 100644 --- a/homeassistant/components/huisbaasje/translations/en.json +++ b/homeassistant/components/huisbaasje/translations/en.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "Failed to connect", - "connection_exception": "Failed to connect", "invalid_auth": "Invalid authentication", - "unauthenticated_exception": "Invalid authentication", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/es.json b/homeassistant/components/huisbaasje/translations/es.json index a66da5b00d9..d537185eb68 100644 --- a/homeassistant/components/huisbaasje/translations/es.json +++ b/homeassistant/components/huisbaasje/translations/es.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "connection_exception": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "unauthenticated_exception": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/et.json b/homeassistant/components/huisbaasje/translations/et.json index b4170142778..ce02ca14929 100644 --- a/homeassistant/components/huisbaasje/translations/et.json +++ b/homeassistant/components/huisbaasje/translations/et.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "\u00dchendamine nurjus", - "connection_exception": "\u00dchendamine nurjus", "invalid_auth": "Vigane autentimine", - "unauthenticated_exception": "Vigane autentimine", "unknown": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/fr.json b/homeassistant/components/huisbaasje/translations/fr.json index 567f4a08f4a..9012293fa48 100644 --- a/homeassistant/components/huisbaasje/translations/fr.json +++ b/homeassistant/components/huisbaasje/translations/fr.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "[%key::common::config_flow::error::cannot_connect%]", - "connection_exception": "\u00c9chec de la connexion ", "invalid_auth": "Authentification invalide ", - "unauthenticated_exception": "Authentification invalide ", "unknown": "Erreur inatendue" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/hu.json b/homeassistant/components/huisbaasje/translations/hu.json index 9d94d9d76ab..fd8db27da5e 100644 --- a/homeassistant/components/huisbaasje/translations/hu.json +++ b/homeassistant/components/huisbaasje/translations/hu.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "connection_exception": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unauthenticated_exception": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/id.json b/homeassistant/components/huisbaasje/translations/id.json index c83d53b3849..4a84db42a14 100644 --- a/homeassistant/components/huisbaasje/translations/id.json +++ b/homeassistant/components/huisbaasje/translations/id.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "Gagal terhubung", - "connection_exception": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", - "unauthenticated_exception": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/it.json b/homeassistant/components/huisbaasje/translations/it.json index a8f899f9f82..843262aa318 100644 --- a/homeassistant/components/huisbaasje/translations/it.json +++ b/homeassistant/components/huisbaasje/translations/it.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "connection_exception": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", - "unauthenticated_exception": "Autenticazione non valida", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/ko.json b/homeassistant/components/huisbaasje/translations/ko.json index 19387dfe542..94261de9637 100644 --- a/homeassistant/components/huisbaasje/translations/ko.json +++ b/homeassistant/components/huisbaasje/translations/ko.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "connection_exception": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "unauthenticated_exception": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/nl.json b/homeassistant/components/huisbaasje/translations/nl.json index a13c1837b9f..50b4c3f2fe6 100644 --- a/homeassistant/components/huisbaasje/translations/nl.json +++ b/homeassistant/components/huisbaasje/translations/nl.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "connection_exception": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", - "unauthenticated_exception": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/no.json b/homeassistant/components/huisbaasje/translations/no.json index 3eebfb72df2..4ea7b2401c3 100644 --- a/homeassistant/components/huisbaasje/translations/no.json +++ b/homeassistant/components/huisbaasje/translations/no.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", - "connection_exception": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", - "unauthenticated_exception": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/pl.json b/homeassistant/components/huisbaasje/translations/pl.json index c87d3d0be7d..8a08a06c699 100644 --- a/homeassistant/components/huisbaasje/translations/pl.json +++ b/homeassistant/components/huisbaasje/translations/pl.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "connection_exception": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", - "unauthenticated_exception": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/ru.json b/homeassistant/components/huisbaasje/translations/ru.json index c9fbe5cdcb2..aef0fdff54e 100644 --- a/homeassistant/components/huisbaasje/translations/ru.json +++ b/homeassistant/components/huisbaasje/translations/ru.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "connection_exception": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "unauthenticated_exception": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/tr.json b/homeassistant/components/huisbaasje/translations/tr.json index fa5bd311286..80f1529066b 100644 --- a/homeassistant/components/huisbaasje/translations/tr.json +++ b/homeassistant/components/huisbaasje/translations/tr.json @@ -4,9 +4,7 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "connection_exception": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "unauthenticated_exception": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen Hata" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/zh-Hant.json b/homeassistant/components/huisbaasje/translations/zh-Hant.json index cb71ec30060..b07b7115b07 100644 --- a/homeassistant/components/huisbaasje/translations/zh-Hant.json +++ b/homeassistant/components/huisbaasje/translations/zh-Hant.json @@ -5,9 +5,7 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "connection_exception": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unauthenticated_exception": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/ialarm/translations/ca.json b/homeassistant/components/ialarm/translations/ca.json index 371c2518503..f99632df604 100644 --- a/homeassistant/components/ialarm/translations/ca.json +++ b/homeassistant/components/ialarm/translations/ca.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Amfitri\u00f3", - "pin": "Codi PIN", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/cs.json b/homeassistant/components/ialarm/translations/cs.json index f6e1a56ca4a..960063a8816 100644 --- a/homeassistant/components/ialarm/translations/cs.json +++ b/homeassistant/components/ialarm/translations/cs.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Hostitel", - "pin": "PIN k\u00f3d", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/de.json b/homeassistant/components/ialarm/translations/de.json index 6577f995acc..45727d85ee0 100644 --- a/homeassistant/components/ialarm/translations/de.json +++ b/homeassistant/components/ialarm/translations/de.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Host", - "pin": "PIN-Code", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/en.json b/homeassistant/components/ialarm/translations/en.json index 39069f3d2b1..5492c4dfdea 100644 --- a/homeassistant/components/ialarm/translations/en.json +++ b/homeassistant/components/ialarm/translations/en.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Host", - "pin": "PIN Code", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/es.json b/homeassistant/components/ialarm/translations/es.json index fcf028791ae..537455fffe0 100644 --- a/homeassistant/components/ialarm/translations/es.json +++ b/homeassistant/components/ialarm/translations/es.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Host", - "pin": "C\u00f3digo PIN", "port": "Puerto" } } diff --git a/homeassistant/components/ialarm/translations/et.json b/homeassistant/components/ialarm/translations/et.json index d77ca5140b6..91c96c42846 100644 --- a/homeassistant/components/ialarm/translations/et.json +++ b/homeassistant/components/ialarm/translations/et.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Host", - "pin": "PIN kood", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/fr.json b/homeassistant/components/ialarm/translations/fr.json index 8cfb9a62470..ae61afa9d78 100644 --- a/homeassistant/components/ialarm/translations/fr.json +++ b/homeassistant/components/ialarm/translations/fr.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "H\u00f4te", - "pin": "Code PIN", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/id.json b/homeassistant/components/ialarm/translations/id.json index 4f299f816f1..3c6f4c4f844 100644 --- a/homeassistant/components/ialarm/translations/id.json +++ b/homeassistant/components/ialarm/translations/id.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Host", - "pin": "Kode PIN", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/it.json b/homeassistant/components/ialarm/translations/it.json index 89cb26f8e45..58174814a09 100644 --- a/homeassistant/components/ialarm/translations/it.json +++ b/homeassistant/components/ialarm/translations/it.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Host", - "pin": "Codice PIN", "port": "Porta" } } diff --git a/homeassistant/components/ialarm/translations/ko.json b/homeassistant/components/ialarm/translations/ko.json index 7eb20913d2d..2c630e533ff 100644 --- a/homeassistant/components/ialarm/translations/ko.json +++ b/homeassistant/components/ialarm/translations/ko.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "\ud638\uc2a4\ud2b8", - "pin": "PIN \ucf54\ub4dc", "port": "\ud3ec\ud2b8" } } diff --git a/homeassistant/components/ialarm/translations/nl.json b/homeassistant/components/ialarm/translations/nl.json index 6ae046200c5..9e11dbad82b 100644 --- a/homeassistant/components/ialarm/translations/nl.json +++ b/homeassistant/components/ialarm/translations/nl.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Host", - "pin": "PIN-code", "port": "Poort" } } diff --git a/homeassistant/components/ialarm/translations/no.json b/homeassistant/components/ialarm/translations/no.json index 016ba859abd..49412231035 100644 --- a/homeassistant/components/ialarm/translations/no.json +++ b/homeassistant/components/ialarm/translations/no.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Vert", - "pin": "PIN kode", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/pl.json b/homeassistant/components/ialarm/translations/pl.json index db52ec86612..1708694e123 100644 --- a/homeassistant/components/ialarm/translations/pl.json +++ b/homeassistant/components/ialarm/translations/pl.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "Nazwa hosta lub adres IP", - "pin": "Kod PIN", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/ru.json b/homeassistant/components/ialarm/translations/ru.json index 03f43f1b62f..47388153672 100644 --- a/homeassistant/components/ialarm/translations/ru.json +++ b/homeassistant/components/ialarm/translations/ru.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "\u0425\u043e\u0441\u0442", - "pin": "PIN-\u043a\u043e\u0434", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/ialarm/translations/zh-Hant.json b/homeassistant/components/ialarm/translations/zh-Hant.json index ef436312d7e..0b8669bf996 100644 --- a/homeassistant/components/ialarm/translations/zh-Hant.json +++ b/homeassistant/components/ialarm/translations/zh-Hant.json @@ -11,7 +11,6 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "pin": "PIN \u78bc", "port": "\u901a\u8a0a\u57e0" } } diff --git a/homeassistant/components/ipp/translations/ca.json b/homeassistant/components/ipp/translations/ca.json index 9f508c18246..e3669e6d458 100644 --- a/homeassistant/components/ipp/translations/ca.json +++ b/homeassistant/components/ipp/translations/ca.json @@ -13,7 +13,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "connection_upgrade": "No s'ha pogut connectar amb la impressora. Prova-ho novament amb l'opci\u00f3 SSL/TLS activada." }, - "flow_title": "Impressora: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ipp/translations/et.json b/homeassistant/components/ipp/translations/et.json index 5a0a2e69cfb..b7e6c8b746f 100644 --- a/homeassistant/components/ipp/translations/et.json +++ b/homeassistant/components/ipp/translations/et.json @@ -13,7 +13,7 @@ "cannot_connect": "\u00dchendamine nurjus", "connection_upgrade": "Printeriga \u00fchenduse loomine nurjus. Proovi uuesti kui SSL/TLS-i suvand on m\u00e4rgitud." }, - "flow_title": "Printer: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ipp/translations/no.json b/homeassistant/components/ipp/translations/no.json index c612163d51e..69c0cd86346 100644 --- a/homeassistant/components/ipp/translations/no.json +++ b/homeassistant/components/ipp/translations/no.json @@ -13,7 +13,7 @@ "cannot_connect": "Tilkobling mislyktes", "connection_upgrade": "Kunne ikke koble til skriveren. Vennligst pr\u00f8v igjen med alternativet SSL / TLS merket." }, - "flow_title": "Skriver: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ipp/translations/ru.json b/homeassistant/components/ipp/translations/ru.json index 44521b1ae0d..e2a4413c211 100644 --- a/homeassistant/components/ipp/translations/ru.json +++ b/homeassistant/components/ipp/translations/ru.json @@ -13,7 +13,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "connection_upgrade": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 SSL/TLS." }, - "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ipp/translations/zh-Hant.json b/homeassistant/components/ipp/translations/zh-Hant.json index 7a0abd19d98..efe960a6a2e 100644 --- a/homeassistant/components/ipp/translations/zh-Hant.json +++ b/homeassistant/components/ipp/translations/zh-Hant.json @@ -13,7 +13,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "connection_upgrade": "\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002\u8acb\u52fe\u9078 SSL/TLS \u9078\u9805\u5f8c\u518d\u8a66\u4e00\u6b21\u3002" }, - "flow_title": "\u5370\u8868\u6a5f\uff1a{name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/ca.json b/homeassistant/components/isy994/translations/ca.json index d8d5911e871..420214d3e16 100644 --- a/homeassistant/components/isy994/translations/ca.json +++ b/homeassistant/components/isy994/translations/ca.json @@ -9,7 +9,7 @@ "invalid_host": "L'entrada de l'amfitri\u00f3 no t\u00e9 el fromat d'URL complet, ex: http://192.168.10.100:80", "unknown": "Error inesperat" }, - "flow_title": "Dispositius universals ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/et.json b/homeassistant/components/isy994/translations/et.json index 010e5ec9a03..d7d4a12b2cb 100644 --- a/homeassistant/components/isy994/translations/et.json +++ b/homeassistant/components/isy994/translations/et.json @@ -9,7 +9,7 @@ "invalid_host": "Hostikirje ei olnud sobivas URL-vormingus, nt http://192.168.10.100:80", "unknown": "Tundmatu viga" }, - "flow_title": "", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/no.json b/homeassistant/components/isy994/translations/no.json index 422c8fb7c9d..f15baed6657 100644 --- a/homeassistant/components/isy994/translations/no.json +++ b/homeassistant/components/isy994/translations/no.json @@ -9,7 +9,7 @@ "invalid_host": "Vertsoppf\u00f8ringen var ikke i fullstendig URL-format, for eksempel http://192.168.10.100:80", "unknown": "Uventet feil" }, - "flow_title": "Universelle enheter ISY994 {name} ({host})", + "flow_title": "{name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index 50aa75cab37..fe4c161c1a9 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -9,7 +9,7 @@ "invalid_host": "URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.10.100:80').", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Universal Devices ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index 0fbaefb498c..edec9f514ce 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -9,7 +9,7 @@ "invalid_host": "\u4e3b\u6a5f\u7aef\u4e26\u672a\u4ee5\u5b8c\u6574\u7db2\u5740\u683c\u5f0f\u8f38\u5165\uff0c\u4f8b\u5982\uff1ahttp://192.168.10.100:80", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "Universal Devices ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/bg.json b/homeassistant/components/keenetic_ndms2/translations/bg.json index db122ec078b..fc2115d9ca0 100644 --- a/homeassistant/components/keenetic_ndms2/translations/bg.json +++ b/homeassistant/components/keenetic_ndms2/translations/bg.json @@ -3,7 +3,6 @@ "step": { "user": { "data": { - "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" diff --git a/homeassistant/components/keenetic_ndms2/translations/ca.json b/homeassistant/components/keenetic_ndms2/translations/ca.json index f15b11b3eb4..364b5dad10e 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ca.json +++ b/homeassistant/components/keenetic_ndms2/translations/ca.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Amfitri\u00f3", - "name": "Nom", "password": "Contrasenya", "port": "Port", "username": "Nom d'usuari" diff --git a/homeassistant/components/keenetic_ndms2/translations/cs.json b/homeassistant/components/keenetic_ndms2/translations/cs.json index f34807f3fee..e90e755ac90 100644 --- a/homeassistant/components/keenetic_ndms2/translations/cs.json +++ b/homeassistant/components/keenetic_ndms2/translations/cs.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Hostitel", - "name": "N\u00e1zev", "password": "Heslo", "port": "Port", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" diff --git a/homeassistant/components/keenetic_ndms2/translations/de.json b/homeassistant/components/keenetic_ndms2/translations/de.json index cc9a3630ab1..68de4001255 100644 --- a/homeassistant/components/keenetic_ndms2/translations/de.json +++ b/homeassistant/components/keenetic_ndms2/translations/de.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Host", - "name": "Name", "password": "Passwort", "port": "Port", "username": "Benutzername" diff --git a/homeassistant/components/keenetic_ndms2/translations/en.json b/homeassistant/components/keenetic_ndms2/translations/en.json index e95f2f740ef..5a946751ff4 100644 --- a/homeassistant/components/keenetic_ndms2/translations/en.json +++ b/homeassistant/components/keenetic_ndms2/translations/en.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Host", - "name": "Name", "password": "Password", "port": "Port", "username": "Username" diff --git a/homeassistant/components/keenetic_ndms2/translations/es.json b/homeassistant/components/keenetic_ndms2/translations/es.json index 6846cfbef42..6b8eed6e26b 100644 --- a/homeassistant/components/keenetic_ndms2/translations/es.json +++ b/homeassistant/components/keenetic_ndms2/translations/es.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Host", - "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Usuario" diff --git a/homeassistant/components/keenetic_ndms2/translations/et.json b/homeassistant/components/keenetic_ndms2/translations/et.json index dc500be7e1a..ccb23aff796 100644 --- a/homeassistant/components/keenetic_ndms2/translations/et.json +++ b/homeassistant/components/keenetic_ndms2/translations/et.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Host", - "name": "Nimi", "password": "Salas\u00f5na", "port": "Port", "username": "Kasutajanimi" diff --git a/homeassistant/components/keenetic_ndms2/translations/fr.json b/homeassistant/components/keenetic_ndms2/translations/fr.json index 2ac19dcdc64..bf3ebbf5b22 100644 --- a/homeassistant/components/keenetic_ndms2/translations/fr.json +++ b/homeassistant/components/keenetic_ndms2/translations/fr.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "H\u00f4te", - "name": "Nom", "password": "Mot de passe", "port": "Port", "username": "Nom d'utilisateur" diff --git a/homeassistant/components/keenetic_ndms2/translations/hu.json b/homeassistant/components/keenetic_ndms2/translations/hu.json index 72482de8604..b545f065ddc 100644 --- a/homeassistant/components/keenetic_ndms2/translations/hu.json +++ b/homeassistant/components/keenetic_ndms2/translations/hu.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Hoszt", - "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/keenetic_ndms2/translations/id.json b/homeassistant/components/keenetic_ndms2/translations/id.json index 6a427a875a0..bb30e715579 100644 --- a/homeassistant/components/keenetic_ndms2/translations/id.json +++ b/homeassistant/components/keenetic_ndms2/translations/id.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Host", - "name": "Nama", "password": "Kata Sandi", "port": "Port", "username": "Nama Pengguna" diff --git a/homeassistant/components/keenetic_ndms2/translations/it.json b/homeassistant/components/keenetic_ndms2/translations/it.json index e5a705d14b8..e19961f5823 100644 --- a/homeassistant/components/keenetic_ndms2/translations/it.json +++ b/homeassistant/components/keenetic_ndms2/translations/it.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Host", - "name": "Nome", "password": "Password", "port": "Porta", "username": "Nome utente" diff --git a/homeassistant/components/keenetic_ndms2/translations/ko.json b/homeassistant/components/keenetic_ndms2/translations/ko.json index 8968d9e7709..9444b447d37 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ko.json +++ b/homeassistant/components/keenetic_ndms2/translations/ko.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "\ud638\uc2a4\ud2b8", - "name": "\uc774\ub984", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" diff --git a/homeassistant/components/keenetic_ndms2/translations/nl.json b/homeassistant/components/keenetic_ndms2/translations/nl.json index b7c89bb65e9..d2a85c8b059 100644 --- a/homeassistant/components/keenetic_ndms2/translations/nl.json +++ b/homeassistant/components/keenetic_ndms2/translations/nl.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Host", - "name": "Naam", "password": "Wachtwoord", "port": "Poort", "username": "Gebruikersnaam" diff --git a/homeassistant/components/keenetic_ndms2/translations/no.json b/homeassistant/components/keenetic_ndms2/translations/no.json index 6ad2805eb3d..d37a306d9a6 100644 --- a/homeassistant/components/keenetic_ndms2/translations/no.json +++ b/homeassistant/components/keenetic_ndms2/translations/no.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Vert", - "name": "Navn", "password": "Passord", "port": "Port", "username": "Brukernavn" diff --git a/homeassistant/components/keenetic_ndms2/translations/pl.json b/homeassistant/components/keenetic_ndms2/translations/pl.json index 13bcbfb91ba..ba8b709301d 100644 --- a/homeassistant/components/keenetic_ndms2/translations/pl.json +++ b/homeassistant/components/keenetic_ndms2/translations/pl.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Nazwa hosta lub adres IP", - "name": "Nazwa", "password": "Has\u0142o", "port": "Port", "username": "Nazwa u\u017cytkownika" diff --git a/homeassistant/components/keenetic_ndms2/translations/ru.json b/homeassistant/components/keenetic_ndms2/translations/ru.json index 6f99453888e..191dfbb1f04 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ru.json +++ b/homeassistant/components/keenetic_ndms2/translations/ru.json @@ -10,7 +10,6 @@ "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": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" diff --git a/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json b/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json index 7900f3a8854..2f64eee7439 100644 --- a/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json +++ b/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" diff --git a/homeassistant/components/kodi/translations/ca.json b/homeassistant/components/kodi/translations/ca.json index fbeaa3d4c71..c04408b4a6c 100644 --- a/homeassistant/components/kodi/translations/ca.json +++ b/homeassistant/components/kodi/translations/ca.json @@ -12,7 +12,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/kodi/translations/et.json b/homeassistant/components/kodi/translations/et.json index f12665dfe8a..48c2f678c16 100644 --- a/homeassistant/components/kodi/translations/et.json +++ b/homeassistant/components/kodi/translations/et.json @@ -12,7 +12,7 @@ "invalid_auth": "Tuvastamine nurjus", "unknown": "Tundmatu viga" }, - "flow_title": "", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/kodi/translations/no.json b/homeassistant/components/kodi/translations/no.json index b7815c3aa3a..8e1f266f6a0 100644 --- a/homeassistant/components/kodi/translations/no.json +++ b/homeassistant/components/kodi/translations/no.json @@ -12,7 +12,7 @@ "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json index f0ec31654dd..45a4994e86e 100644 --- a/homeassistant/components/kodi/translations/ru.json +++ b/homeassistant/components/kodi/translations/ru.json @@ -12,7 +12,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json index 735df851060..32ecb4164d5 100644 --- a/homeassistant/components/kodi/translations/zh-Hant.json +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -12,7 +12,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "Kodi\uff1a{name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/local_ip/translations/ca.json b/homeassistant/components/local_ip/translations/ca.json index cfd425e3034..f5fae4a032d 100644 --- a/homeassistant/components/local_ip/translations/ca.json +++ b/homeassistant/components/local_ip/translations/ca.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Nom del sensor" - }, "description": "Vols comen\u00e7ar la configuraci\u00f3?", "title": "Adre\u00e7a IP local" } diff --git a/homeassistant/components/local_ip/translations/cs.json b/homeassistant/components/local_ip/translations/cs.json index f7254ebaf44..cbd2d5c0d25 100644 --- a/homeassistant/components/local_ip/translations/cs.json +++ b/homeassistant/components/local_ip/translations/cs.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "N\u00e1zev senzoru" - }, "description": "Chcete za\u010d\u00edt nastavovat?", "title": "M\u00edstn\u00ed IP adresa" } diff --git a/homeassistant/components/local_ip/translations/da.json b/homeassistant/components/local_ip/translations/da.json index eebf7c1d0ce..212a04c134e 100644 --- a/homeassistant/components/local_ip/translations/da.json +++ b/homeassistant/components/local_ip/translations/da.json @@ -2,9 +2,6 @@ "config": { "step": { "user": { - "data": { - "name": "Sensornavn" - }, "title": "Lokal IP-adresse" } } diff --git a/homeassistant/components/local_ip/translations/de.json b/homeassistant/components/local_ip/translations/de.json index 2d31f90139d..345874615ec 100644 --- a/homeassistant/components/local_ip/translations/de.json +++ b/homeassistant/components/local_ip/translations/de.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Sensorname" - }, "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", "title": "Lokale IP-Adresse" } diff --git a/homeassistant/components/local_ip/translations/en.json b/homeassistant/components/local_ip/translations/en.json index 167989b7ba8..eae07e0931b 100644 --- a/homeassistant/components/local_ip/translations/en.json +++ b/homeassistant/components/local_ip/translations/en.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Sensor Name" - }, "description": "Do you want to start set up?", "title": "Local IP Address" } diff --git a/homeassistant/components/local_ip/translations/es-419.json b/homeassistant/components/local_ip/translations/es-419.json index ba120a1ce14..06a7a5c3e2e 100644 --- a/homeassistant/components/local_ip/translations/es-419.json +++ b/homeassistant/components/local_ip/translations/es-419.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Nombre del sensor" - }, "title": "Direcci\u00f3n IP local" } } diff --git a/homeassistant/components/local_ip/translations/es.json b/homeassistant/components/local_ip/translations/es.json index a3048d396d5..3b094636057 100644 --- a/homeassistant/components/local_ip/translations/es.json +++ b/homeassistant/components/local_ip/translations/es.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Nombre del sensor" - }, "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", "title": "Direcci\u00f3n IP local" } diff --git a/homeassistant/components/local_ip/translations/et.json b/homeassistant/components/local_ip/translations/et.json index 63e9e251d30..a96342d049a 100644 --- a/homeassistant/components/local_ip/translations/et.json +++ b/homeassistant/components/local_ip/translations/et.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Anduri nimi" - }, "description": "Kas soovid alustada seadistamist?", "title": "Kohalik IP-aadress" } diff --git a/homeassistant/components/local_ip/translations/fr.json b/homeassistant/components/local_ip/translations/fr.json index 1c5a8fc9634..554ecef2380 100644 --- a/homeassistant/components/local_ip/translations/fr.json +++ b/homeassistant/components/local_ip/translations/fr.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Nom du capteur" - }, "description": "Voulez-vous commencer la configuration ?", "title": "Adresse IP locale" } diff --git a/homeassistant/components/local_ip/translations/hu.json b/homeassistant/components/local_ip/translations/hu.json index b692e87f922..e930d58784a 100644 --- a/homeassistant/components/local_ip/translations/hu.json +++ b/homeassistant/components/local_ip/translations/hu.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "\u00c9rz\u00e9kel\u0151 neve" - }, "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "Helyi IP c\u00edm" } diff --git a/homeassistant/components/local_ip/translations/id.json b/homeassistant/components/local_ip/translations/id.json index a7d8993baa6..31cc2dfbc70 100644 --- a/homeassistant/components/local_ip/translations/id.json +++ b/homeassistant/components/local_ip/translations/id.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Nama Sensor" - }, "description": "Ingin memulai penyiapan?", "title": "Alamat IP Lokal" } diff --git a/homeassistant/components/local_ip/translations/it.json b/homeassistant/components/local_ip/translations/it.json index db47d7b9f9f..525b75715fd 100644 --- a/homeassistant/components/local_ip/translations/it.json +++ b/homeassistant/components/local_ip/translations/it.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Nome del sensore" - }, "description": "Vuoi iniziare la configurazione?", "title": "Indirizzo IP locale" } diff --git a/homeassistant/components/local_ip/translations/ko.json b/homeassistant/components/local_ip/translations/ko.json index f7248ab0f6f..92582729e94 100644 --- a/homeassistant/components/local_ip/translations/ko.json +++ b/homeassistant/components/local_ip/translations/ko.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "\uc13c\uc11c \uc774\ub984" - }, "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "\ub85c\uceec IP \uc8fc\uc18c" } diff --git a/homeassistant/components/local_ip/translations/lb.json b/homeassistant/components/local_ip/translations/lb.json index c94685b1a2a..3b232746f41 100644 --- a/homeassistant/components/local_ip/translations/lb.json +++ b/homeassistant/components/local_ip/translations/lb.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Numm vum Sensor" - }, "description": "Soll den Ariichtungs Prozess gestart ginn?", "title": "Lokal IP Adresse" } diff --git a/homeassistant/components/local_ip/translations/nl.json b/homeassistant/components/local_ip/translations/nl.json index b8b033d2e73..3ea8140a96e 100644 --- a/homeassistant/components/local_ip/translations/nl.json +++ b/homeassistant/components/local_ip/translations/nl.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Sensor Naam" - }, "description": "Wil je beginnen met instellen?", "title": "Lokaal IP-adres" } diff --git a/homeassistant/components/local_ip/translations/no.json b/homeassistant/components/local_ip/translations/no.json index f5686ee5236..a33662de715 100644 --- a/homeassistant/components/local_ip/translations/no.json +++ b/homeassistant/components/local_ip/translations/no.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Sensornavn" - }, "description": "Vil du starte oppsettet?", "title": "Lokal IP-adresse" } diff --git a/homeassistant/components/local_ip/translations/pl.json b/homeassistant/components/local_ip/translations/pl.json index eab29842291..a8c5dd7aaf2 100644 --- a/homeassistant/components/local_ip/translations/pl.json +++ b/homeassistant/components/local_ip/translations/pl.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Nazwa sensora" - }, "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?", "title": "Lokalny adres IP" } diff --git a/homeassistant/components/local_ip/translations/pt-BR.json b/homeassistant/components/local_ip/translations/pt-BR.json index be06de8b7f6..179e720abca 100644 --- a/homeassistant/components/local_ip/translations/pt-BR.json +++ b/homeassistant/components/local_ip/translations/pt-BR.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Nome do sensor" - }, "title": "Endere\u00e7o IP local" } } diff --git a/homeassistant/components/local_ip/translations/ru.json b/homeassistant/components/local_ip/translations/ru.json index afa78a42778..ab6d04f2e69 100644 --- a/homeassistant/components/local_ip/translations/ru.json +++ b/homeassistant/components/local_ip/translations/ru.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" - }, "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?", "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441" } diff --git a/homeassistant/components/local_ip/translations/sl.json b/homeassistant/components/local_ip/translations/sl.json index ca1103043e3..06c5f4182d0 100644 --- a/homeassistant/components/local_ip/translations/sl.json +++ b/homeassistant/components/local_ip/translations/sl.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Ime tipala" - }, "title": "Lokalni IP naslov" } } diff --git a/homeassistant/components/local_ip/translations/sv.json b/homeassistant/components/local_ip/translations/sv.json index d4b508b41a7..7f1b7ce637a 100644 --- a/homeassistant/components/local_ip/translations/sv.json +++ b/homeassistant/components/local_ip/translations/sv.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Sensor Namn" - }, "title": "Lokal IP-adress" } } diff --git a/homeassistant/components/local_ip/translations/tr.json b/homeassistant/components/local_ip/translations/tr.json index e8e82814f8a..d0540ec7a6e 100644 --- a/homeassistant/components/local_ip/translations/tr.json +++ b/homeassistant/components/local_ip/translations/tr.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "Sens\u00f6r Ad\u0131" - }, "description": "Kuruluma ba\u015flamak ister misiniz?", "title": "Yerel IP Adresi" } diff --git a/homeassistant/components/local_ip/translations/uk.json b/homeassistant/components/local_ip/translations/uk.json index b88c1c002bf..52aed47fa20 100644 --- a/homeassistant/components/local_ip/translations/uk.json +++ b/homeassistant/components/local_ip/translations/uk.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "\u041d\u0430\u0437\u0432\u0430" - }, "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430" } diff --git a/homeassistant/components/local_ip/translations/zh-Hant.json b/homeassistant/components/local_ip/translations/zh-Hant.json index b14abdd6b62..d7498843b75 100644 --- a/homeassistant/components/local_ip/translations/zh-Hant.json +++ b/homeassistant/components/local_ip/translations/zh-Hant.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "name": "\u50b3\u611f\u5668\u540d\u7a31" - }, "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f", "title": "\u672c\u5730 IP \u4f4d\u5740" } diff --git a/homeassistant/components/lutron_caseta/translations/ca.json b/homeassistant/components/lutron_caseta/translations/ca.json index 5f2cc5d4087..de714fea726 100644 --- a/homeassistant/components/lutron_caseta/translations/ca.json +++ b/homeassistant/components/lutron_caseta/translations/ca.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "No s'ha pogut configurar l'enlla\u00e7 (amfitri\u00f3: {host}) importat de configuration.yaml.", diff --git a/homeassistant/components/lutron_caseta/translations/et.json b/homeassistant/components/lutron_caseta/translations/et.json index 5e57dd63b8b..b6d73a920d4 100644 --- a/homeassistant/components/lutron_caseta/translations/et.json +++ b/homeassistant/components/lutron_caseta/translations/et.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )", + "flow_title": "{name} ( {host} )", "step": { "import_failed": { "description": "Silla (host: {host} ) seadistamine configuration.yaml kirje teabest nurjus.", diff --git a/homeassistant/components/lutron_caseta/translations/no.json b/homeassistant/components/lutron_caseta/translations/no.json index b985c87caf0..91e7bc28007 100644 --- a/homeassistant/components/lutron_caseta/translations/no.json +++ b/homeassistant/components/lutron_caseta/translations/no.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "Kunne ikke konfigurere bridge (host: {host} ) importert fra configuration.yaml.", diff --git a/homeassistant/components/lutron_caseta/translations/ru.json b/homeassistant/components/lutron_caseta/translations/ru.json index f54057f464e..090af1923f3 100644 --- a/homeassistant/components/lutron_caseta/translations/ru.json +++ b/homeassistant/components/lutron_caseta/translations/ru.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml (\u0445\u043e\u0441\u0442: {host}).", diff --git a/homeassistant/components/lutron_caseta/translations/zh-Hant.json b/homeassistant/components/lutron_caseta/translations/zh-Hant.json index 9e388e52288..320c26fd2ea 100644 --- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json +++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "\u7121\u6cd5\u8a2d\u5b9a\u7531 configuration.yaml \u532f\u5165\u7684 bridge\uff08\u4e3b\u6a5f\uff1a{host}\uff09\u3002", diff --git a/homeassistant/components/lyric/translations/de.json b/homeassistant/components/lyric/translations/de.json index 62404451984..b067b299145 100644 --- a/homeassistant/components/lyric/translations/de.json +++ b/homeassistant/components/lyric/translations/de.json @@ -13,7 +13,7 @@ "title": "W\u00e4hle die Authentifizierungsmethode" }, "reauth_confirm": { - "description": "Die Lyric-Integration muss Ihr Konto neu authentifizieren.", + "description": "Die Lyric-Integration muss dein Konto neu authentifizieren.", "title": "Integration erneut authentifizieren" } } diff --git a/homeassistant/components/mazda/translations/bg.json b/homeassistant/components/mazda/translations/bg.json index 6f3c5a54f3f..e51e1112202 100644 --- a/homeassistant/components/mazda/translations/bg.json +++ b/homeassistant/components/mazda/translations/bg.json @@ -4,12 +4,6 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { - "reauth": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "region": "\u0420\u0435\u0433\u0438\u043e\u043d" - } - }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/mazda/translations/ca.json b/homeassistant/components/mazda/translations/ca.json index d45b9177c3f..ef00713216a 100644 --- a/homeassistant/components/mazda/translations/ca.json +++ b/homeassistant/components/mazda/translations/ca.json @@ -11,15 +11,6 @@ "unknown": "Error inesperat" }, "step": { - "reauth": { - "data": { - "email": "Correu electr\u00f2nic", - "password": "Contrasenya", - "region": "Regi\u00f3" - }, - "description": "Ha fallat l'autenticaci\u00f3 dels Serveis connectats de Mazda. Introdueix les teves credencials actuals.", - "title": "Serveis connectats de Mazda - Ha fallat l'autenticaci\u00f3" - }, "user": { "data": { "email": "Correu electr\u00f2nic", diff --git a/homeassistant/components/mazda/translations/cs.json b/homeassistant/components/mazda/translations/cs.json index 89fde600735..c769fdc28dd 100644 --- a/homeassistant/components/mazda/translations/cs.json +++ b/homeassistant/components/mazda/translations/cs.json @@ -10,13 +10,6 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { - "reauth": { - "data": { - "email": "E-mail", - "password": "Heslo", - "region": "Region" - } - }, "user": { "data": { "email": "E-mail", diff --git a/homeassistant/components/mazda/translations/de.json b/homeassistant/components/mazda/translations/de.json index 9050ee9f00c..01dc3d97ffa 100644 --- a/homeassistant/components/mazda/translations/de.json +++ b/homeassistant/components/mazda/translations/de.json @@ -11,15 +11,6 @@ "unknown": "Unerwarteter Fehler" }, "step": { - "reauth": { - "data": { - "email": "E-Mail", - "password": "Passwort", - "region": "Region" - }, - "description": "Die Authentifizierung f\u00fcr Mazda Connected Services ist fehlgeschlagen. Bitte geben Sie Ihre aktuellen Anmeldedaten ein.", - "title": "Mazda Connected Services - Authentifizierung fehlgeschlagen" - }, "user": { "data": { "email": "E-Mail", diff --git a/homeassistant/components/mazda/translations/en.json b/homeassistant/components/mazda/translations/en.json index b9e02fb3a41..b483947aaa0 100644 --- a/homeassistant/components/mazda/translations/en.json +++ b/homeassistant/components/mazda/translations/en.json @@ -11,15 +11,6 @@ "unknown": "Unexpected error" }, "step": { - "reauth": { - "data": { - "email": "Email", - "password": "Password", - "region": "Region" - }, - "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.", - "title": "Mazda Connected Services - Authentication Failed" - }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index bfe1b430365..a9ffa26787c 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -11,15 +11,6 @@ "unknown": "Error inesperado" }, "step": { - "reauth": { - "data": { - "email": "Correo electr\u00f3nico", - "password": "Contrase\u00f1a", - "region": "Regi\u00f3n" - }, - "description": "Ha fallado la autenticaci\u00f3n para los Servicios Conectados de Mazda. Por favor, introduce tus credenciales actuales.", - "title": "Servicios Conectados de Mazda - Fallo de autenticaci\u00f3n" - }, "user": { "data": { "email": "Correo electronico", diff --git a/homeassistant/components/mazda/translations/et.json b/homeassistant/components/mazda/translations/et.json index 4ce2e2fa5f3..39ead99765c 100644 --- a/homeassistant/components/mazda/translations/et.json +++ b/homeassistant/components/mazda/translations/et.json @@ -11,15 +11,6 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { - "reauth": { - "data": { - "email": "E-posti aadress", - "password": "Salas\u00f5na", - "region": "Piirkond" - }, - "description": "Mazda Connected Services tuvastamine nurjus. Sisesta oma kehtivad andmed.", - "title": "Mazda Connected Services - tuvastamine nurjus" - }, "user": { "data": { "email": "E-posti aadress", diff --git a/homeassistant/components/mazda/translations/fr.json b/homeassistant/components/mazda/translations/fr.json index aa1ea252c0c..ff0ee33d368 100644 --- a/homeassistant/components/mazda/translations/fr.json +++ b/homeassistant/components/mazda/translations/fr.json @@ -11,15 +11,6 @@ "unknown": "Erreur inattendue" }, "step": { - "reauth": { - "data": { - "email": "Email", - "password": "Mot de passe", - "region": "R\u00e9gion" - }, - "description": "L'authentification a \u00e9chou\u00e9 pour les services connect\u00e9s Mazda. Veuillez saisir vos informations d'identification actuelles.", - "title": "Services connect\u00e9s Mazda - \u00c9chec de l'authentification" - }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/mazda/translations/hu.json b/homeassistant/components/mazda/translations/hu.json index 1b9c6893ed5..f881bfbac45 100644 --- a/homeassistant/components/mazda/translations/hu.json +++ b/homeassistant/components/mazda/translations/hu.json @@ -11,14 +11,6 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { - "reauth": { - "data": { - "email": "E-mail", - "password": "Jelsz\u00f3", - "region": "R\u00e9gi\u00f3" - }, - "title": "Mazda Connected Services - A hiteles\u00edt\u00e9s sikertelen" - }, "user": { "data": { "email": "E-mail", diff --git a/homeassistant/components/mazda/translations/id.json b/homeassistant/components/mazda/translations/id.json index 0a6e81e8454..fdcc736162e 100644 --- a/homeassistant/components/mazda/translations/id.json +++ b/homeassistant/components/mazda/translations/id.json @@ -11,15 +11,6 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { - "reauth": { - "data": { - "email": "Email", - "password": "Kata Sandi", - "region": "Wilayah" - }, - "description": "Autentikasi gagal untuk Mazda Connected Services. Masukkan kredensial Anda saat ini.", - "title": "Mazda Connected Services - Autentikasi Gagal" - }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/mazda/translations/it.json b/homeassistant/components/mazda/translations/it.json index d5a2796ed18..7d7ab19a329 100644 --- a/homeassistant/components/mazda/translations/it.json +++ b/homeassistant/components/mazda/translations/it.json @@ -11,15 +11,6 @@ "unknown": "Errore imprevisto" }, "step": { - "reauth": { - "data": { - "email": "E-mail", - "password": "Password", - "region": "Area geografica" - }, - "description": "Autenticazione non riuscita per Mazda Connected Services. Inserisci le tue credenziali attuali.", - "title": "Mazda Connected Services - Autenticazione non riuscita" - }, "user": { "data": { "email": "E-mail", diff --git a/homeassistant/components/mazda/translations/ko.json b/homeassistant/components/mazda/translations/ko.json index aa9dcf99d14..6023a053a7e 100644 --- a/homeassistant/components/mazda/translations/ko.json +++ b/homeassistant/components/mazda/translations/ko.json @@ -11,15 +11,6 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { - "reauth": { - "data": { - "email": "\uc774\uba54\uc77c", - "password": "\ube44\ubc00\ubc88\ud638", - "region": "\uc9c0\uc5ed" - }, - "description": "Mazda Connected Services\uc5d0 \ub300\ud55c \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud604\uc7ac \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "Mazda Connected Services - \uc778\uc99d \uc2e4\ud328" - }, "user": { "data": { "email": "\uc774\uba54\uc77c", diff --git a/homeassistant/components/mazda/translations/nl.json b/homeassistant/components/mazda/translations/nl.json index 64975c9b14b..3370fe29bb8 100644 --- a/homeassistant/components/mazda/translations/nl.json +++ b/homeassistant/components/mazda/translations/nl.json @@ -11,15 +11,6 @@ "unknown": "Onverwachte fout" }, "step": { - "reauth": { - "data": { - "email": "E-mail", - "password": "Wachtwoord", - "region": "Regio" - }, - "description": "Verificatie mislukt voor Mazda Connected Services. Voer uw huidige inloggegevens in.", - "title": "Mazda Connected Services - Authenticatie mislukt" - }, "user": { "data": { "email": "E-mail", diff --git a/homeassistant/components/mazda/translations/no.json b/homeassistant/components/mazda/translations/no.json index e3a05de51f9..3f4db47d2b0 100644 --- a/homeassistant/components/mazda/translations/no.json +++ b/homeassistant/components/mazda/translations/no.json @@ -11,15 +11,6 @@ "unknown": "Uventet feil" }, "step": { - "reauth": { - "data": { - "email": "E-post", - "password": "Passord", - "region": "Region" - }, - "description": "Autentisering mislyktes for Mazda Connected Services. Vennligst skriv inn din n\u00e5v\u00e6rende legitimasjon.", - "title": "Mazda Connected Services - Autentisering mislyktes" - }, "user": { "data": { "email": "E-post", diff --git a/homeassistant/components/mazda/translations/pl.json b/homeassistant/components/mazda/translations/pl.json index 12254f20662..fdd1a8c5ce9 100644 --- a/homeassistant/components/mazda/translations/pl.json +++ b/homeassistant/components/mazda/translations/pl.json @@ -11,15 +11,6 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { - "reauth": { - "data": { - "email": "Adres e-mail", - "password": "Has\u0142o", - "region": "Region" - }, - "description": "Uwierzytelnianie dla Mazda Connected Services nie powiod\u0142o si\u0119. Wprowad\u017a aktualne dane uwierzytelniaj\u0105ce.", - "title": "Mazda Connected Services - Uwierzytelnianie nie powiod\u0142o si\u0119" - }, "user": { "data": { "email": "Adres e-mail", diff --git a/homeassistant/components/mazda/translations/ru.json b/homeassistant/components/mazda/translations/ru.json index be3f861d406..cf949416274 100644 --- a/homeassistant/components/mazda/translations/ru.json +++ b/homeassistant/components/mazda/translations/ru.json @@ -11,15 +11,6 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { - "reauth": { - "data": { - "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "region": "\u0420\u0435\u0433\u0438\u043e\u043d" - }, - "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0442\u0435\u043a\u0443\u0449\u0438\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" - }, "user": { "data": { "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", diff --git a/homeassistant/components/mazda/translations/zh-Hant.json b/homeassistant/components/mazda/translations/zh-Hant.json index 48232664683..0c0e3f0fd8c 100644 --- a/homeassistant/components/mazda/translations/zh-Hant.json +++ b/homeassistant/components/mazda/translations/zh-Hant.json @@ -11,15 +11,6 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { - "reauth": { - "data": { - "email": "\u96fb\u5b50\u90f5\u4ef6", - "password": "\u5bc6\u78bc", - "region": "\u5340\u57df" - }, - "description": "Mazda Connected \u670d\u52d9\u8a8d\u8b49\u5931\u6557\u3002\u8acb\u8f38\u5165\u76ee\u524d\u6191\u8b49\u3002", - "title": "Mazda Connected \u670d\u52d9 - \u8a8d\u8b49\u5931\u6557" - }, "user": { "data": { "email": "\u96fb\u5b50\u90f5\u4ef6", diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 02716d73e84..3a968bc6491 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -8,6 +8,7 @@ "error": { "discovery_error": "Failed to discover a Motion Gateway" }, + "flow_title": "Motion Blinds", "step": { "connect": { "data": { @@ -25,6 +26,7 @@ }, "user": { "data": { + "api_key": "API Key", "host": "IP Address" }, "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 3bd07c4507b..2c5895a98c8 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -70,7 +70,8 @@ "discovery": "Erkennung aktivieren", "will_enable": "Letzten Willen aktivieren" }, - "description": "Bitte die MQTT-Einstellungen ausw\u00e4hlen." + "description": "Bitte die MQTT-Einstellungen ausw\u00e4hlen.", + "title": "MQTT-Optionen" } } } diff --git a/homeassistant/components/mullvad/translations/bg.json b/homeassistant/components/mullvad/translations/bg.json index a84e1c3bfdf..5d274ec2b73 100644 --- a/homeassistant/components/mullvad/translations/bg.json +++ b/homeassistant/components/mullvad/translations/bg.json @@ -2,14 +2,6 @@ "config": { "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\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" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/ca.json b/homeassistant/components/mullvad/translations/ca.json index f81781cbc0f..0dc1f0bd4a9 100644 --- a/homeassistant/components/mullvad/translations/ca.json +++ b/homeassistant/components/mullvad/translations/ca.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "step": { "user": { - "data": { - "host": "Amfitri\u00f3", - "password": "Contrasenya", - "username": "Nom d'usuari" - }, "description": "Vols configurar la integraci\u00f3 Mullvad VPN?" } } diff --git a/homeassistant/components/mullvad/translations/cs.json b/homeassistant/components/mullvad/translations/cs.json index 0f02cd974c2..0887542d784 100644 --- a/homeassistant/components/mullvad/translations/cs.json +++ b/homeassistant/components/mullvad/translations/cs.json @@ -5,17 +5,7 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "step": { - "user": { - "data": { - "host": "Hostitel", - "password": "Heslo", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/de.json b/homeassistant/components/mullvad/translations/de.json index 6014a9155c8..be3beba93fd 100644 --- a/homeassistant/components/mullvad/translations/de.json +++ b/homeassistant/components/mullvad/translations/de.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Passwort", - "username": "Benutzername" - }, "description": "Mullvad VPN Integration einrichten?" } } diff --git a/homeassistant/components/mullvad/translations/el.json b/homeassistant/components/mullvad/translations/el.json index 6f19f0039ed..06559d04b01 100644 --- a/homeassistant/components/mullvad/translations/el.json +++ b/homeassistant/components/mullvad/translations/el.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_auth": "\u0386\u03ba\u03c5\u03c1\u03b7 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "user": { - "data": { - "host": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" - }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Mullvad VPN;" } } diff --git a/homeassistant/components/mullvad/translations/en.json b/homeassistant/components/mullvad/translations/en.json index fcfa89ef082..45664554aed 100644 --- a/homeassistant/components/mullvad/translations/en.json +++ b/homeassistant/components/mullvad/translations/en.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Password", - "username": "Username" - }, "description": "Set up the Mullvad VPN integration?" } } diff --git a/homeassistant/components/mullvad/translations/es.json b/homeassistant/components/mullvad/translations/es.json index 7b64c9b1128..f7ad856ea91 100644 --- a/homeassistant/components/mullvad/translations/es.json +++ b/homeassistant/components/mullvad/translations/es.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Fallo al conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Contrase\u00f1a", - "username": "Usuario" - }, "description": "\u00bfConfigurar la integraci\u00f3n VPN de Mullvad?" } } diff --git a/homeassistant/components/mullvad/translations/et.json b/homeassistant/components/mullvad/translations/et.json index 671d18a2cd3..66e74822d18 100644 --- a/homeassistant/components/mullvad/translations/et.json +++ b/homeassistant/components/mullvad/translations/et.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "\u00dchendumine nurjus", - "invalid_auth": "Tuvastamise viga", "unknown": "Ootamatu t\u00f5rge" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Salas\u00f5na", - "username": "Kasutajanimi" - }, "description": "Kas seadistada Mullvad VPN sidumine?" } } diff --git a/homeassistant/components/mullvad/translations/fr.json b/homeassistant/components/mullvad/translations/fr.json index 1a8b10de809..542412da986 100644 --- a/homeassistant/components/mullvad/translations/fr.json +++ b/homeassistant/components/mullvad/translations/fr.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, "step": { "user": { - "data": { - "host": "H\u00f4te", - "password": "Mot de passe", - "username": "Nom d'utilisateur" - }, "description": "Configurez l'int\u00e9gration VPN Mullvad?" } } diff --git a/homeassistant/components/mullvad/translations/he.json b/homeassistant/components/mullvad/translations/he.json index 7f60f15d598..1551f5e6bb0 100644 --- a/homeassistant/components/mullvad/translations/he.json +++ b/homeassistant/components/mullvad/translations/he.json @@ -5,17 +5,7 @@ }, "error": { "cannot_connect": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05ea\u05e7\u05d9\u05df", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "step": { - "user": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/hu.json b/homeassistant/components/mullvad/translations/hu.json index 0abcc301f0c..e92d5c4bdea 100644 --- a/homeassistant/components/mullvad/translations/hu.json +++ b/homeassistant/components/mullvad/translations/hu.json @@ -5,17 +5,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "step": { - "user": { - "data": { - "host": "Hoszt", - "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/id.json b/homeassistant/components/mullvad/translations/id.json index a5409549f19..1bab1395422 100644 --- a/homeassistant/components/mullvad/translations/id.json +++ b/homeassistant/components/mullvad/translations/id.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Gagal terhubung", - "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Kata Sandi", - "username": "Nama Pengguna" - }, "description": "Siapkan integrasi VPN Mullvad?" } } diff --git a/homeassistant/components/mullvad/translations/it.json b/homeassistant/components/mullvad/translations/it.json index 47cd8290f21..7b1941a5ee8 100644 --- a/homeassistant/components/mullvad/translations/it.json +++ b/homeassistant/components/mullvad/translations/it.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Password", - "username": "Nome utente" - }, "description": "Configurare l'integrazione VPN Mullvad?" } } diff --git a/homeassistant/components/mullvad/translations/ko.json b/homeassistant/components/mullvad/translations/ko.json index fd9134b977c..09f9ce4771c 100644 --- a/homeassistant/components/mullvad/translations/ko.json +++ b/homeassistant/components/mullvad/translations/ko.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { - "data": { - "host": "\ud638\uc2a4\ud2b8", - "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, "description": "Mullvad VPN \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } diff --git a/homeassistant/components/mullvad/translations/nl.json b/homeassistant/components/mullvad/translations/nl.json index aa4d80ac71d..e056a50a091 100644 --- a/homeassistant/components/mullvad/translations/nl.json +++ b/homeassistant/components/mullvad/translations/nl.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Wachtwoord", - "username": "Gebruikersnaam" - }, "description": "De Mullvad VPN-integratie instellen?" } } diff --git a/homeassistant/components/mullvad/translations/no.json b/homeassistant/components/mullvad/translations/no.json index d33f2640445..232c581b0b1 100644 --- a/homeassistant/components/mullvad/translations/no.json +++ b/homeassistant/components/mullvad/translations/no.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", - "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "step": { "user": { - "data": { - "host": "Vert", - "password": "Passord", - "username": "Brukernavn" - }, "description": "Sette opp Mullvad VPN-integrasjon?" } } diff --git a/homeassistant/components/mullvad/translations/pl.json b/homeassistant/components/mullvad/translations/pl.json index f5aca4e092c..249d2f1ff24 100644 --- a/homeassistant/components/mullvad/translations/pl.json +++ b/homeassistant/components/mullvad/translations/pl.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { - "data": { - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" - }, "description": "Skonfigurowa\u0107 integracj\u0119 Mullvad VPN?" } } diff --git a/homeassistant/components/mullvad/translations/pt.json b/homeassistant/components/mullvad/translations/pt.json index 561c8d77287..657ce03e544 100644 --- a/homeassistant/components/mullvad/translations/pt.json +++ b/homeassistant/components/mullvad/translations/pt.json @@ -5,17 +5,7 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "host": "Servidor", - "password": "Palavra-passe", - "username": "Nome de Utilizador" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/ru.json b/homeassistant/components/mullvad/translations/ru.json index af2ecd321f0..cf22d6e4f49 100644 --- a/homeassistant/components/mullvad/translations/ru.json +++ b/homeassistant/components/mullvad/translations/ru.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { "user": { - "data": { - "host": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Mullvad VPN." } } diff --git a/homeassistant/components/mullvad/translations/sv.json b/homeassistant/components/mullvad/translations/sv.json index ecc6740fc9d..c5ad71d784d 100644 --- a/homeassistant/components/mullvad/translations/sv.json +++ b/homeassistant/components/mullvad/translations/sv.json @@ -6,14 +6,6 @@ "error": { "cannot_connect": "Kunde inte ansluta", "unknown": "Ov\u00e4ntat fel" - }, - "step": { - "user": { - "data": { - "password": "L\u00f6senord", - "username": "Anv\u00e4ndarnamn" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/tr.json b/homeassistant/components/mullvad/translations/tr.json deleted file mode 100644 index 0f3ddabfc4f..00000000000 --- a/homeassistant/components/mullvad/translations/tr.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/zh-Hans.json b/homeassistant/components/mullvad/translations/zh-Hans.json index acb02a7d0f6..ae40024a841 100644 --- a/homeassistant/components/mullvad/translations/zh-Hans.json +++ b/homeassistant/components/mullvad/translations/zh-Hans.json @@ -5,16 +5,7 @@ }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "invalid_auth": "\u9a8c\u8bc1\u5931\u8d25", "unknown": "\u9884\u671f\u5916\u7684\u9519\u8bef" - }, - "step": { - "user": { - "data": { - "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/zh-Hant.json b/homeassistant/components/mullvad/translations/zh-Hant.json index 9a72286991c..dedfa2febbc 100644 --- a/homeassistant/components/mullvad/translations/zh-Hant.json +++ b/homeassistant/components/mullvad/translations/zh-Hant.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { "user": { - "data": { - "host": "\u4e3b\u6a5f\u7aef", - "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, "description": "\u8a2d\u5b9a Mullvad VPN \u6574\u5408\uff1f" } } diff --git a/homeassistant/components/mutesync/translations/de.json b/homeassistant/components/mutesync/translations/de.json index ac8a06458b9..613cac29b1c 100644 --- a/homeassistant/components/mutesync/translations/de.json +++ b/homeassistant/components/mutesync/translations/de.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Aktivieren Sie die Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", + "invalid_auth": "Aktivieredie Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/myq/translations/de.json b/homeassistant/components/myq/translations/de.json index 0a6941d6bcb..5d8b5acdc79 100644 --- a/homeassistant/components/myq/translations/de.json +++ b/homeassistant/components/myq/translations/de.json @@ -13,7 +13,9 @@ "reauth_confirm": { "data": { "password": "Passwort" - } + }, + "description": "Das Passwort f\u00fcr {username} ist nicht mehr g\u00fcltig.", + "title": "Authentifizieren Sie Ihr MyQ-Konto erneut" }, "user": { "data": { diff --git a/homeassistant/components/nam/translations/de.json b/homeassistant/components/nam/translations/de.json index 2920ef12e11..823a9675726 100644 --- a/homeassistant/components/nam/translations/de.json +++ b/homeassistant/components/nam/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "device_unsupported": "Das Ger\u00e4t wird nicht unterst\u00fctzt." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -9,6 +10,9 @@ }, "flow_title": "{name}", "step": { + "confirm_discovery": { + "description": "M\u00f6chtest du Nettigo Air Monitor unter {host} einrichten?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/neato/translations/bg.json b/homeassistant/components/neato/translations/bg.json index f652830217a..d8f67f2185d 100644 --- a/homeassistant/components/neato/translations/bg.json +++ b/homeassistant/components/neato/translations/bg.json @@ -5,17 +5,6 @@ }, "create_entry": { "default": "\u0412\u0438\u0436\u0442\u0435 [Neato \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f]({docs_url})." - }, - "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", - "vendor": "\u0414\u043e\u0441\u0442\u0430\u0432\u0447\u0438\u043a" - }, - "description": "\u0412\u0438\u0436\u0442\u0435 [Neato \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f]({docs_url}).", - "title": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 Neato \u0430\u043a\u0430\u0443\u043d\u0442" - } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/ca.json b/homeassistant/components/neato/translations/ca.json index f818135c51f..ab8210afb2f 100644 --- a/homeassistant/components/neato/translations/ca.json +++ b/homeassistant/components/neato/translations/ca.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" @@ -11,25 +10,12 @@ "create_entry": { "default": "Autenticaci\u00f3 exitosa" }, - "error": { - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" - }, "step": { "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" }, "reauth_confirm": { "title": "Vols comen\u00e7ar la configuraci\u00f3?" - }, - "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" } } }, diff --git a/homeassistant/components/neato/translations/cs.json b/homeassistant/components/neato/translations/cs.json index 5d45710f4a6..6cf7e314c4b 100644 --- a/homeassistant/components/neato/translations/cs.json +++ b/homeassistant/components/neato/translations/cs.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" @@ -11,24 +10,12 @@ "create_entry": { "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" }, - "error": { - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, "step": { "pick_implementation": { "title": "Vyberte metodu ov\u011b\u0159en\u00ed" }, "reauth_confirm": { "title": "Chcete za\u010d\u00edt nastavovat?" - }, - "user": { - "data": { - "password": "Heslo", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, - "description": "Viz [dokumentace Neato]({docs_url}).", - "title": "Informace o \u00fa\u010dtu Neato" } } }, diff --git a/homeassistant/components/neato/translations/da.json b/homeassistant/components/neato/translations/da.json index 785bf4aca9d..c68c4889631 100644 --- a/homeassistant/components/neato/translations/da.json +++ b/homeassistant/components/neato/translations/da.json @@ -5,17 +5,6 @@ }, "create_entry": { "default": "Se [Neato-dokumentation]({docs_url})." - }, - "step": { - "user": { - "data": { - "password": "Adgangskode", - "username": "Brugernavn", - "vendor": "Udbyder" - }, - "description": "Se [Neato-dokumentation]({docs_url}).", - "title": "Neato-kontooplysninger" - } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index 272191a4221..dac965a13bf 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "invalid_auth": "Ung\u00fcltige Authentifizierung", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler sind [im Hilfebereich]({docs_url}) zu finden", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" @@ -11,25 +10,12 @@ "create_entry": { "default": "Erfolgreich authentifiziert" }, - "error": { - "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" - }, "step": { "pick_implementation": { "title": "W\u00e4hle die Authentifizierungsmethode" }, "reauth_confirm": { "title": "M\u00f6chten Sie mit der Einrichtung beginnen?" - }, - "user": { - "data": { - "password": "Passwort", - "username": "Benutzername", - "vendor": "Hersteller" - }, - "description": "Siehe [Neato-Dokumentation]({docs_url}).", - "title": "Neato-Kontoinformationen" } } }, diff --git a/homeassistant/components/neato/translations/el.json b/homeassistant/components/neato/translations/el.json deleted file mode 100644 index 36bd6653da6..00000000000 --- a/homeassistant/components/neato/translations/el.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "abort": { - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" - }, - "error": { - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", - "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/en.json b/homeassistant/components/neato/translations/en.json index cc633979645..684d667678b 100644 --- a/homeassistant/components/neato/translations/en.json +++ b/homeassistant/components/neato/translations/en.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Device is already configured", "authorize_url_timeout": "Timeout generating authorize URL.", - "invalid_auth": "Invalid authentication", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful" @@ -11,25 +10,12 @@ "create_entry": { "default": "Successfully authenticated" }, - "error": { - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, "step": { "pick_implementation": { "title": "Pick Authentication Method" }, "reauth_confirm": { "title": "Do you want to start set up?" - }, - "user": { - "data": { - "password": "Password", - "username": "Username", - "vendor": "Vendor" - }, - "description": "See [Neato documentation]({docs_url}).", - "title": "Neato Account Info" } } }, diff --git a/homeassistant/components/neato/translations/es-419.json b/homeassistant/components/neato/translations/es-419.json index 46ae7ba2f86..5efd26cea61 100644 --- a/homeassistant/components/neato/translations/es-419.json +++ b/homeassistant/components/neato/translations/es-419.json @@ -5,17 +5,6 @@ }, "create_entry": { "default": "Consulte [Documentaci\u00f3n de Neato] ({docs_url})." - }, - "step": { - "user": { - "data": { - "password": "Contrase\u00f1a", - "username": "Nombre de usuario", - "vendor": "Vendedor" - }, - "description": "Consulte [Documentaci\u00f3n de Neato] ({docs_url}).", - "title": "Informaci\u00f3n de cuenta de Neato" - } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index b88a9d0cfa4..f9b6fe54e22 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" @@ -11,25 +10,12 @@ "create_entry": { "default": "Ver [documentaci\u00f3n Neato]({docs_url})." }, - "error": { - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "unknown": "Error inesperado" - }, "step": { "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { "title": "\u00bfQuieres iniciar la configuraci\u00f3n?" - }, - "user": { - "data": { - "password": "Contrase\u00f1a", - "username": "Usuario", - "vendor": "Vendedor" - }, - "description": "Ver [documentaci\u00f3n Neato]({docs_url}).", - "title": "Informaci\u00f3n de la cuenta de Neato" } } }, diff --git a/homeassistant/components/neato/translations/et.json b/homeassistant/components/neato/translations/et.json index 0c0aaa5f172..40e601dfe9d 100644 --- a/homeassistant/components/neato/translations/et.json +++ b/homeassistant/components/neato/translations/et.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", - "invalid_auth": "Tuvastamise viga", "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", "no_url_available": "URL-i pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", "reauth_successful": "Taastuvastamine \u00f5nnestus" @@ -11,25 +10,12 @@ "create_entry": { "default": "Tuvastamine \u00f5nnestus" }, - "error": { - "invalid_auth": "Tuvastamise viga", - "unknown": "Ootamatu t\u00f5rge" - }, "step": { "pick_implementation": { "title": "Vali tuvastusmeetod" }, "reauth_confirm": { "title": "Kas soovid alustada seadistamist?" - }, - "user": { - "data": { - "password": "Salas\u00f5na", - "username": "Kasutajanimi", - "vendor": "Tootja" - }, - "description": "Vaata [Neato documentation] ( {docs_url} ).", - "title": "Neato konto teave" } } }, diff --git a/homeassistant/components/neato/translations/fr.json b/homeassistant/components/neato/translations/fr.json index 26b97e83c0b..bf11c9edfe3 100644 --- a/homeassistant/components/neato/translations/fr.json +++ b/homeassistant/components/neato/translations/fr.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "D\u00e9j\u00e0 configur\u00e9", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", - "invalid_auth": "Authentification invalide", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation ", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" @@ -11,25 +10,12 @@ "create_entry": { "default": "Voir [Documentation Neato]({docs_url})." }, - "error": { - "invalid_auth": "Authentification invalide", - "unknown": "Erreur inattendue" - }, "step": { "pick_implementation": { "title": "S\u00e9lectionner une m\u00e9thode d'authentification" }, "reauth_confirm": { "title": "Voulez-vous commencer la configuration ?" - }, - "user": { - "data": { - "password": "Mot de passe", - "username": "Nom d'utilisateur", - "vendor": "Vendeur" - }, - "description": "Voir [Documentation Neato] ( {docs_url} ).", - "title": "Informations compte Neato" } } }, diff --git a/homeassistant/components/neato/translations/he.json b/homeassistant/components/neato/translations/he.json deleted file mode 100644 index 6f4191da70d..00000000000 --- a/homeassistant/components/neato/translations/he.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/hu.json b/homeassistant/components/neato/translations/hu.json index 3cb6ffd3364..90fb417e6a6 100644 --- a/homeassistant/components/neato/translations/hu.json +++ b/homeassistant/components/neato/translations/hu.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" @@ -11,25 +10,12 @@ "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" }, - "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, "step": { "pick_implementation": { "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" }, "reauth_confirm": { "title": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" - }, - "user": { - "data": { - "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v", - "vendor": "Sz\u00e1ll\u00edt\u00f3" - }, - "description": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3]({docs_url}).", - "title": "Neato Fi\u00f3kinform\u00e1ci\u00f3" } } }, diff --git a/homeassistant/components/neato/translations/id.json b/homeassistant/components/neato/translations/id.json index 17eee515787..b1811fe298e 100644 --- a/homeassistant/components/neato/translations/id.json +++ b/homeassistant/components/neato/translations/id.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", - "invalid_auth": "Autentikasi tidak valid", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", "reauth_successful": "Autentikasi ulang berhasil" @@ -11,25 +10,12 @@ "create_entry": { "default": "Berhasil diautentikasi" }, - "error": { - "invalid_auth": "Autentikasi tidak valid", - "unknown": "Kesalahan yang tidak diharapkan" - }, "step": { "pick_implementation": { "title": "Pilih Metode Autentikasi" }, "reauth_confirm": { "title": "Ingin memulai penyiapan?" - }, - "user": { - "data": { - "password": "Kata Sandi", - "username": "Nama Pengguna", - "vendor": "Vendor" - }, - "description": "Baca [dokumentasi Neato]({docs_url}).", - "title": "Info Akun Neato" } } }, diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json index b559c23bb1a..51d9119eb2c 100644 --- a/homeassistant/components/neato/translations/it.json +++ b/homeassistant/components/neato/translations/it.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "invalid_auth": "Autenticazione non valida", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" @@ -11,25 +10,12 @@ "create_entry": { "default": "Autenticazione riuscita" }, - "error": { - "invalid_auth": "Autenticazione non valida", - "unknown": "Errore imprevisto" - }, "step": { "pick_implementation": { "title": "Scegli il metodo di autenticazione" }, "reauth_confirm": { "title": "Vuoi iniziare la configurazione?" - }, - "user": { - "data": { - "password": "Password", - "username": "Nome utente", - "vendor": "Fornitore" - }, - "description": "Vedere la [Documentazione di Neato]({docs_url}).", - "title": "Informazioni sull'account Neato" } } }, diff --git a/homeassistant/components/neato/translations/ko.json b/homeassistant/components/neato/translations/ko.json index d08000871ea..dc651939665 100644 --- a/homeassistant/components/neato/translations/ko.json +++ b/homeassistant/components/neato/translations/ko.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" @@ -11,25 +10,12 @@ "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, - "error": { - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, "step": { "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" }, "reauth_confirm": { "title": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" - }, - "user": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", - "vendor": "\uacf5\uae09 \uc5c5\uccb4" - }, - "description": "[Neato \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "title": "Neato \uacc4\uc815 \uc815\ubcf4" } } }, diff --git a/homeassistant/components/neato/translations/lb.json b/homeassistant/components/neato/translations/lb.json index adc42ae840d..d54443e6671 100644 --- a/homeassistant/components/neato/translations/lb.json +++ b/homeassistant/components/neato/translations/lb.json @@ -3,32 +3,18 @@ "abort": { "already_configured": "Apparat ass scho konfigur\u00e9iert", "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.", - "invalid_auth": "Ong\u00eblteg Authentifikatioun", "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich" }, "create_entry": { "default": "Kuckt [Neato Dokumentatioun]({docs_url})." }, - "error": { - "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "unknown": "Onerwaarte Feeler" - }, "step": { "pick_implementation": { "title": "Authentifikatiouns Method auswielen" }, "reauth_confirm": { "title": "Soll den Ariichtungs Prozess gestart ginn?" - }, - "user": { - "data": { - "password": "Passwuert", - "username": "Benotzernumm", - "vendor": "Hiersteller" - }, - "description": "Kuckt [Neato Dokumentatioun]({docs_url}).", - "title": "Neato Kont Informatiounen" } } }, diff --git a/homeassistant/components/neato/translations/lv.json b/homeassistant/components/neato/translations/lv.json deleted file mode 100644 index 6ada7a17452..00000000000 --- a/homeassistant/components/neato/translations/lv.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "password": "Parole", - "username": "Lietot\u0101jv\u0101rds" - }, - "title": "Neato konta inform\u0101cija" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/nl.json b/homeassistant/components/neato/translations/nl.json index 919928ad91a..3d7bbab2e75 100644 --- a/homeassistant/components/neato/translations/nl.json +++ b/homeassistant/components/neato/translations/nl.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "invalid_auth": "Ongeldige authenticatie", "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "reauth_successful": "Herauthenticatie was succesvol" @@ -11,25 +10,12 @@ "create_entry": { "default": "Succesvol geauthenticeerd" }, - "error": { - "invalid_auth": "Ongeldige authenticatie", - "unknown": "Onverwachte fout" - }, "step": { "pick_implementation": { "title": "Kies een authenticatie methode" }, "reauth_confirm": { "title": "Wil je beginnen met instellen?" - }, - "user": { - "data": { - "password": "Wachtwoord", - "username": "Gebruikersnaam", - "vendor": "Leverancier" - }, - "description": "Zie [Neato-documentatie] ({docs_url}).", - "title": "Neato-account info" } } }, diff --git a/homeassistant/components/neato/translations/nn.json b/homeassistant/components/neato/translations/nn.json deleted file mode 100644 index 7c129cba3af..00000000000 --- a/homeassistant/components/neato/translations/nn.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "username": "Brukarnamn" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/no.json b/homeassistant/components/neato/translations/no.json index a788c79ff5d..fc50308eda8 100644 --- a/homeassistant/components/neato/translations/no.json +++ b/homeassistant/components/neato/translations/no.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", - "invalid_auth": "Ugyldig godkjenning", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" @@ -11,25 +10,12 @@ "create_entry": { "default": "Vellykket godkjenning" }, - "error": { - "invalid_auth": "Ugyldig godkjenning", - "unknown": "Uventet feil" - }, "step": { "pick_implementation": { "title": "Velg godkjenningsmetode" }, "reauth_confirm": { "title": "Vil du starte oppsettet?" - }, - "user": { - "data": { - "password": "Passord", - "username": "Brukernavn", - "vendor": "Leverand\u00f8r" - }, - "description": "Se [Neato dokumentasjon]({docs_url}).", - "title": "Neato kontoinformasjon" } } }, diff --git a/homeassistant/components/neato/translations/pl.json b/homeassistant/components/neato/translations/pl.json index 3177ed9d8e8..34f1d42bda5 100644 --- a/homeassistant/components/neato/translations/pl.json +++ b/homeassistant/components/neato/translations/pl.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", - "invalid_auth": "Niepoprawne uwierzytelnienie", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" @@ -11,25 +10,12 @@ "create_entry": { "default": "Pomy\u015blnie uwierzytelniono" }, - "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, "step": { "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" }, "reauth_confirm": { "title": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" - }, - "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" } } }, diff --git a/homeassistant/components/neato/translations/pt-BR.json b/homeassistant/components/neato/translations/pt-BR.json deleted file mode 100644 index 932b4b8a72e..00000000000 --- a/homeassistant/components/neato/translations/pt-BR.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "username": "Usu\u00e1rio" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/pt.json b/homeassistant/components/neato/translations/pt.json index 48e73c763f0..030211ca1cf 100644 --- a/homeassistant/components/neato/translations/pt.json +++ b/homeassistant/components/neato/translations/pt.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" @@ -11,22 +10,12 @@ "create_entry": { "default": "Autenticado com sucesso" }, - "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" - }, "step": { "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" }, "reauth_confirm": { "title": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" - }, - "user": { - "data": { - "password": "Palavra-passe", - "username": "Nome de Utilizador" - } } } } diff --git a/homeassistant/components/neato/translations/ru.json b/homeassistant/components/neato/translations/ru.json index 430300126a3..29201df669d 100644 --- a/homeassistant/components/neato/translations/ru.json +++ b/homeassistant/components/neato/translations/ru.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "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.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." @@ -11,25 +10,12 @@ "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." }, - "error": { - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, "step": { "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" }, "reauth_confirm": { "title": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" - }, - "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", - "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 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({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" } } }, diff --git a/homeassistant/components/neato/translations/sl.json b/homeassistant/components/neato/translations/sl.json index 96af3b0453f..1aaae76ada8 100644 --- a/homeassistant/components/neato/translations/sl.json +++ b/homeassistant/components/neato/translations/sl.json @@ -16,15 +16,6 @@ }, "reauth_confirm": { "title": "Bi radi zagnali namestitev?" - }, - "user": { - "data": { - "password": "Geslo", - "username": "Uporabni\u0161ko ime", - "vendor": "Prodajalec" - }, - "description": "Glejte [neato dokumentacija] ({docs_url}).", - "title": "Podatki o ra\u010dunu Neato" } } }, diff --git a/homeassistant/components/neato/translations/sv.json b/homeassistant/components/neato/translations/sv.json index 544b6d2b292..71f24f595e5 100644 --- a/homeassistant/components/neato/translations/sv.json +++ b/homeassistant/components/neato/translations/sv.json @@ -5,17 +5,6 @@ }, "create_entry": { "default": "Se [Neato-dokumentation]({docs_url})." - }, - "step": { - "user": { - "data": { - "password": "L\u00f6senord", - "username": "Anv\u00e4ndarnamn", - "vendor": "Leverant\u00f6r" - }, - "description": "Se [Neato-dokumentation] ({docs_url}).", - "title": "Neato-kontoinfo" - } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/tr.json b/homeassistant/components/neato/translations/tr.json index 53a8e0503cb..18fa4749d88 100644 --- a/homeassistant/components/neato/translations/tr.json +++ b/homeassistant/components/neato/translations/tr.json @@ -2,23 +2,11 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, - "error": { - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "unknown": "Beklenmeyen hata" - }, "step": { "pick_implementation": { "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" - }, - "user": { - "data": { - "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "title": "Neato Hesap Bilgisi" } } } diff --git a/homeassistant/components/neato/translations/uk.json b/homeassistant/components/neato/translations/uk.json index 58b56a52f6c..353005546cf 100644 --- a/homeassistant/components/neato/translations/uk.json +++ b/homeassistant/components/neato/translations/uk.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", - "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" @@ -11,25 +10,12 @@ "create_entry": { "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." }, - "error": { - "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", - "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" - }, "step": { "pick_implementation": { "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" }, "reauth_confirm": { "title": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" - }, - "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", - "vendor": "\u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a" - }, - "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c.", - "title": "Neato" } } }, diff --git a/homeassistant/components/neato/translations/zh-Hans.json b/homeassistant/components/neato/translations/zh-Hans.json deleted file mode 100644 index b0b26b02261..00000000000 --- a/homeassistant/components/neato/translations/zh-Hans.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/zh-Hant.json b/homeassistant/components/neato/translations/zh-Hant.json index 35e90146b36..781ee1f952b 100644 --- a/homeassistant/components/neato/translations/zh-Hant.json +++ b/homeassistant/components/neato/translations/zh-Hant.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" @@ -11,25 +10,12 @@ "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" }, - "error": { - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, "step": { "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" }, "reauth_confirm": { "title": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" - }, - "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" } } }, diff --git a/homeassistant/components/nest/translations/bg.json b/homeassistant/components/nest/translations/bg.json index b45171d2817..e53ab436a77 100644 --- a/homeassistant/components/nest/translations/bg.json +++ b/homeassistant/components/nest/translations/bg.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a." }, "error": { diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index bc19d5c2c7c..35cc6eff946 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json index 843ce983305..cbba19dac1d 100644 --- a/homeassistant/components/nest/translations/cs.json +++ b/homeassistant/components/nest/translations/cs.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", diff --git a/homeassistant/components/nest/translations/da.json b/homeassistant/components/nest/translations/da.json index 234c9ba97f4..054b4442506 100644 --- a/homeassistant/components/nest/translations/da.json +++ b/homeassistant/components/nest/translations/da.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", "authorize_url_timeout": "Timeout ved generering af autoriseret url." }, "error": { diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 3925b7537b2..dde725681d8 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index d7b000d921f..4487beb0f43 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", diff --git a/homeassistant/components/nest/translations/es-419.json b/homeassistant/components/nest/translations/es-419.json index 94c1d95e4d7..185856f61de 100644 --- a/homeassistant/components/nest/translations/es-419.json +++ b/homeassistant/components/nest/translations/es-419.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n." }, "error": { diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 4c0b8b2617c..f9e88c9180f 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 1319bc2ca4b..773835ea29b 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Tundmatu viga tuvastamise URL-i loomisel.", "authorize_url_timeout": "Tuvastamise URL-i loomise ajal\u00f5pp.", "missing_configuration": "Osis pole seadistatud. Vaata dokumentatsiooni.", "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", diff --git a/homeassistant/components/nest/translations/fi.json b/homeassistant/components/nest/translations/fi.json index d81891d89b4..5365f73b721 100644 --- a/homeassistant/components/nest/translations/fi.json +++ b/homeassistant/components/nest/translations/fi.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "authorize_url_fail": "Tuntematon virhe luotaessa valtuutuksen URL-osoitetta." - }, "step": { "init": { "data": { diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index ce716fb3083..2d74b179d19 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index 9d0974128dd..6f43df5ac81 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea.", "authorize_url_timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea" }, "error": { diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 9400ea7875c..5690724c4a0 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 757c53e2866..d035433361f 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index c6e62db314d..bb4c916384d 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", diff --git a/homeassistant/components/nest/translations/ko.json b/homeassistant/components/nest/translations/ko.json index f8d6de2244a..048a0c7d100 100644 --- a/homeassistant/components/nest/translations/ko.json +++ b/homeassistant/components/nest/translations/ko.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/nest/translations/lb.json b/homeassistant/components/nest/translations/lb.json index 612d1f30258..010f34e0dc5 100644 --- a/homeassistant/components/nest/translations/lb.json +++ b/homeassistant/components/nest/translations/lb.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich", diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index a55f19d2a42..53066e9e720 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", diff --git a/homeassistant/components/nest/translations/nn.json b/homeassistant/components/nest/translations/nn.json index d9251d1e327..19094b460ec 100644 --- a/homeassistant/components/nest/translations/nn.json +++ b/homeassistant/components/nest/translations/nn.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ukjent feil ved generering av autentiserings-URL", "authorize_url_timeout": "Tida gjekk ut for generert autentikasjons-URL" }, "error": { diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index d6b6c89bcaa..914545d5a54 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index d1147e03afc..98506684dbb 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index ae2d88eeeaa..aab253057dd 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Excedido tempo limite de url de autoriza\u00e7\u00e3o" }, "error": { diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 33ff857af7e..13a7439b93d 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 4897114d286..1919f8db89e 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "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.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", diff --git a/homeassistant/components/nest/translations/sl.json b/homeassistant/components/nest/translations/sl.json index 25660b4805e..84f07fdcf42 100644 --- a/homeassistant/components/nest/translations/sl.json +++ b/homeassistant/components/nest/translations/sl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", "reauth_successful": "Ponovna overitev je uspela.", "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index cddb9e2fe79..d929451e504 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ok\u00e4nt fel vid generering av autentisieringsadress.", "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress." }, "error": { diff --git a/homeassistant/components/nest/translations/uk.json b/homeassistant/components/nest/translations/uk.json index f2869a76f42..f2ee64e7fc4 100644 --- a/homeassistant/components/nest/translations/uk.json +++ b/homeassistant/components/nest/translations/uk.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", diff --git a/homeassistant/components/nest/translations/zh-Hans.json b/homeassistant/components/nest/translations/zh-Hans.json index 38be428a0c7..3d481dec9d5 100644 --- a/homeassistant/components/nest/translations/zh-Hans.json +++ b/homeassistant/components/nest/translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002" }, "error": { diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index a271ca666f4..d9b29c7fa7a 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", diff --git a/homeassistant/components/nightscout/translations/en.json b/homeassistant/components/nightscout/translations/en.json index af29cff5b56..d8b4c441283 100644 --- a/homeassistant/components/nightscout/translations/en.json +++ b/homeassistant/components/nightscout/translations/en.json @@ -8,6 +8,7 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, + "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/ca.json b/homeassistant/components/nzbget/translations/ca.json index 4bd1b53b762..396a63da66c 100644 --- a/homeassistant/components/nzbget/translations/ca.json +++ b/homeassistant/components/nzbget/translations/ca.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/et.json b/homeassistant/components/nzbget/translations/et.json index ee83b59928d..719ec008119 100644 --- a/homeassistant/components/nzbget/translations/et.json +++ b/homeassistant/components/nzbget/translations/et.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/no.json b/homeassistant/components/nzbget/translations/no.json index cb9fce35897..170d99d5853 100644 --- a/homeassistant/components/nzbget/translations/no.json +++ b/homeassistant/components/nzbget/translations/no.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/ru.json b/homeassistant/components/nzbget/translations/ru.json index 4c5d7379527..ea48445df34 100644 --- a/homeassistant/components/nzbget/translations/ru.json +++ b/homeassistant/components/nzbget/translations/ru.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/zh-Hant.json b/homeassistant/components/nzbget/translations/zh-Hant.json index 26fd5f34117..28edec03d67 100644 --- a/homeassistant/components/nzbget/translations/zh-Hant.json +++ b/homeassistant/components/nzbget/translations/zh-Hant.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "NZBGet\uff1a{name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ondilo_ico/translations/ca.json b/homeassistant/components/ondilo_ico/translations/ca.json index 77453bda398..195d3d59262 100644 --- a/homeassistant/components/ondilo_ico/translations/ca.json +++ b/homeassistant/components/ondilo_ico/translations/ca.json @@ -12,6 +12,5 @@ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/cs.json b/homeassistant/components/ondilo_ico/translations/cs.json index bcb8849839c..2a54a82f41b 100644 --- a/homeassistant/components/ondilo_ico/translations/cs.json +++ b/homeassistant/components/ondilo_ico/translations/cs.json @@ -12,6 +12,5 @@ "title": "Vyberte metodu ov\u011b\u0159en\u00ed" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/de.json b/homeassistant/components/ondilo_ico/translations/de.json index ad11cefde66..5bab6ed132b 100644 --- a/homeassistant/components/ondilo_ico/translations/de.json +++ b/homeassistant/components/ondilo_ico/translations/de.json @@ -12,6 +12,5 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/en.json b/homeassistant/components/ondilo_ico/translations/en.json index c88a152ef81..e3849fc17a3 100644 --- a/homeassistant/components/ondilo_ico/translations/en.json +++ b/homeassistant/components/ondilo_ico/translations/en.json @@ -12,6 +12,5 @@ "title": "Pick Authentication Method" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/es.json b/homeassistant/components/ondilo_ico/translations/es.json index 2394c610796..db8d744d176 100644 --- a/homeassistant/components/ondilo_ico/translations/es.json +++ b/homeassistant/components/ondilo_ico/translations/es.json @@ -12,6 +12,5 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/et.json b/homeassistant/components/ondilo_ico/translations/et.json index 132e9849cf1..c7d46e7e942 100644 --- a/homeassistant/components/ondilo_ico/translations/et.json +++ b/homeassistant/components/ondilo_ico/translations/et.json @@ -12,6 +12,5 @@ "title": "Vali tuvastusmeetod" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/fr.json b/homeassistant/components/ondilo_ico/translations/fr.json index c05fc0caaa6..540d3e1e6c2 100644 --- a/homeassistant/components/ondilo_ico/translations/fr.json +++ b/homeassistant/components/ondilo_ico/translations/fr.json @@ -12,6 +12,5 @@ "title": "S\u00e9lectionner une m\u00e9thode d'authentification" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/id.json b/homeassistant/components/ondilo_ico/translations/id.json index 1227a6d6689..876fe2f8c39 100644 --- a/homeassistant/components/ondilo_ico/translations/id.json +++ b/homeassistant/components/ondilo_ico/translations/id.json @@ -12,6 +12,5 @@ "title": "Pilih Metode Autentikasi" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/it.json b/homeassistant/components/ondilo_ico/translations/it.json index cd75684a437..42536508716 100644 --- a/homeassistant/components/ondilo_ico/translations/it.json +++ b/homeassistant/components/ondilo_ico/translations/it.json @@ -12,6 +12,5 @@ "title": "Scegli il metodo di autenticazione" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/ko.json b/homeassistant/components/ondilo_ico/translations/ko.json index 88f3d678171..fa000ea1c06 100644 --- a/homeassistant/components/ondilo_ico/translations/ko.json +++ b/homeassistant/components/ondilo_ico/translations/ko.json @@ -12,6 +12,5 @@ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/nl.json b/homeassistant/components/ondilo_ico/translations/nl.json index 50d09340555..0613d559fce 100644 --- a/homeassistant/components/ondilo_ico/translations/nl.json +++ b/homeassistant/components/ondilo_ico/translations/nl.json @@ -12,6 +12,5 @@ "title": "Kies een authenticatie methode" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/no.json b/homeassistant/components/ondilo_ico/translations/no.json index 4a06b93d045..a8f6ce4f9a3 100644 --- a/homeassistant/components/ondilo_ico/translations/no.json +++ b/homeassistant/components/ondilo_ico/translations/no.json @@ -12,6 +12,5 @@ "title": "Velg godkjenningsmetode" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/pl.json b/homeassistant/components/ondilo_ico/translations/pl.json index f3aa08a250f..8c75c11dd7c 100644 --- a/homeassistant/components/ondilo_ico/translations/pl.json +++ b/homeassistant/components/ondilo_ico/translations/pl.json @@ -12,6 +12,5 @@ "title": "Wybierz metod\u0119 uwierzytelniania" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/ru.json b/homeassistant/components/ondilo_ico/translations/ru.json index 199eb034c93..10a412c8a64 100644 --- a/homeassistant/components/ondilo_ico/translations/ru.json +++ b/homeassistant/components/ondilo_ico/translations/ru.json @@ -12,6 +12,5 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/tr.json b/homeassistant/components/ondilo_ico/translations/tr.json index 96722757365..6a0084d0a96 100644 --- a/homeassistant/components/ondilo_ico/translations/tr.json +++ b/homeassistant/components/ondilo_ico/translations/tr.json @@ -5,6 +5,5 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/uk.json b/homeassistant/components/ondilo_ico/translations/uk.json index 31e5834b027..b7e2a5601bf 100644 --- a/homeassistant/components/ondilo_ico/translations/uk.json +++ b/homeassistant/components/ondilo_ico/translations/uk.json @@ -12,6 +12,5 @@ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/zh-Hant.json b/homeassistant/components/ondilo_ico/translations/zh-Hant.json index ea1902b3295..b740fd3e063 100644 --- a/homeassistant/components/ondilo_ico/translations/zh-Hant.json +++ b/homeassistant/components/ondilo_ico/translations/zh-Hant.json @@ -12,6 +12,5 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/bg.json b/homeassistant/components/opentherm_gw/translations/bg.json index fbcaed52db5..3b120cebde9 100644 --- a/homeassistant/components/opentherm_gw/translations/bg.json +++ b/homeassistant/components/opentherm_gw/translations/bg.json @@ -19,8 +19,7 @@ "step": { "init": { "data": { - "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043f\u043e\u0434\u0430", - "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442" + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043f\u043e\u0434\u0430" }, "description": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/ca.json b/homeassistant/components/opentherm_gw/translations/ca.json index 3f38055fb8c..c4caf76d233 100644 --- a/homeassistant/components/opentherm_gw/translations/ca.json +++ b/homeassistant/components/opentherm_gw/translations/ca.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Temperatura de la planta", - "precision": "Precisi\u00f3", "read_precision": "Llegeix precisi\u00f3", "set_precision": "Defineix precisi\u00f3", "temporary_override_mode": "Mode de sobreescriptura temporal" diff --git a/homeassistant/components/opentherm_gw/translations/cs.json b/homeassistant/components/opentherm_gw/translations/cs.json index 1b497fcf396..39d70d05cfc 100644 --- a/homeassistant/components/opentherm_gw/translations/cs.json +++ b/homeassistant/components/opentherm_gw/translations/cs.json @@ -20,8 +20,7 @@ "step": { "init": { "data": { - "floor_temperature": "Teplota podlahy", - "precision": "P\u0159esnost" + "floor_temperature": "Teplota podlahy" }, "description": "Mo\u017enosti br\u00e1ny OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/translations/da.json b/homeassistant/components/opentherm_gw/translations/da.json index efc2262b415..cca7cb347db 100644 --- a/homeassistant/components/opentherm_gw/translations/da.json +++ b/homeassistant/components/opentherm_gw/translations/da.json @@ -19,8 +19,7 @@ "step": { "init": { "data": { - "floor_temperature": "Gulvtemperatur", - "precision": "Pr\u00e6cision" + "floor_temperature": "Gulvtemperatur" }, "description": "Indstillinger for OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/de.json b/homeassistant/components/opentherm_gw/translations/de.json index 66636a73a26..e7be7815366 100644 --- a/homeassistant/components/opentherm_gw/translations/de.json +++ b/homeassistant/components/opentherm_gw/translations/de.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Boden-Temperatur", - "precision": "Genauigkeit", "read_precision": "Pr\u00e4zision abfragen", "set_precision": "Pr\u00e4zision einstellen", "temporary_override_mode": "Tempor\u00e4rer Sollwert\u00fcbersteuerungsmodus" diff --git a/homeassistant/components/opentherm_gw/translations/en.json b/homeassistant/components/opentherm_gw/translations/en.json index a4e9eb664b2..c1e897c631b 100644 --- a/homeassistant/components/opentherm_gw/translations/en.json +++ b/homeassistant/components/opentherm_gw/translations/en.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Floor Temperature", - "precision": "Precision", "read_precision": "Read Precision", "set_precision": "Set Precision", "temporary_override_mode": "Temporary Setpoint Override Mode" diff --git a/homeassistant/components/opentherm_gw/translations/es-419.json b/homeassistant/components/opentherm_gw/translations/es-419.json index 3b3a32987be..f9a6f60b463 100644 --- a/homeassistant/components/opentherm_gw/translations/es-419.json +++ b/homeassistant/components/opentherm_gw/translations/es-419.json @@ -20,7 +20,6 @@ "init": { "data": { "floor_temperature": "Temperatura del piso", - "precision": "Precisi\u00f3n", "read_precision": "Leer precisi\u00f3n", "set_precision": "Establecer precisi\u00f3n" }, diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index e0799932a52..d780548a8fa 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Temperatura del suelo", - "precision": "Precisi\u00f3n", "read_precision": "Leer precisi\u00f3n", "set_precision": "Establecer precisi\u00f3n", "temporary_override_mode": "Modo de anulaci\u00f3n temporal del punto de ajuste" diff --git a/homeassistant/components/opentherm_gw/translations/et.json b/homeassistant/components/opentherm_gw/translations/et.json index 9aef362a6a0..67528b034c5 100644 --- a/homeassistant/components/opentherm_gw/translations/et.json +++ b/homeassistant/components/opentherm_gw/translations/et.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "P\u00f5randa temperatuur", - "precision": "T\u00e4psus", "read_precision": "Lugemi t\u00e4psus", "set_precision": "M\u00e4\u00e4ra lugemi t\u00e4psus", "temporary_override_mode": "Ajutine seadepunkti alistamine" diff --git a/homeassistant/components/opentherm_gw/translations/fr.json b/homeassistant/components/opentherm_gw/translations/fr.json index c9a19eba3dd..6b9bf032244 100644 --- a/homeassistant/components/opentherm_gw/translations/fr.json +++ b/homeassistant/components/opentherm_gw/translations/fr.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Temp\u00e9rature du sol", - "precision": "Pr\u00e9cision", "read_precision": "Pr\u00e9cision de lecture", "set_precision": "D\u00e9finir la pr\u00e9cision", "temporary_override_mode": "Mode de neutralisation du point de consigne temporaire" diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index 9ca79a3ccdd..78ff8c88636 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Padl\u00f3 h\u0151m\u00e9rs\u00e9klete", - "precision": "Pontoss\u00e1g", "temporary_override_mode": "Ideiglenes be\u00e1ll\u00edt\u00e1s fel\u00fclb\u00edr\u00e1l\u00e1si m\u00f3dja" } } diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json index c0fc97d9c8f..f3677b9de3b 100644 --- a/homeassistant/components/opentherm_gw/translations/id.json +++ b/homeassistant/components/opentherm_gw/translations/id.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Suhu Lantai", - "precision": "Tingkat Presisi", "read_precision": "Tingkat Presisi Baca", "set_precision": "Atur Presisi", "temporary_override_mode": "Mode Penimpaan Setpoint Sementara" diff --git a/homeassistant/components/opentherm_gw/translations/it.json b/homeassistant/components/opentherm_gw/translations/it.json index a082cd87586..4b3407cd95f 100644 --- a/homeassistant/components/opentherm_gw/translations/it.json +++ b/homeassistant/components/opentherm_gw/translations/it.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Temperatura del pavimento", - "precision": "Precisione", "read_precision": "Leggi la precisione", "set_precision": "Imposta la precisione", "temporary_override_mode": "Modalit\u00e0 di esclusione temporanea del setpoint" diff --git a/homeassistant/components/opentherm_gw/translations/ko.json b/homeassistant/components/opentherm_gw/translations/ko.json index 658fd24348e..178bf915a6b 100644 --- a/homeassistant/components/opentherm_gw/translations/ko.json +++ b/homeassistant/components/opentherm_gw/translations/ko.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", - "precision": "\uc815\ubc00\ub3c4", "read_precision": "\uc77d\uae30 \uc815\ubc00\ub3c4", "set_precision": "\uc815\ubc00\ub3c4 \uc124\uc815\ud558\uae30", "temporary_override_mode": "\uc784\uc2dc \uc124\uc815\uac12 \uc7ac\uc815\uc758 \ubaa8\ub4dc" diff --git a/homeassistant/components/opentherm_gw/translations/lb.json b/homeassistant/components/opentherm_gw/translations/lb.json index 452ca69540f..e90a6c2a310 100644 --- a/homeassistant/components/opentherm_gw/translations/lb.json +++ b/homeassistant/components/opentherm_gw/translations/lb.json @@ -20,8 +20,7 @@ "step": { "init": { "data": { - "floor_temperature": "Buedem Temperatur", - "precision": "Pr\u00e4zisioun" + "floor_temperature": "Buedem Temperatur" }, "description": "Optioune fir OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/lv.json b/homeassistant/components/opentherm_gw/translations/lv.json index 2c146e9d563..916fe4661a6 100644 --- a/homeassistant/components/opentherm_gw/translations/lv.json +++ b/homeassistant/components/opentherm_gw/translations/lv.json @@ -3,8 +3,7 @@ "step": { "init": { "data": { - "floor_temperature": "Gr\u012bdas temperat\u016bra", - "precision": "Precizit\u0101te" + "floor_temperature": "Gr\u012bdas temperat\u016bra" } } } diff --git a/homeassistant/components/opentherm_gw/translations/nl.json b/homeassistant/components/opentherm_gw/translations/nl.json index 5a4d868e81e..025cdea128d 100644 --- a/homeassistant/components/opentherm_gw/translations/nl.json +++ b/homeassistant/components/opentherm_gw/translations/nl.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Vloertemperatuur", - "precision": "Precisie", "read_precision": "Lees Precisie", "set_precision": "Precisie instellen", "temporary_override_mode": "Tijdelijke setpoint-overschrijvingsmodus" diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index 8d82d3c6106..9ef1aa0a5fb 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Etasje Temperatur", - "precision": "Presisjon", "read_precision": "Les presisjon", "set_precision": "Angi presisjon", "temporary_override_mode": "Midlertidig overstyringsmodus for settpunkt" diff --git a/homeassistant/components/opentherm_gw/translations/pl.json b/homeassistant/components/opentherm_gw/translations/pl.json index dc06752e404..9e1c4b1363d 100644 --- a/homeassistant/components/opentherm_gw/translations/pl.json +++ b/homeassistant/components/opentherm_gw/translations/pl.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142", - "precision": "Precyzja", "read_precision": "Odczytaj precyzj\u0119", "set_precision": "Ustaw precyzj\u0119" }, diff --git a/homeassistant/components/opentherm_gw/translations/pt-BR.json b/homeassistant/components/opentherm_gw/translations/pt-BR.json deleted file mode 100644 index 96907fd4cdc..00000000000 --- a/homeassistant/components/opentherm_gw/translations/pt-BR.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "precision": "Precis\u00e3o" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/pt.json b/homeassistant/components/opentherm_gw/translations/pt.json index 85b6e617963..94a446dbe71 100644 --- a/homeassistant/components/opentherm_gw/translations/pt.json +++ b/homeassistant/components/opentherm_gw/translations/pt.json @@ -12,14 +12,5 @@ } } } - }, - "options": { - "step": { - "init": { - "data": { - "precision": "Precis\u00e3o" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/ru.json b/homeassistant/components/opentherm_gw/translations/ru.json index 3b10be1166a..9f2bfff07cf 100644 --- a/homeassistant/components/opentherm_gw/translations/ru.json +++ b/homeassistant/components/opentherm_gw/translations/ru.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", - "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c", "read_precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0447\u0442\u0435\u043d\u0438\u044f", "set_precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438", "temporary_override_mode": "\u0420\u0435\u0436\u0438\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0430\u0432\u043a\u0438" diff --git a/homeassistant/components/opentherm_gw/translations/sl.json b/homeassistant/components/opentherm_gw/translations/sl.json index fb8ed5cac6d..a54ceb5031e 100644 --- a/homeassistant/components/opentherm_gw/translations/sl.json +++ b/homeassistant/components/opentherm_gw/translations/sl.json @@ -19,8 +19,7 @@ "step": { "init": { "data": { - "floor_temperature": "Temperatura nadstropja", - "precision": "Natan\u010dnost" + "floor_temperature": "Temperatura nadstropja" }, "description": "Mo\u017enosti za prehod OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/translations/sv.json b/homeassistant/components/opentherm_gw/translations/sv.json index 6f21273b67c..01aa96564ac 100644 --- a/homeassistant/components/opentherm_gw/translations/sv.json +++ b/homeassistant/components/opentherm_gw/translations/sv.json @@ -19,8 +19,7 @@ "step": { "init": { "data": { - "floor_temperature": "Golvetemperatur", - "precision": "Precision" + "floor_temperature": "Golvetemperatur" }, "description": "Alternativ f\u00f6r OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/uk.json b/homeassistant/components/opentherm_gw/translations/uk.json index af769927113..fdffbfec00e 100644 --- a/homeassistant/components/opentherm_gw/translations/uk.json +++ b/homeassistant/components/opentherm_gw/translations/uk.json @@ -20,8 +20,7 @@ "step": { "init": { "data": { - "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u0456\u0434\u043b\u043e\u0433\u0438", - "precision": "\u0422\u043e\u0447\u043d\u0456\u0441\u0442\u044c" + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u0456\u0434\u043b\u043e\u0433\u0438" }, "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0443 Opentherm" } diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json index fff046ef244..7b8466aa424 100644 --- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", - "precision": "\u6e96\u78ba\u5ea6", "read_precision": "\u8b80\u53d6\u7cbe\u6e96\u5ea6", "set_precision": "\u8a2d\u5b9a\u7cbe\u6e96\u5ea6", "temporary_override_mode": "\u81e8\u6642 Setpoint \u8986\u84cb\u6a21\u5f0f" diff --git a/homeassistant/components/ovo_energy/translations/ca.json b/homeassistant/components/ovo_energy/translations/ca.json index 3cc971434a4..f8552caa86b 100644 --- a/homeassistant/components/ovo_energy/translations/ca.json +++ b/homeassistant/components/ovo_energy/translations/ca.json @@ -5,7 +5,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/et.json b/homeassistant/components/ovo_energy/translations/et.json index b91f360159a..ab33d27f337 100644 --- a/homeassistant/components/ovo_energy/translations/et.json +++ b/homeassistant/components/ovo_energy/translations/et.json @@ -5,7 +5,7 @@ "cannot_connect": "\u00dchendus nurjus", "invalid_auth": "Tuvastamise viga" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json index 97bf9fc498f..64b989de78b 100644 --- a/homeassistant/components/ovo_energy/translations/no.json +++ b/homeassistant/components/ovo_energy/translations/no.json @@ -5,7 +5,7 @@ "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json index 89eb632102f..d0a2ffb798d 100644 --- a/homeassistant/components/ovo_energy/translations/ru.json +++ b/homeassistant/components/ovo_energy/translations/ru.json @@ -5,7 +5,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/zh-Hant.json b/homeassistant/components/ovo_energy/translations/zh-Hant.json index 43f456f7574..0b1c218d94c 100644 --- a/homeassistant/components/ovo_energy/translations/zh-Hant.json +++ b/homeassistant/components/ovo_energy/translations/zh-Hant.json @@ -5,7 +5,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "OVO Energy\uff1a{username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index ee580d2786e..c29fa230519 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -8,7 +8,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index 8ea77f78104..029b334f7c4 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -8,7 +8,7 @@ "invalid_auth": "Tuvastamine nurjus", "unknown": "Tundmatu viga" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index f8e128a0d27..58ee9d4aed8 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -8,7 +8,7 @@ "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index f027ebfc772..c7b28d5b415 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -8,7 +8,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index 62e186ab61a..edd67dd41cb 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -8,7 +8,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "Smile : {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/point/translations/bg.json b/homeassistant/components/point/translations/bg.json index 9a52ca8ad52..4356554e7f8 100644 --- a/homeassistant/components/point/translations/bg.json +++ b/homeassistant/components/point/translations/bg.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Point \u0430\u043a\u0430\u0443\u043d\u0442.", - "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.", "external_setup": "Point \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043e\u0442 \u0434\u0440\u0443\u0433 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u043f\u0440\u043e\u0446\u0435\u0441.", "no_flows": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Point, \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 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438\u0442\u0435](https://www.home-assistant.io/components/point/)." diff --git a/homeassistant/components/point/translations/ca.json b/homeassistant/components/point/translations/ca.json index 158c6addade..39269e3740d 100644 --- a/homeassistant/components/point/translations/ca.json +++ b/homeassistant/components/point/translations/ca.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", - "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "external_setup": "Point s'ha configurat correctament des d'un altre flux de dades.", "no_flows": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", diff --git a/homeassistant/components/point/translations/cs.json b/homeassistant/components/point/translations/cs.json index 6dedba8af11..d5e0e7c08b7 100644 --- a/homeassistant/components/point/translations/cs.json +++ b/homeassistant/components/point/translations/cs.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", - "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "external_setup": "Point \u00fasp\u011b\u0161n\u011b nastaveno jin\u00fdm zp\u016fsobem.", "no_flows": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", diff --git a/homeassistant/components/point/translations/da.json b/homeassistant/components/point/translations/da.json index 4530705a8b2..80bdf84dedf 100644 --- a/homeassistant/components/point/translations/da.json +++ b/homeassistant/components/point/translations/da.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Du kan kun konfigurere en enkelt Point konto.", - "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", "authorize_url_timeout": "Timeout ved generering af autoriseret url.", "external_setup": "Point er konfigureret med succes fra et andet flow.", "no_flows": "Du skal konfigurere Point f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/point/)." diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index c36c7a44b51..41a8eb4344f 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", - "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "external_setup": "Pointt erfolgreich von einem anderen Flow konfiguriert.", "no_flows": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", diff --git a/homeassistant/components/point/translations/en.json b/homeassistant/components/point/translations/en.json index 685a16cbbf5..c41dd93683f 100644 --- a/homeassistant/components/point/translations/en.json +++ b/homeassistant/components/point/translations/en.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Already configured. Only a single configuration possible.", - "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": "The component is not configured. Please follow the documentation.", diff --git a/homeassistant/components/point/translations/es-419.json b/homeassistant/components/point/translations/es-419.json index 2b177e26825..c6337dbd8ca 100644 --- a/homeassistant/components/point/translations/es-419.json +++ b/homeassistant/components/point/translations/es-419.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Solo puede configurar una cuenta Point.", - "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", "external_setup": "Punto configurado con \u00e9xito desde otro flujo.", "no_flows": "Debe configurar Point antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/point/)." diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index 51c334e794f..c495a4fe3bd 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "external_setup": "Point se ha configurado correctamente a partir de otro flujo.", "no_flows": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", diff --git a/homeassistant/components/point/translations/et.json b/homeassistant/components/point/translations/et.json index 7317e2cd3e3..7fa9a466e23 100644 --- a/homeassistant/components/point/translations/et.json +++ b/homeassistant/components/point/translations/et.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", - "authorize_url_fail": "Tundmatu viga tuvastamise URL-i loomisel.", "authorize_url_timeout": "Tuvastamise URL'i loomise ajal\u00f5pp.", "external_setup": "Point on teisest voost edukalt seadistatud.", "no_flows": "Osis pole seadistatud. Palun vaata dokumentatsiooni.", diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json index ab9cd7af34e..0d05e0a5363 100644 --- a/homeassistant/components/point/translations/fr.json +++ b/homeassistant/components/point/translations/fr.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "external_setup": "Point correctement configur\u00e9 \u00e0 partir d\u2019un autre flux.", "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", diff --git a/homeassistant/components/point/translations/hu.json b/homeassistant/components/point/translations/hu.json index 7f4346a6ea9..1fc46d0c19b 100644 --- a/homeassistant/components/point/translations/hu.json +++ b/homeassistant/components/point/translations/hu.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "no_flows": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." diff --git a/homeassistant/components/point/translations/id.json b/homeassistant/components/point/translations/id.json index 868321d7469..854a67449a0 100644 --- a/homeassistant/components/point/translations/id.json +++ b/homeassistant/components/point/translations/id.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", - "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", "external_setup": "Point berhasil dikonfigurasi dari alur konfigurasi lainnya.", "no_flows": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", diff --git a/homeassistant/components/point/translations/it.json b/homeassistant/components/point/translations/it.json index 49eb2a760a4..76aae0f4403 100644 --- a/homeassistant/components/point/translations/it.json +++ b/homeassistant/components/point/translations/it.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", - "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "external_setup": "Point configurato correttamente da un altro flusso.", "no_flows": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", diff --git a/homeassistant/components/point/translations/ko.json b/homeassistant/components/point/translations/ko.json index 5813dbba137..c88c32906b8 100644 --- a/homeassistant/components/point/translations/ko.json +++ b/homeassistant/components/point/translations/ko.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "external_setup": "Point\uac00 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "no_flows": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/point/translations/lb.json b/homeassistant/components/point/translations/lb.json index ccdc9db4fff..4fe8d86b796 100644 --- a/homeassistant/components/point/translations/lb.json +++ b/homeassistant/components/point/translations/lb.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech.", - "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", "external_setup": "Point gouf vun engem anere Floss erfollegr\u00e4ich konfigur\u00e9iert.", "no_flows": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." diff --git a/homeassistant/components/point/translations/nl.json b/homeassistant/components/point/translations/nl.json index 59b50f1636b..37dae8481eb 100644 --- a/homeassistant/components/point/translations/nl.json +++ b/homeassistant/components/point/translations/nl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", - "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "external_setup": "Punt succesvol geconfigureerd vanuit een andere stroom.", "no_flows": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json index a72a8083f6f..912c0683401 100644 --- a/homeassistant/components/point/translations/no.json +++ b/homeassistant/components/point/translations/no.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", - "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "external_setup": "Punktet er konfigurert fra en annen flyt.", "no_flows": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", diff --git a/homeassistant/components/point/translations/pl.json b/homeassistant/components/point/translations/pl.json index 66b81d5675e..56c65d52d11 100644 --- a/homeassistant/components/point/translations/pl.json +++ b/homeassistant/components/point/translations/pl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", - "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "external_setup": "Punkt pomy\u015blnie skonfigurowany", "no_flows": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", diff --git a/homeassistant/components/point/translations/pt-BR.json b/homeassistant/components/point/translations/pt-BR.json index 5816f1a0bcd..c9384d82c38 100644 --- a/homeassistant/components/point/translations/pt-BR.json +++ b/homeassistant/components/point/translations/pt-BR.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Voc\u00ea s\u00f3 pode configurar uma conta Point.", - "authorize_url_fail": "Erro desconhecido ao gerar URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Excedido tempo limite gerando a URL de autoriza\u00e7\u00e3o.", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", "no_flows": "Voc\u00ea precisa configurar o Point antes de ser capaz de autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es](https://www.home-assistant.io/components/point/)." diff --git a/homeassistant/components/point/translations/pt.json b/homeassistant/components/point/translations/pt.json index 401e10c256a..3af92508762 100644 --- a/homeassistant/components/point/translations/pt.json +++ b/homeassistant/components/point/translations/pt.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "S\u00f3 pode configurar uma \u00fanica conta Point.", - "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/).", diff --git a/homeassistant/components/point/translations/ru.json b/homeassistant/components/point/translations/ru.json index c29345af04d..1345877d54a 100644 --- a/homeassistant/components/point/translations/ru.json +++ b/homeassistant/components/point/translations/ru.json @@ -2,7 +2,6 @@ "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. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", - "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", diff --git a/homeassistant/components/point/translations/sl.json b/homeassistant/components/point/translations/sl.json index 3c928935cce..009ea51370e 100644 --- a/homeassistant/components/point/translations/sl.json +++ b/homeassistant/components/point/translations/sl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Nastavite lahko samo en ra\u010dun Point.", - "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", "external_setup": "To\u010dka uspe\u0161no konfigurirana iz drugega toka.", "no_flows": "Preden lahko preverite pristnost, morate konfigurirati Point. [Preberite navodila](https://www.home-assistant.io/components/point/).", diff --git a/homeassistant/components/point/translations/sv.json b/homeassistant/components/point/translations/sv.json index f7050c2c255..51627c78f3d 100644 --- a/homeassistant/components/point/translations/sv.json +++ b/homeassistant/components/point/translations/sv.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Du kan endast konfigurera ett Point-konto.", - "authorize_url_fail": "Ok\u00e4nt fel n\u00e4r f\u00f6rs\u00f6ker generera en url f\u00f6r auktorisering.", "authorize_url_timeout": "Timeout n\u00e4r genererar url f\u00f6r auktorisering.", "external_setup": "Point har lyckats med konfigurering ifr\u00e5n ett annat fl\u00f6de.", "no_flows": "Du beh\u00f6ver konfigurera Point innan de kan autentisera med den. [L\u00e4s instruktioner](https://www.home-assistant.io/components/point/)." diff --git a/homeassistant/components/point/translations/uk.json b/homeassistant/components/point/translations/uk.json index 6b66a39a291..798f76e4f6b 100644 --- a/homeassistant/components/point/translations/uk.json +++ b/homeassistant/components/point/translations/uk.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", - "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "external_setup": "Point \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.", "no_flows": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", diff --git a/homeassistant/components/point/translations/zh-Hans.json b/homeassistant/components/point/translations/zh-Hans.json index ecefc3b656c..09f66edf323 100644 --- a/homeassistant/components/point/translations/zh-Hans.json +++ b/homeassistant/components/point/translations/zh-Hans.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "\u60a8\u53ea\u80fd\u914d\u7f6e\u4e00\u4e2a Point \u5e10\u6237\u3002", - "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", "external_setup": "Point\u914d\u7f6e\u6210\u529f\u3002", "no_flows": "\u60a8\u9700\u8981\u5148\u914d\u7f6e Point\uff0c\u7136\u540e\u624d\u80fd\u5bf9\u5176\u8fdb\u884c\u6388\u6743\u3002 [\u8bf7\u9605\u8bfb\u8bf4\u660e](https://www.home-assistant.io/components/point/)\u3002" diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json index 2bb1a8fc239..3f9df05d697 100644 --- a/homeassistant/components/point/translations/zh-Hant.json +++ b/homeassistant/components/point/translations/zh-Hant.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", - "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002", "no_flows": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json index 8016cd12371..c8020069676 100644 --- a/homeassistant/components/powerwall/translations/ca.json +++ b/homeassistant/components/powerwall/translations/ca.json @@ -10,7 +10,7 @@ "unknown": "Error inesperat", "wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d'aquest problema perqu\u00e8 sigui solucionat." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/et.json b/homeassistant/components/powerwall/translations/et.json index 8811b870316..98eb25ca17a 100644 --- a/homeassistant/components/powerwall/translations/et.json +++ b/homeassistant/components/powerwall/translations/et.json @@ -10,7 +10,7 @@ "unknown": "Ootamatu t\u00f5rge", "wrong_version": "Powerwall kasutab tarkvaraversiooni, mida ei toetata. Kaaluge tarkvara uuendamist v\u00f5i probleemist teavitamist, et see saaks lahendatud." }, - "flow_title": "Tesla Powerwall ( {ip_address} )", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/no.json b/homeassistant/components/powerwall/translations/no.json index 00b77e14566..6f45fb144f5 100644 --- a/homeassistant/components/powerwall/translations/no.json +++ b/homeassistant/components/powerwall/translations/no.json @@ -10,7 +10,7 @@ "unknown": "Uventet feil", "wrong_version": "Powerwall bruker en programvareversjon som ikke st\u00f8ttes. Vennligst vurder \u00e5 oppgradere eller rapportere dette problemet, s\u00e5 det kan l\u00f8ses." }, - "flow_title": "", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/ru.json b/homeassistant/components/powerwall/translations/ru.json index f79b62c2c78..f8299a59445 100644 --- a/homeassistant/components/powerwall/translations/ru.json +++ b/homeassistant/components/powerwall/translations/ru.json @@ -10,7 +10,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "wrong_version": "\u0412\u0430\u0448 powerwall \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0433\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435, \u0447\u0442\u043e\u0431\u044b \u0435\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0440\u0435\u0448\u0438\u0442\u044c." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json index 06925ef5a41..21a2a4f2159 100644 --- a/homeassistant/components/powerwall/translations/zh-Hant.json +++ b/homeassistant/components/powerwall/translations/zh-Hant.json @@ -10,7 +10,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4", "wrong_version": "\u4e0d\u652f\u63f4\u60a8\u6240\u4f7f\u7528\u7684 Powerwall \u7248\u672c\u3002\u8acb\u8003\u616e\u9032\u884c\u5347\u7d1a\u6216\u56de\u5831\u6b64\u554f\u984c\u3001\u4ee5\u671f\u554f\u984c\u53ef\u4ee5\u7372\u5f97\u89e3\u6c7a\u3002" }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/ca.json b/homeassistant/components/rainmachine/translations/ca.json index 76c91db7f32..d441654ce08 100644 --- a/homeassistant/components/rainmachine/translations/ca.json +++ b/homeassistant/components/rainmachine/translations/ca.json @@ -6,7 +6,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, - "flow_title": "RainMachine {ip}", + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/et.json b/homeassistant/components/rainmachine/translations/et.json index 81c474e4942..cad1284ad3d 100644 --- a/homeassistant/components/rainmachine/translations/et.json +++ b/homeassistant/components/rainmachine/translations/et.json @@ -6,7 +6,7 @@ "error": { "invalid_auth": "Tuvastamise viga" }, - "flow_title": "RainMachine {ip}", + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/no.json b/homeassistant/components/rainmachine/translations/no.json index 7c106a400e9..7fbeda38374 100644 --- a/homeassistant/components/rainmachine/translations/no.json +++ b/homeassistant/components/rainmachine/translations/no.json @@ -6,7 +6,7 @@ "error": { "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "RainMachine {ip}", + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/ru.json b/homeassistant/components/rainmachine/translations/ru.json index 6e56f9214a2..8dbe804ecab 100644 --- a/homeassistant/components/rainmachine/translations/ru.json +++ b/homeassistant/components/rainmachine/translations/ru.json @@ -6,7 +6,7 @@ "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, - "flow_title": "RainMachine {ip}", + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/zh-Hant.json b/homeassistant/components/rainmachine/translations/zh-Hant.json index 942fd9ebea6..d37ae79541f 100644 --- a/homeassistant/components/rainmachine/translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/translations/zh-Hant.json @@ -6,7 +6,7 @@ "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "RainMachine {ip}", + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/roku/translations/ca.json b/homeassistant/components/roku/translations/ca.json index b60b8f83eb9..be84d78fbff 100644 --- a/homeassistant/components/roku/translations/ca.json +++ b/homeassistant/components/roku/translations/ca.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "Vols configurar {name}?", diff --git a/homeassistant/components/roku/translations/en.json b/homeassistant/components/roku/translations/en.json index 5d43a016afb..192b9b23085 100644 --- a/homeassistant/components/roku/translations/en.json +++ b/homeassistant/components/roku/translations/en.json @@ -11,7 +11,10 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "data": {}, + "description": "Do you want to set up {name}?", + "title": "Roku" + }, + "ssdp_confirm": { "description": "Do you want to set up {name}?", "title": "Roku" }, diff --git a/homeassistant/components/roku/translations/et.json b/homeassistant/components/roku/translations/et.json index 17bce39f5df..bb496ab8716 100644 --- a/homeassistant/components/roku/translations/et.json +++ b/homeassistant/components/roku/translations/et.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "Kas soovid seadistada {name}?", diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json index e7dc663b8f8..1bbd5bbea86 100644 --- a/homeassistant/components/roku/translations/no.json +++ b/homeassistant/components/roku/translations/no.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "Vil du konfigurere {name}?", diff --git a/homeassistant/components/roku/translations/ru.json b/homeassistant/components/roku/translations/ru.json index c3ae135ed76..4ba55bc8e1a 100644 --- a/homeassistant/components/roku/translations/ru.json +++ b/homeassistant/components/roku/translations/ru.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index a0d755d8997..5cfe9232301 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "Roku\uff1a{name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json index 3bdf842df9b..d41b7d3833f 100644 --- a/homeassistant/components/roomba/translations/ca.json +++ b/homeassistant/components/roomba/translations/ca.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index 705706c7fd5..32853564e53 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -36,6 +36,17 @@ }, "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-` or `Roomba-`. Please follow the steps outlined in the documentation at: {auth_help_url}", "title": "Manually connect to the device" + }, + "user": { + "data": { + "blid": "BLID", + "continuous": "Continuous", + "delay": "Delay", + "host": "Host", + "password": "Password" + }, + "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "Connect to the device" } } }, diff --git a/homeassistant/components/roomba/translations/et.json b/homeassistant/components/roomba/translations/et.json index 7943df95b4f..0f992a57de6 100644 --- a/homeassistant/components/roomba/translations/et.json +++ b/homeassistant/components/roomba/translations/et.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "iRobot {name} ( {host} )", + "flow_title": "{name} ( {host} )", "step": { "init": { "data": { diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index 4f051cfde3f..2caba79f50c 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index c47c9ef8fd6..6ff4feb4e9c 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index edbe4ba64a4..81ba19a3a57 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/roon/translations/ca.json b/homeassistant/components/roon/translations/ca.json index ef32dd00e75..f05ac1a4acb 100644 --- a/homeassistant/components/roon/translations/ca.json +++ b/homeassistant/components/roon/translations/ca.json @@ -4,7 +4,6 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "duplicate_entry": "Aquest amfitri\u00f3 ja ha estat afegit.", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/roon/translations/cs.json b/homeassistant/components/roon/translations/cs.json index fd01ed1cd25..20187f9d4d2 100644 --- a/homeassistant/components/roon/translations/cs.json +++ b/homeassistant/components/roon/translations/cs.json @@ -4,7 +4,6 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { - "duplicate_entry": "Tento hostitel ji\u017e byl p\u0159id\u00e1n.", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index 48f8fc456cf..cd4aae46adc 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -4,7 +4,6 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "duplicate_entry": "Dieser Host wurde bereits hinzugef\u00fcgt.", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/roon/translations/el.json b/homeassistant/components/roon/translations/el.json index fd197369cb8..873f82d4f68 100644 --- a/homeassistant/components/roon/translations/el.json +++ b/homeassistant/components/roon/translations/el.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "duplicate_entry": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03b5\u03af." - }, "step": { "link": { "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03c3\u03c4\u03bf Roon. \u0391\u03c6\u03bf\u03cd \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Roon Core, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03b7\u03bd \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1 \u0395\u03c0\u03b5\u03ba\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2.", diff --git a/homeassistant/components/roon/translations/en.json b/homeassistant/components/roon/translations/en.json index b763fbb1e0c..d1f86fbce5f 100644 --- a/homeassistant/components/roon/translations/en.json +++ b/homeassistant/components/roon/translations/en.json @@ -4,7 +4,6 @@ "already_configured": "Device is already configured" }, "error": { - "duplicate_entry": "That host has already been added.", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, diff --git a/homeassistant/components/roon/translations/es.json b/homeassistant/components/roon/translations/es.json index daf3200a0e4..e38b7691f40 100644 --- a/homeassistant/components/roon/translations/es.json +++ b/homeassistant/components/roon/translations/es.json @@ -4,7 +4,6 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "duplicate_entry": "Ese host ya ha sido a\u00f1adido.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/roon/translations/et.json b/homeassistant/components/roon/translations/et.json index e29b1ccc6c6..bbdf2b5edc5 100644 --- a/homeassistant/components/roon/translations/et.json +++ b/homeassistant/components/roon/translations/et.json @@ -4,7 +4,6 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { - "duplicate_entry": "See host on juba lisatud.", "invalid_auth": "Tuvastamine nurjus", "unknown": "Tundmatu viga" }, diff --git a/homeassistant/components/roon/translations/fr.json b/homeassistant/components/roon/translations/fr.json index 7e61b556d39..94e31ba445f 100644 --- a/homeassistant/components/roon/translations/fr.json +++ b/homeassistant/components/roon/translations/fr.json @@ -4,7 +4,6 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "duplicate_entry": "Cet h\u00f4te a d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9.", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index 6ea0aa2b25c..f05fd838572 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -4,7 +4,6 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "duplicate_entry": "Ez a hoszt m\u00e1r konfigur\u00e1lva van.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/roon/translations/id.json b/homeassistant/components/roon/translations/id.json index bfd70955ac8..96b26640320 100644 --- a/homeassistant/components/roon/translations/id.json +++ b/homeassistant/components/roon/translations/id.json @@ -4,7 +4,6 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "duplicate_entry": "Host ini telah ditambahkan.", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/roon/translations/it.json b/homeassistant/components/roon/translations/it.json index e0450af9d39..e21a74aae43 100644 --- a/homeassistant/components/roon/translations/it.json +++ b/homeassistant/components/roon/translations/it.json @@ -4,7 +4,6 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "duplicate_entry": "Questo host \u00e8 gi\u00e0 stato aggiunto.", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/roon/translations/ko.json b/homeassistant/components/roon/translations/ko.json index b1e0fee4089..a55249417af 100644 --- a/homeassistant/components/roon/translations/ko.json +++ b/homeassistant/components/roon/translations/ko.json @@ -4,7 +4,6 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "duplicate_entry": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\ub294 \uc774\ubbf8 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/roon/translations/lb.json b/homeassistant/components/roon/translations/lb.json index a8fef968e81..f5722ee75bb 100644 --- a/homeassistant/components/roon/translations/lb.json +++ b/homeassistant/components/roon/translations/lb.json @@ -4,7 +4,6 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert" }, "error": { - "duplicate_entry": "D\u00ebsen Apparat gouf scho dob\u00e4igesat.", "invalid_auth": "Ong\u00eblteg Authentifikatioun", "unknown": "Onerwaarte Feeler" }, diff --git a/homeassistant/components/roon/translations/nl.json b/homeassistant/components/roon/translations/nl.json index 535c56a2f98..452436d0e05 100644 --- a/homeassistant/components/roon/translations/nl.json +++ b/homeassistant/components/roon/translations/nl.json @@ -4,7 +4,6 @@ "already_configured": "Apparaat is al toegevoegd" }, "error": { - "duplicate_entry": "Die host is al toegevoegd.", "invalid_auth": "Ongeldige authencatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/roon/translations/no.json b/homeassistant/components/roon/translations/no.json index e872e03a69d..2558c2c27c7 100644 --- a/homeassistant/components/roon/translations/no.json +++ b/homeassistant/components/roon/translations/no.json @@ -4,7 +4,6 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { - "duplicate_entry": "Denne verten er allerede lagt til.", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json index d763fc12bd2..7ebfd0ad777 100644 --- a/homeassistant/components/roon/translations/pl.json +++ b/homeassistant/components/roon/translations/pl.json @@ -4,7 +4,6 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "duplicate_entry": "Ten host lub adres IP zosta\u0142 ju\u017c dodany", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, diff --git a/homeassistant/components/roon/translations/pt-BR.json b/homeassistant/components/roon/translations/pt-BR.json index cbcba0352a9..39538d2bf52 100644 --- a/homeassistant/components/roon/translations/pt-BR.json +++ b/homeassistant/components/roon/translations/pt-BR.json @@ -4,7 +4,6 @@ "already_configured": "O dispositivo j\u00e1 foi configurado" }, "error": { - "duplicate_entry": "Esse host j\u00e1 foi adicionado.", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Ocorreu um erro inexperado" }, diff --git a/homeassistant/components/roon/translations/ru.json b/homeassistant/components/roon/translations/ru.json index c01006d6269..1bcb07c7695 100644 --- a/homeassistant/components/roon/translations/ru.json +++ b/homeassistant/components/roon/translations/ru.json @@ -4,7 +4,6 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "duplicate_entry": "\u042d\u0442\u043e\u0442 \u0445\u043e\u0441\u0442 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/roon/translations/tr.json b/homeassistant/components/roon/translations/tr.json index 97241919c9b..94e452e48bb 100644 --- a/homeassistant/components/roon/translations/tr.json +++ b/homeassistant/components/roon/translations/tr.json @@ -4,7 +4,6 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "duplicate_entry": "Bu ana bilgisayar zaten eklendi.", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/roon/translations/uk.json b/homeassistant/components/roon/translations/uk.json index 91a530787ae..a892e24692d 100644 --- a/homeassistant/components/roon/translations/uk.json +++ b/homeassistant/components/roon/translations/uk.json @@ -4,7 +4,6 @@ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." }, "error": { - "duplicate_entry": "\u0426\u0435\u0439 \u0445\u043e\u0441\u0442 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0438\u0439.", "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json index 525270fa90d..f8f52d11b1c 100644 --- a/homeassistant/components/roon/translations/zh-Hant.json +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -4,7 +4,6 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "duplicate_entry": "\u8a72\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u3002", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json index bba625bc815..ee30b508798 100644 --- a/homeassistant/components/samsungtv/translations/ca.json +++ b/homeassistant/components/samsungtv/translations/ca.json @@ -7,7 +7,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "not_supported": "Actualment aquest televisor Samsung no \u00e9s compatible." }, - "flow_title": "Televisor Samsung: {model}", + "flow_title": "{model}", "step": { "confirm": { "description": "Vols configurar el televisior Samsung {model}? Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3. Les configuracions manuals d'aquest televisor es sobreescriuran.", diff --git a/homeassistant/components/samsungtv/translations/et.json b/homeassistant/components/samsungtv/translations/et.json index e1c9bc9dd46..fe236da487d 100644 --- a/homeassistant/components/samsungtv/translations/et.json +++ b/homeassistant/components/samsungtv/translations/et.json @@ -7,7 +7,7 @@ "cannot_connect": "\u00dchendamine nurjus", "not_supported": "Seda Samsungi tv-seadet praegu ei toetata." }, - "flow_title": "Samsungi teler: {model}", + "flow_title": "{model}", "step": { "confirm": { "description": "Kas soovid seadistada Samsung TV-d {model} ? Kui seda pole kunagi enne Home Assistantiga \u00fchendatud, n\u00e4ed oma teleris h\u00fcpikakent, mis k\u00fcsib tuvastamist. Selle teleri k\u00e4sitsi seadistused kirjutatakse \u00fcle.", diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index 5d53619a96a..90c64e02a3e 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -7,7 +7,7 @@ "cannot_connect": "Tilkobling mislyktes", "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." }, - "flow_title": "", + "flow_title": "{model}", "step": { "confirm": { "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", diff --git a/homeassistant/components/samsungtv/translations/ru.json b/homeassistant/components/samsungtv/translations/ru.json index 983e7417d6b..48bb435ae61 100644 --- a/homeassistant/components/samsungtv/translations/ru.json +++ b/homeassistant/components/samsungtv/translations/ru.json @@ -7,7 +7,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "not_supported": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, - "flow_title": "Samsung TV: {model}", + "flow_title": "{model}", "step": { "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung {model}? \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e, \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u044b.", diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index 6dfa1bd4f91..529df4b9ee9 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -7,7 +7,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "not_supported": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u4e09\u661f\u96fb\u8996\u3002" }, - "flow_title": "\u4e09\u661f\u96fb\u8996\uff1a{model}", + "flow_title": "{model}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4e09\u661f\u96fb\u8996 {model}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002\u624b\u52d5\u8a2d\u5b9a\u5c07\u6703\u8986\u84cb\u539f\u8a2d\u5b9a\u3002", diff --git a/homeassistant/components/screenlogic/translations/ca.json b/homeassistant/components/screenlogic/translations/ca.json index 68bfad1ff94..a264ecec51a 100644 --- a/homeassistant/components/screenlogic/translations/ca.json +++ b/homeassistant/components/screenlogic/translations/ca.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/et.json b/homeassistant/components/screenlogic/translations/et.json index cf2cf19418f..3e0047e5be7 100644 --- a/homeassistant/components/screenlogic/translations/et.json +++ b/homeassistant/components/screenlogic/translations/et.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/no.json b/homeassistant/components/screenlogic/translations/no.json index 0ca4827514a..e108572c648 100644 --- a/homeassistant/components/screenlogic/translations/no.json +++ b/homeassistant/components/screenlogic/translations/no.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/ru.json b/homeassistant/components/screenlogic/translations/ru.json index a657b7360c7..957bf155c0b 100644 --- a/homeassistant/components/screenlogic/translations/ru.json +++ b/homeassistant/components/screenlogic/translations/ru.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/zh-Hant.json b/homeassistant/components/screenlogic/translations/zh-Hant.json index d028c77f54b..575392f15c7 100644 --- a/homeassistant/components/screenlogic/translations/zh-Hant.json +++ b/homeassistant/components/screenlogic/translations/zh-Hant.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/sensor/translations/bg.json b/homeassistant/components/sensor/translations/bg.json index 8d0881c6dd5..b657bdb03b6 100644 --- a/homeassistant/components/sensor/translations/bg.json +++ b/homeassistant/components/sensor/translations/bg.json @@ -8,7 +8,6 @@ "is_pressure": "\u0422\u0435\u043a\u0443\u0449\u043e \u043d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 {entity_name}", "is_signal_strength": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0441\u0438\u043b\u0430 \u043d\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0430 \u043d\u0430 {entity_name}", "is_temperature": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 {entity_name}", - "is_timestamp": "\u0422\u0435\u043a\u0443\u0449\u043e \u0432\u0440\u0435\u043c\u0435 \u043d\u0430 {entity_name}", "is_value": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}" }, "trigger_type": { @@ -19,7 +18,6 @@ "pressure": "\u043d\u0430\u043b\u044f\u0433\u0430\u043d\u0435\u0442\u043e \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", "signal_strength": "\u0441\u0438\u043b\u0430\u0442\u0430 \u043d\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", "temperature": "\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430\u0442\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", - "timestamp": "\u0432\u0440\u0435\u043c\u0435\u0442\u043e \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", "value": "\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438" } }, diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index e0cfb5faa50..a30748d5c9c 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -13,7 +13,6 @@ "is_pressure": "Pressi\u00f3 actual de {entity_name}", "is_signal_strength": "Pot\u00e8ncia de senyal actual de {entity_name}", "is_temperature": "Temperatura actual de {entity_name}", - "is_timestamp": "Marca temporal actual de {entity_name}", "is_value": "Valor actual de {entity_name}", "is_voltage": "Voltatge actual de {entity_name}" }, @@ -30,7 +29,6 @@ "pressure": "Canvia la pressi\u00f3 de {entity_name}", "signal_strength": "Canvia la pot\u00e8ncia de senyal de {entity_name}", "temperature": "Canvia la temperatura de {entity_name}", - "timestamp": "Canvia la marca temporal de {entity_name}", "value": "Canvia el valor de {entity_name}", "voltage": "Canvia el voltatge de {entity_name}" } diff --git a/homeassistant/components/sensor/translations/cs.json b/homeassistant/components/sensor/translations/cs.json index ccfb1548f27..dfa2a263783 100644 --- a/homeassistant/components/sensor/translations/cs.json +++ b/homeassistant/components/sensor/translations/cs.json @@ -11,7 +11,6 @@ "is_pressure": "Aktu\u00e1ln\u00ed tlak {entity_name}", "is_signal_strength": "Aktu\u00e1ln\u00ed s\u00edla sign\u00e1lu {entity_name}", "is_temperature": "Aktu\u00e1ln\u00ed teplota {entity_name}", - "is_timestamp": "Aktu\u00e1ln\u00ed \u010dasov\u00e9 raz\u00edtko {entity_name}", "is_value": "Aktu\u00e1ln\u00ed hodnota {entity_name}", "is_voltage": "Aktu\u00e1ln\u00ed nap\u011bt\u00ed {entity_name}" }, @@ -26,7 +25,6 @@ "pressure": "P\u0159i zm\u011bn\u011b tlaku {entity_name}", "signal_strength": "P\u0159i zm\u011bn\u011b s\u00edly sign\u00e1lu {entity_name}", "temperature": "P\u0159i zm\u011bn\u011b teploty {entity_name}", - "timestamp": "P\u0159i zm\u011bn\u011b \u010dasov\u00e9ho raz\u00edtka {entity_name}", "value": "P\u0159i zm\u011bn\u011b hodnoty {entity_name}", "voltage": "P\u0159i zm\u011bn\u011b nap\u011bt\u00ed {entity_name}" } diff --git a/homeassistant/components/sensor/translations/da.json b/homeassistant/components/sensor/translations/da.json index 25e6597023a..53e2c9b98fc 100644 --- a/homeassistant/components/sensor/translations/da.json +++ b/homeassistant/components/sensor/translations/da.json @@ -8,7 +8,6 @@ "is_pressure": "Aktuelt {entity_name}-lufttryk", "is_signal_strength": "Aktuel {entity_name}-signalstyrke", "is_temperature": "Aktuel {entity_name}-temperatur", - "is_timestamp": "Aktuel {entity_name}-tidsstempel", "is_value": "Aktuel {entity_name}-v\u00e6rdi" }, "trigger_type": { @@ -19,7 +18,6 @@ "pressure": "{entity_name} lufttryk \u00e6ndres", "signal_strength": "{entity_name} signalstyrke \u00e6ndres", "temperature": "{entity_name} temperatur \u00e6ndres", - "timestamp": "{entity_name} tidsstempel \u00e6ndres", "value": "{entity_name} v\u00e6rdi \u00e6ndres" } }, diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index 24c87bc15b9..4f16c07be01 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -13,7 +13,6 @@ "is_pressure": "{entity_name} Druck", "is_signal_strength": "Aktuelle {entity_name} Signalst\u00e4rke", "is_temperature": "Aktuelle {entity_name} Temperatur", - "is_timestamp": "Aktueller Zeitstempel von {entity_name}", "is_value": "Aktueller {entity_name} Wert", "is_voltage": "Aktuelle Spannung von {entity_name}" }, @@ -30,7 +29,6 @@ "pressure": "{entity_name} Druck\u00e4nderungen", "signal_strength": "{entity_name} Signalst\u00e4rke\u00e4nderungen", "temperature": "{entity_name} Temperatur\u00e4nderungen", - "timestamp": "{entity_name} Zeitstempel\u00e4nderungen", "value": "{entity_name} Wert\u00e4nderungen", "voltage": "{entity_name} Spannungs\u00e4nderungen" } diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index e32ae845c1c..f8f45f93309 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -13,7 +13,6 @@ "is_pressure": "Current {entity_name} pressure", "is_signal_strength": "Current {entity_name} signal strength", "is_temperature": "Current {entity_name} temperature", - "is_timestamp": "Current {entity_name} timestamp", "is_value": "Current {entity_name} value", "is_voltage": "Current {entity_name} voltage" }, @@ -30,7 +29,6 @@ "pressure": "{entity_name} pressure changes", "signal_strength": "{entity_name} signal strength changes", "temperature": "{entity_name} temperature changes", - "timestamp": "{entity_name} timestamp changes", "value": "{entity_name} value changes", "voltage": "{entity_name} voltage changes" } diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index b810a3f0eb1..da96c7d92db 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -13,7 +13,6 @@ "is_pressure": "Presi\u00f3n actual de {entity_name}", "is_signal_strength": "Intensidad de la se\u00f1al actual de {entity_name}", "is_temperature": "Temperatura actual de {entity_name}", - "is_timestamp": "Marca de tiempo actual de {entity_name}", "is_value": "Valor actual de {entity_name}", "is_voltage": "Voltaje actual de {entity_name}" }, @@ -30,7 +29,6 @@ "pressure": "Cambios de presi\u00f3n de {entity_name}", "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name}", "temperature": "{entity_name} cambios de temperatura", - "timestamp": "{entity_name} cambios de fecha y hora", "value": "Cambios de valor de la {entity_name}", "voltage": "Cambio de voltaje en {entity_name}" } diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 55b1fa48a8f..4169e7b82db 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -13,7 +13,6 @@ "is_pressure": "Praegune {entity_name} r\u00f5hk", "is_signal_strength": "Praegune {entity_name} signaali tugevus", "is_temperature": "Praegune {entity_name} temperatuur", - "is_timestamp": "Praegune {entity_name} aeg", "is_value": "Praegune {entity_name} v\u00e4\u00e4rtus", "is_voltage": "Praegune {entity_name}pinge" }, @@ -30,7 +29,6 @@ "pressure": "{entity_name} r\u00f5hk muutub", "signal_strength": "{entity_name} signaalitugevus muutub", "temperature": "{entity_name} temperatuur muutub", - "timestamp": "{entity_name} aeg muutub", "value": "{entity_name} v\u00e4\u00e4rtus muutub", "voltage": "{entity_name} pingemuutub" } diff --git a/homeassistant/components/sensor/translations/fr.json b/homeassistant/components/sensor/translations/fr.json index ef88b7acecc..1aeee79ddeb 100644 --- a/homeassistant/components/sensor/translations/fr.json +++ b/homeassistant/components/sensor/translations/fr.json @@ -13,7 +13,6 @@ "is_pressure": "Pression de {entity_name}", "is_signal_strength": "Force du signal de {entity_name}", "is_temperature": "Temp\u00e9rature de {entity_name}", - "is_timestamp": "Horodatage de {entity_name}", "is_value": "La valeur actuelle de {entity_name}", "is_voltage": "Tension actuelle pour {entity_name}" }, @@ -30,7 +29,6 @@ "pressure": "{entity_name} modification de la pression", "signal_strength": "{entity_name} modification de la force du signal", "temperature": "{entity_name} modification de temp\u00e9rature", - "timestamp": "{entity_name} modification d'horodatage", "value": "Changements de valeur de {entity_name}", "voltage": "{entity_name} changement de tension" } diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index 98e817fc164..beb1c56b305 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -10,7 +10,6 @@ "is_pressure": "{entity_name} aktu\u00e1lis nyom\u00e1sa", "is_signal_strength": "{entity_name} aktu\u00e1lis jeler\u0151ss\u00e9ge", "is_temperature": "{entity_name} aktu\u00e1lis h\u0151m\u00e9rs\u00e9klete", - "is_timestamp": "{entity_name} aktu\u00e1lis id\u0151b\u00e9lyege", "is_value": "{entity_name} aktu\u00e1lis \u00e9rt\u00e9ke" }, "trigger_type": { @@ -23,7 +22,6 @@ "pressure": "{entity_name} nyom\u00e1sa v\u00e1ltozik", "signal_strength": "{entity_name} jeler\u0151ss\u00e9ge v\u00e1ltozik", "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klete v\u00e1ltozik", - "timestamp": "{entity_name} id\u0151b\u00e9lyege v\u00e1ltozik", "value": "{entity_name} \u00e9rt\u00e9ke v\u00e1ltozik", "voltage": "{entity_name} fesz\u00fclts\u00e9ge v\u00e1ltozik" } diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index d43c2304428..9af162d1357 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -13,7 +13,6 @@ "is_pressure": "Tekanan {entity_name} saat ini", "is_signal_strength": "Kekuatan sinyal {entity_name} saat ini", "is_temperature": "Suhu {entity_name} saat ini", - "is_timestamp": "Stempel waktu {entity_name} saat ini", "is_value": "Nilai {entity_name} saat ini", "is_voltage": "Tegangan {entity_name} saat ini" }, @@ -30,7 +29,6 @@ "pressure": "Perubahan tekanan {entity_name}", "signal_strength": "Perubahan kekuatan sinyal {entity_name}", "temperature": "Perubahan suhu {entity_name}", - "timestamp": "Perubahan stempel waktu {entity_name}", "value": "Perubahan nilai {entity_name}", "voltage": "Perubahan tegangan {entity_name}" } diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index c8b4a2f9b9c..6ae19c201d7 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -13,7 +13,6 @@ "is_pressure": "Pressione attuale di {entity_name}", "is_signal_strength": "Potenza del segnale attuale di {entity_name}", "is_temperature": "Temperatura attuale di {entity_name}", - "is_timestamp": "Data e ora attuali di {entity_name}", "is_value": "Valore attuale di {entity_name}", "is_voltage": "Tensione attuale di {entity_name}" }, @@ -30,7 +29,6 @@ "pressure": "variazioni della pressione di {entity_name}", "signal_strength": "variazioni della potenza del segnale di {entity_name}", "temperature": "variazioni di temperatura di {entity_name}", - "timestamp": "variazioni di data e ora di {entity_name}", "value": "{entity_name} valori cambiati", "voltage": "variazioni di tensione di {entity_name}" } diff --git a/homeassistant/components/sensor/translations/ko.json b/homeassistant/components/sensor/translations/ko.json index d8e99874822..69ff6adb5e7 100644 --- a/homeassistant/components/sensor/translations/ko.json +++ b/homeassistant/components/sensor/translations/ko.json @@ -13,7 +13,6 @@ "is_pressure": "\ud604\uc7ac {entity_name}\uc758 \uc555\ub825\uc774 ~ \uc774\uba74", "is_signal_strength": "\ud604\uc7ac {entity_name}\uc758 \uc2e0\ud638 \uac15\ub3c4\uac00 ~ \uc774\uba74", "is_temperature": "\ud604\uc7ac {entity_name}\uc758 \uc628\ub3c4\uac00 ~ \uc774\uba74", - "is_timestamp": "\ud604\uc7ac {entity_name}\uc758 \uc2dc\uac01\uc774 ~ \uc774\uba74", "is_value": "\ud604\uc7ac {entity_name}\uc758 \uac12\uc774 ~ \uc774\uba74", "is_voltage": "\ud604\uc7ac {entity_name}\uc758 \uc804\uc555\uc774 ~ \uc774\uba74" }, @@ -30,7 +29,6 @@ "pressure": "{entity_name}\uc758 \uc555\ub825\uc774 \ubcc0\ud560 \ub54c", "signal_strength": "{entity_name}\uc758 \uc2e0\ud638 \uac15\ub3c4\uac00 \ubcc0\ud560 \ub54c", "temperature": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ubcc0\ud560 \ub54c", - "timestamp": "{entity_name}\uc758 \uc2dc\uac01\uc774 \ubcc0\ud560 \ub54c", "value": "{entity_name}\uc758 \uac12\uc774 \ubcc0\ud560 \ub54c", "voltage": "{entity_name}\uc758 \uc804\uc555\uc774 \ubcc0\ud560 \ub54c" } diff --git a/homeassistant/components/sensor/translations/lb.json b/homeassistant/components/sensor/translations/lb.json index 97c0e2f0a6b..77e8707f1ef 100644 --- a/homeassistant/components/sensor/translations/lb.json +++ b/homeassistant/components/sensor/translations/lb.json @@ -11,7 +11,6 @@ "is_pressure": "Aktuell {entity_name} Drock", "is_signal_strength": "Aktuell {entity_name} Signal St\u00e4erkt", "is_temperature": "Aktuell {entity_name} Temperatur", - "is_timestamp": "Aktuelle {entity_name} Z\u00e4itstempel", "is_value": "Aktuelle {entity_name} W\u00e4ert", "is_voltage": "Aktuell {entity_name} Spannung" }, @@ -26,7 +25,6 @@ "pressure": "{entity_name} Drock \u00e4nnert", "signal_strength": "{entity_name} Signal St\u00e4erkt \u00e4nnert", "temperature": "{entity_name} Temperatur \u00e4nnert", - "timestamp": "{entity_name} Z\u00e4itstempel \u00e4nnert", "value": "{entity_name} W\u00e4ert \u00e4nnert", "voltage": "{entity_name} Spannung \u00e4nnert" } diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index 411ebf3cefd..745e097c6ee 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -13,7 +13,6 @@ "is_pressure": "Huidige {entity_name} druk", "is_signal_strength": "Huidige {entity_name} signaalsterkte", "is_temperature": "Huidige {entity_name} temperatuur", - "is_timestamp": "Huidige {entity_name} tijdstip", "is_value": "Huidige {entity_name} waarde", "is_voltage": "Huidige {entity_name} spanning" }, @@ -30,7 +29,6 @@ "pressure": "{entity_name} druk gewijzigd", "signal_strength": "{entity_name} signaalsterkte gewijzigd", "temperature": "{entity_name} temperatuur gewijzigd", - "timestamp": "{entity_name} tijdstip gewijzigd", "value": "{entity_name} waarde gewijzigd", "voltage": "{entity_name} voltage verandert" } diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index 3662356d15e..02204a4a49a 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -13,7 +13,6 @@ "is_pressure": "Gjeldende {entity_name} trykk", "is_signal_strength": "Gjeldende {entity_name} signalstyrke", "is_temperature": "Gjeldende {entity_name} temperatur", - "is_timestamp": "Gjeldende {entity_name} tidsstempel", "is_value": "Gjeldende {entity_name} verdi", "is_voltage": "Gjeldende {entity_name} spenning" }, @@ -30,7 +29,6 @@ "pressure": "{entity_name} trykk endringer", "signal_strength": "{entity_name} signalstyrkeendringer", "temperature": "{entity_name} temperaturendringer", - "timestamp": "{entity_name} tidsstempel endringer", "value": "{entity_name} verdi endringer", "voltage": "{entity_name} spenningsendringer" } diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index a2baa380174..fe47bfd902c 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -13,7 +13,6 @@ "is_pressure": "obecne ci\u015bnienie {entity_name}", "is_signal_strength": "obecna si\u0142a sygna\u0142u {entity_name}", "is_temperature": "obecna temperatura {entity_name}", - "is_timestamp": "obecny znacznik czasu {entity_name}", "is_value": "obecna warto\u015b\u0107 {entity_name}", "is_voltage": "obecne napi\u0119cie {entity_name}" }, @@ -30,7 +29,6 @@ "pressure": "zmieni si\u0119 ci\u015bnienie {entity_name}", "signal_strength": "zmieni si\u0119 si\u0142a sygna\u0142u {entity_name}", "temperature": "zmieni si\u0119 temperatura {entity_name}", - "timestamp": "zmieni si\u0119 znacznik czasu {entity_name}", "value": "zmieni si\u0119 warto\u015b\u0107 {entity_name}", "voltage": "zmieni si\u0119 napi\u0119cie w {entity_name}" } diff --git a/homeassistant/components/sensor/translations/pt.json b/homeassistant/components/sensor/translations/pt.json index 94a516b8aef..eaef8ef5755 100644 --- a/homeassistant/components/sensor/translations/pt.json +++ b/homeassistant/components/sensor/translations/pt.json @@ -8,7 +8,6 @@ "is_pressure": "Press\u00e3o atual de {entity_name}", "is_signal_strength": "Intensidade atual do sinal de {entity_name}", "is_temperature": "Temperatura atual de {entity_name}", - "is_timestamp": "momento temporal de {entity_name}", "is_value": "valor {entity_name}" }, "trigger_type": { @@ -19,7 +18,6 @@ "pressure": "press\u00e3o {entity_name}", "signal_strength": "for\u00e7a do sinal de {entity_name}", "temperature": "temperatura de {entity_name}", - "timestamp": "momento temporal de {entity_name}", "value": "valor {entity_name}" } }, diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index ae0a0997dd6..0e823c8d94c 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -13,7 +13,6 @@ "is_pressure": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_signal_strength": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_temperature": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", - "is_timestamp": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_value": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_voltage": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, @@ -30,7 +29,6 @@ "pressure": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "signal_strength": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "temperature": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", - "timestamp": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "value": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "voltage": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" } diff --git a/homeassistant/components/sensor/translations/sl.json b/homeassistant/components/sensor/translations/sl.json index 0a8a3454584..a83b273946c 100644 --- a/homeassistant/components/sensor/translations/sl.json +++ b/homeassistant/components/sensor/translations/sl.json @@ -8,7 +8,6 @@ "is_pressure": "Trenutni tlak {entity_name}", "is_signal_strength": "Trenutna jakost signala {entity_name}", "is_temperature": "Trenutna temperatura {entity_name}", - "is_timestamp": "Trenutni {entity_name} \u010dasovni \u017eig", "is_value": "Trenutna vrednost {entity_name}" }, "trigger_type": { @@ -19,7 +18,6 @@ "pressure": "{entity_name} spremembe tlaka", "signal_strength": "{entity_name} spremembe mo\u010di signala", "temperature": "{entity_name} spremembe temperatur", - "timestamp": "{entity_name} spremembe \u010dasovnega \u017eiga", "value": "{entity_name} spremembe vrednosti" } }, diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index 855034e690a..720fac84f59 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -8,7 +8,6 @@ "is_pressure": "Aktuellt {entity_name} tryck", "is_signal_strength": "Aktuell {entity_name} signalstyrka", "is_temperature": "Aktuell {entity_name} temperatur", - "is_timestamp": "Aktuell {entity_name} tidsst\u00e4mpel", "is_value": "Aktuellt {entity_name} v\u00e4rde" }, "trigger_type": { @@ -19,7 +18,6 @@ "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar", "signal_strength": "{entity_name} signalstyrka \u00e4ndras", "temperature": "{entity_name} temperaturf\u00f6r\u00e4ndringar", - "timestamp": "{entity_name} tidst\u00e4mpel \u00e4ndras", "value": "{entity_name} v\u00e4rde \u00e4ndras" } }, diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index feca40991ee..bfdd538306f 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -6,7 +6,6 @@ "is_power_factor": "Mevcut {entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc", "is_signal_strength": "Mevcut {entity_name} sinyal g\u00fcc\u00fc", "is_temperature": "Mevcut {entity_name} s\u0131cakl\u0131\u011f\u0131", - "is_timestamp": "Mevcut {entity_name} zaman damgas\u0131", "is_value": "Mevcut {entity_name} de\u011feri", "is_voltage": "Mevcut {entity_name} voltaj\u0131" }, @@ -21,7 +20,6 @@ "pressure": "{entity_name} bas\u0131n\u00e7 de\u011fi\u015fiklikleri", "signal_strength": "{entity_name} sinyal g\u00fcc\u00fc de\u011fi\u015fiklikleri", "temperature": "{entity_name} s\u0131cakl\u0131k de\u011fi\u015fiklikleri", - "timestamp": "{entity_name} zaman damgas\u0131 de\u011fi\u015fiklikleri", "value": "{entity_name} de\u011fer de\u011fi\u015fiklikleri", "voltage": "{entity_name} voltaj de\u011fi\u015fiklikleri" } diff --git a/homeassistant/components/sensor/translations/uk.json b/homeassistant/components/sensor/translations/uk.json index 9e6148c3b8c..c2ca555a309 100644 --- a/homeassistant/components/sensor/translations/uk.json +++ b/homeassistant/components/sensor/translations/uk.json @@ -11,7 +11,6 @@ "is_pressure": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", "is_signal_strength": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", "is_temperature": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", - "is_timestamp": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", "is_value": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", "is_voltage": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430\u043f\u0440\u0443\u0433\u0438" }, @@ -26,7 +25,6 @@ "pressure": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", "signal_strength": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", "temperature": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", - "timestamp": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", "value": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", "voltage": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430\u043f\u0440\u0443\u0433\u0438" } diff --git a/homeassistant/components/sensor/translations/zh-Hans.json b/homeassistant/components/sensor/translations/zh-Hans.json index 33a375c000a..2edde916c95 100644 --- a/homeassistant/components/sensor/translations/zh-Hans.json +++ b/homeassistant/components/sensor/translations/zh-Hans.json @@ -11,7 +11,6 @@ "is_pressure": "{entity_name} \u5f53\u524d\u7684\u538b\u529b", "is_signal_strength": "{entity_name} \u5f53\u524d\u7684\u4fe1\u53f7\u5f3a\u5ea6", "is_temperature": "{entity_name} \u5f53\u524d\u7684\u6e29\u5ea6", - "is_timestamp": "{entity_name} \u5f53\u524d\u7684\u65f6\u95f4\u6233", "is_value": "{entity_name} \u5f53\u524d\u7684\u503c", "is_voltage": "{entity_name} \u5f53\u524d\u7684\u7535\u538b" }, @@ -26,7 +25,6 @@ "pressure": "{entity_name} \u7684\u538b\u529b\u53d8\u5316", "signal_strength": "{entity_name} \u7684\u4fe1\u53f7\u5f3a\u5ea6\u53d8\u5316", "temperature": "{entity_name} \u7684\u6e29\u5ea6\u53d8\u5316", - "timestamp": "{entity_name} \u7684\u65f6\u95f4\u6233\u53d8\u5316", "value": "{entity_name} \u7684\u503c\u53d8\u5316", "voltage": "{entity_name} \u7684\u7535\u538b\u53d8\u5316" } diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index ff84d8b2790..a15af383da6 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -13,7 +13,6 @@ "is_pressure": "\u76ee\u524d{entity_name}\u58d3\u529b", "is_signal_strength": "\u76ee\u524d{entity_name}\u8a0a\u865f\u5f37\u5ea6", "is_temperature": "\u76ee\u524d{entity_name}\u6eab\u5ea6", - "is_timestamp": "\u76ee\u524d{entity_name}\u6642\u9593\u6a19\u8a18", "is_value": "\u76ee\u524d{entity_name}\u503c", "is_voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3" }, @@ -30,7 +29,6 @@ "pressure": "{entity_name}\u58d3\u529b\u8b8a\u66f4", "signal_strength": "{entity_name}\u8a0a\u865f\u5f37\u5ea6\u8b8a\u66f4", "temperature": "{entity_name}\u6eab\u5ea6\u8b8a\u66f4", - "timestamp": "{entity_name}\u6642\u9593\u6a19\u8a18\u8b8a\u66f4", "value": "{entity_name}\u503c\u8b8a\u66f4", "voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3\u8b8a\u66f4" } diff --git a/homeassistant/components/sma/translations/de.json b/homeassistant/components/sma/translations/de.json index bf1d325a8b4..c17271cae20 100644 --- a/homeassistant/components/sma/translations/de.json +++ b/homeassistant/components/sma/translations/de.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "cannot_retrieve_device_info": "Erfolgreich verbunden, aber Ger\u00e4teinformationen k\u00f6nnen nicht abgerufen werden", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, @@ -18,7 +19,7 @@ "ssl": "Verwendet ein SSL-Zertifikat", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, - "description": "Geben Sie Ihre SMA-Ger\u00e4teinformationen ein.", + "description": "Gib deine SMA-Ger\u00e4teinformationen ein.", "title": "Richten Sie SMA Solar ein" } } diff --git a/homeassistant/components/smappee/translations/ca.json b/homeassistant/components/smappee/translations/ca.json index 92203b9b37e..a630c81e0a7 100644 --- a/homeassistant/components/smappee/translations/ca.json +++ b/homeassistant/components/smappee/translations/ca.json @@ -9,7 +9,7 @@ "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/smappee/translations/et.json b/homeassistant/components/smappee/translations/et.json index 37a10c69ec6..e34d3017a51 100644 --- a/homeassistant/components/smappee/translations/et.json +++ b/homeassistant/components/smappee/translations/et.json @@ -9,7 +9,7 @@ "missing_configuration": "Osis pole seadistatud. Palun vaata dokumentatsiooni.", "no_url_available": "URL pole saadaval. Rohkem teavet [check the help section]({docs_url})" }, - "flow_title": "", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/smappee/translations/no.json b/homeassistant/components/smappee/translations/no.json index f1307e2a169..3aaa61b8efd 100644 --- a/homeassistant/components/smappee/translations/no.json +++ b/homeassistant/components/smappee/translations/no.json @@ -9,7 +9,7 @@ "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})" }, - "flow_title": "", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/smappee/translations/ru.json b/homeassistant/components/smappee/translations/ru.json index 3654f536a41..c6191daab0d 100644 --- a/homeassistant/components/smappee/translations/ru.json +++ b/homeassistant/components/smappee/translations/ru.json @@ -9,7 +9,7 @@ "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json index b867b1888c5..530dddbb7a4 100644 --- a/homeassistant/components/smappee/translations/zh-Hant.json +++ b/homeassistant/components/smappee/translations/zh-Hant.json @@ -9,7 +9,7 @@ "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" }, - "flow_title": "Smappee\uff1a{name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/smarttub/translations/bg.json b/homeassistant/components/smarttub/translations/bg.json index 05ef3ed780e..cef3726d759 100644 --- a/homeassistant/components/smarttub/translations/bg.json +++ b/homeassistant/components/smarttub/translations/bg.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/smarttub/translations/ca.json b/homeassistant/components/smarttub/translations/ca.json index 16ad53b1ff6..d00c4d26c98 100644 --- a/homeassistant/components/smarttub/translations/ca.json +++ b/homeassistant/components/smarttub/translations/ca.json @@ -5,8 +5,7 @@ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/cs.json b/homeassistant/components/smarttub/translations/cs.json index 383b69460e1..1f5d69dcd88 100644 --- a/homeassistant/components/smarttub/translations/cs.json +++ b/homeassistant/components/smarttub/translations/cs.json @@ -5,8 +5,7 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/de.json b/homeassistant/components/smarttub/translations/de.json index 15e4057a888..0e399b9d018 100644 --- a/homeassistant/components/smarttub/translations/de.json +++ b/homeassistant/components/smarttub/translations/de.json @@ -5,12 +5,11 @@ "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "reauth_confirm": { - "description": "Die SmartTub-Integration muss Ihr Konto neu authentifizieren", + "description": "Die SmartTub-Integration muss dein Konto neu authentifizieren", "title": "Integration erneut authentifizieren" }, "user": { diff --git a/homeassistant/components/smarttub/translations/en.json b/homeassistant/components/smarttub/translations/en.json index d7e1476ed33..752faa76b95 100644 --- a/homeassistant/components/smarttub/translations/en.json +++ b/homeassistant/components/smarttub/translations/en.json @@ -5,8 +5,7 @@ "reauth_successful": "Re-authentication was successful" }, "error": { - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "invalid_auth": "Invalid authentication" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/es.json b/homeassistant/components/smarttub/translations/es.json index 3454bf59837..20a57210448 100644 --- a/homeassistant/components/smarttub/translations/es.json +++ b/homeassistant/components/smarttub/translations/es.json @@ -5,8 +5,7 @@ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "unknown": "Error inesperado" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/et.json b/homeassistant/components/smarttub/translations/et.json index 2d39fee07b6..9b49f6b36dc 100644 --- a/homeassistant/components/smarttub/translations/et.json +++ b/homeassistant/components/smarttub/translations/et.json @@ -5,8 +5,7 @@ "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { - "invalid_auth": "Vigane autentimine", - "unknown": "Ootamatu t\u00f5rge" + "invalid_auth": "Vigane autentimine" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/fr.json b/homeassistant/components/smarttub/translations/fr.json index f51d34a0958..c6f3fdb8ce3 100644 --- a/homeassistant/components/smarttub/translations/fr.json +++ b/homeassistant/components/smarttub/translations/fr.json @@ -5,8 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a \u00e9t\u00e9 un succ\u00e8s" }, "error": { - "invalid_auth": "Authentification invalide", - "unknown": "Erreur inattendue" + "invalid_auth": "Authentification invalide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/hu.json b/homeassistant/components/smarttub/translations/hu.json index 666ff85e321..a2a4a9d706e 100644 --- a/homeassistant/components/smarttub/translations/hu.json +++ b/homeassistant/components/smarttub/translations/hu.json @@ -5,8 +5,7 @@ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "user": { diff --git a/homeassistant/components/smarttub/translations/id.json b/homeassistant/components/smarttub/translations/id.json index c1de3aa0453..672310e0d2f 100644 --- a/homeassistant/components/smarttub/translations/id.json +++ b/homeassistant/components/smarttub/translations/id.json @@ -5,8 +5,7 @@ "reauth_successful": "Autentikasi ulang berhasil" }, "error": { - "invalid_auth": "Autentikasi tidak valid", - "unknown": "Kesalahan yang tidak diharapkan" + "invalid_auth": "Autentikasi tidak valid" }, "step": { "user": { diff --git a/homeassistant/components/smarttub/translations/it.json b/homeassistant/components/smarttub/translations/it.json index bbc778a7af2..6199cb35bea 100644 --- a/homeassistant/components/smarttub/translations/it.json +++ b/homeassistant/components/smarttub/translations/it.json @@ -5,8 +5,7 @@ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { - "invalid_auth": "Autenticazione non valida", - "unknown": "Errore imprevisto" + "invalid_auth": "Autenticazione non valida" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/ko.json b/homeassistant/components/smarttub/translations/ko.json index 2ab844cd967..b68ff871d4d 100644 --- a/homeassistant/components/smarttub/translations/ko.json +++ b/homeassistant/components/smarttub/translations/ko.json @@ -5,8 +5,7 @@ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/smarttub/translations/nl.json b/homeassistant/components/smarttub/translations/nl.json index d434c22b398..6d1e605d315 100644 --- a/homeassistant/components/smarttub/translations/nl.json +++ b/homeassistant/components/smarttub/translations/nl.json @@ -5,8 +5,7 @@ "reauth_successful": "Herauthenticatie was succesvol" }, "error": { - "invalid_auth": "Ongeldige authenticatie", - "unknown": "Onverwachte fout" + "invalid_auth": "Ongeldige authenticatie" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/no.json b/homeassistant/components/smarttub/translations/no.json index 9fd441b57a6..10b3ccc421d 100644 --- a/homeassistant/components/smarttub/translations/no.json +++ b/homeassistant/components/smarttub/translations/no.json @@ -5,8 +5,7 @@ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { - "invalid_auth": "Ugyldig godkjenning", - "unknown": "Uventet feil" + "invalid_auth": "Ugyldig godkjenning" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/pl.json b/homeassistant/components/smarttub/translations/pl.json index f17feed06b5..b5b56c385d4 100644 --- a/homeassistant/components/smarttub/translations/pl.json +++ b/homeassistant/components/smarttub/translations/pl.json @@ -5,8 +5,7 @@ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/pt.json b/homeassistant/components/smarttub/translations/pt.json index 414ca7ddf82..5d4e3e1faed 100644 --- a/homeassistant/components/smarttub/translations/pt.json +++ b/homeassistant/components/smarttub/translations/pt.json @@ -5,8 +5,7 @@ "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { diff --git a/homeassistant/components/smarttub/translations/ru.json b/homeassistant/components/smarttub/translations/ru.json index 9a138fc0439..8f204f96e03 100644 --- a/homeassistant/components/smarttub/translations/ru.json +++ b/homeassistant/components/smarttub/translations/ru.json @@ -5,8 +5,7 @@ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/zh-Hant.json b/homeassistant/components/smarttub/translations/zh-Hant.json index ab0b75bf1c8..05f4e6f26a9 100644 --- a/homeassistant/components/smarttub/translations/zh-Hant.json +++ b/homeassistant/components/smarttub/translations/zh-Hant.json @@ -5,8 +5,7 @@ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/solaredge/translations/bg.json b/homeassistant/components/solaredge/translations/bg.json index 72f1ad2a4c7..6871eb6210e 100644 --- a/homeassistant/components/solaredge/translations/bg.json +++ b/homeassistant/components/solaredge/translations/bg.json @@ -1,11 +1,5 @@ { "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": { diff --git a/homeassistant/components/solaredge/translations/ca.json b/homeassistant/components/solaredge/translations/ca.json index 6705e6d19e8..57dfe4e8e98 100644 --- a/homeassistant/components/solaredge/translations/ca.json +++ b/homeassistant/components/solaredge/translations/ca.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat", - "site_exists": "Aquest site_id ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", "could_not_connect": "No s'ha pogut connectar amb l'API de Solaredge", "invalid_api_key": "Clau API inv\u00e0lida", - "site_exists": "Aquest site_id ja est\u00e0 configurat", "site_not_active": "El lloc web no est\u00e0 actiu" }, "step": { diff --git a/homeassistant/components/solaredge/translations/cs.json b/homeassistant/components/solaredge/translations/cs.json index e985ed4f221..1b6fba895a6 100644 --- a/homeassistant/components/solaredge/translations/cs.json +++ b/homeassistant/components/solaredge/translations/cs.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "site_exists": "Tento identifik\u00e1tor je ji\u017e nastaven" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "could_not_connect": "Nelze se p\u0159ipojit k API SolarEdge", "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", - "site_exists": "Tento identifik\u00e1tor je ji\u017e nastaven", "site_not_active": "Str\u00e1nka nen\u00ed aktivn\u00ed" }, "step": { diff --git a/homeassistant/components/solaredge/translations/da.json b/homeassistant/components/solaredge/translations/da.json index ed452d2c9a8..72c46af2b55 100644 --- a/homeassistant/components/solaredge/translations/da.json +++ b/homeassistant/components/solaredge/translations/da.json @@ -1,11 +1,5 @@ { "config": { - "abort": { - "site_exists": "Dette site_id er allerede konfigureret" - }, - "error": { - "site_exists": "Dette site_id er allerede konfigureret" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/solaredge/translations/de.json b/homeassistant/components/solaredge/translations/de.json index ec9b5681e76..20fc557e5c8 100644 --- a/homeassistant/components/solaredge/translations/de.json +++ b/homeassistant/components/solaredge/translations/de.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Das Ger\u00e4t ist bereits konfiguriert", - "site_exists": "Diese site_id ist bereits konfiguriert" + "already_configured": "Das Ger\u00e4t ist bereits konfiguriert" }, "error": { "already_configured": "Das Ger\u00e4t ist bereits konfiguriert", "could_not_connect": "Es konnte keine Verbindung zur Solaredge-API hergestellt werden", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", - "site_exists": "Diese site_id ist bereits konfiguriert", "site_not_active": "Die Seite ist nicht aktiv" }, "step": { diff --git a/homeassistant/components/solaredge/translations/en.json b/homeassistant/components/solaredge/translations/en.json index a9abfd5e013..86261d0b00e 100644 --- a/homeassistant/components/solaredge/translations/en.json +++ b/homeassistant/components/solaredge/translations/en.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Device is already configured", - "site_exists": "This site_id is already configured" + "already_configured": "Device is already configured" }, "error": { "already_configured": "Device is already configured", "could_not_connect": "Could not connect to the solaredge API", "invalid_api_key": "Invalid API key", - "site_exists": "This site_id is already configured", "site_not_active": "The site is not active" }, "step": { diff --git a/homeassistant/components/solaredge/translations/es-419.json b/homeassistant/components/solaredge/translations/es-419.json index 7cbd7f1b5a1..7ee6fc4f31d 100644 --- a/homeassistant/components/solaredge/translations/es-419.json +++ b/homeassistant/components/solaredge/translations/es-419.json @@ -1,11 +1,5 @@ { "config": { - "abort": { - "site_exists": "Este site_id ya est\u00e1 configurado" - }, - "error": { - "site_exists": "Este site_id ya est\u00e1 configurado" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/solaredge/translations/es.json b/homeassistant/components/solaredge/translations/es.json index 0447184c309..d152481ae0d 100644 --- a/homeassistant/components/solaredge/translations/es.json +++ b/homeassistant/components/solaredge/translations/es.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado", - "site_exists": "Este site_id ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", "could_not_connect": "No se pudo conectar con la API de solaredge", "invalid_api_key": "Clave API no v\u00e1lida", - "site_exists": "Este site_id ya est\u00e1 configurado", "site_not_active": "El sitio no est\u00e1 activo" }, "step": { diff --git a/homeassistant/components/solaredge/translations/et.json b/homeassistant/components/solaredge/translations/et.json index 4497c536042..3cfd261021c 100644 --- a/homeassistant/components/solaredge/translations/et.json +++ b/homeassistant/components/solaredge/translations/et.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "site_exists": "See site_id on juba konfigureeritud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "could_not_connect": "Ei saanud \u00fchendust Solaredge API-ga", "invalid_api_key": "Vale API v\u00f5ti", - "site_exists": "See site_id on juba konfigureeritud", "site_not_active": "Sait pole aktiivne" }, "step": { diff --git a/homeassistant/components/solaredge/translations/fr.json b/homeassistant/components/solaredge/translations/fr.json index 3eea6678d03..fb1822f9a40 100644 --- a/homeassistant/components/solaredge/translations/fr.json +++ b/homeassistant/components/solaredge/translations/fr.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", - "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9ja configur\u00e9 " }, "error": { "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", "could_not_connect": "Impossible de se connecter \u00e0 l'API solaredge", "invalid_api_key": "Cl\u00e9 API invalide", - "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9", "site_not_active": "The site n'est pas actif" }, "step": { diff --git a/homeassistant/components/solaredge/translations/id.json b/homeassistant/components/solaredge/translations/id.json index 41c94755af7..dc95ac6aee5 100644 --- a/homeassistant/components/solaredge/translations/id.json +++ b/homeassistant/components/solaredge/translations/id.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi", - "site_exists": "Nilai site_id ini sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { "already_configured": "Perangkat sudah dikonfigurasi", "could_not_connect": "Tidak dapat terhubung ke API solaredge", "invalid_api_key": "Kunci API tidak valid", - "site_exists": "Nilai site_id ini sudah dikonfigurasi", "site_not_active": "Situs tidak aktif" }, "step": { diff --git a/homeassistant/components/solaredge/translations/it.json b/homeassistant/components/solaredge/translations/it.json index 0d99250e77a..2a80e48b24a 100644 --- a/homeassistant/components/solaredge/translations/it.json +++ b/homeassistant/components/solaredge/translations/it.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "could_not_connect": "Impossibile connettersi all'API Solaredge", "invalid_api_key": "Chiave API non valida", - "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato", "site_not_active": "Il sito non \u00e8 attivo" }, "step": { diff --git a/homeassistant/components/solaredge/translations/ko.json b/homeassistant/components/solaredge/translations/ko.json index ce3ed2a767d..9e890491b72 100644 --- a/homeassistant/components/solaredge/translations/ko.json +++ b/homeassistant/components/solaredge/translations/ko.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "could_not_connect": "SolarEdge API\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "site_not_active": "\uc0ac\uc774\ud2b8\uac00 \ud65c\uc131\ud654\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/solaredge/translations/lb.json b/homeassistant/components/solaredge/translations/lb.json index 709a57f070b..c6f3617b74b 100644 --- a/homeassistant/components/solaredge/translations/lb.json +++ b/homeassistant/components/solaredge/translations/lb.json @@ -1,12 +1,10 @@ { "config": { "abort": { - "already_configured": "Apparat ass scho konfigur\u00e9iert", - "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" + "already_configured": "Apparat ass scho konfigur\u00e9iert" }, "error": { "already_configured": "Apparat ass scho konfigur\u00e9iert", - "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert", "site_not_active": "De Site ass net aktiv" }, "step": { diff --git a/homeassistant/components/solaredge/translations/nl.json b/homeassistant/components/solaredge/translations/nl.json index 24e1716dd57..fd4bacdd98a 100644 --- a/homeassistant/components/solaredge/translations/nl.json +++ b/homeassistant/components/solaredge/translations/nl.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd", - "site_exists": "Deze site_id is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "already_configured": "Apparaat is al geconfigureerd", "could_not_connect": "Kon geen verbinding maken met de solaredge API", "invalid_api_key": "Ongeldige API-sleutel", - "site_exists": "Deze site_id is al geconfigureerd", "site_not_active": "De site is niet actief" }, "step": { diff --git a/homeassistant/components/solaredge/translations/no.json b/homeassistant/components/solaredge/translations/no.json index 7ff7dd8f144..9609174f519 100644 --- a/homeassistant/components/solaredge/translations/no.json +++ b/homeassistant/components/solaredge/translations/no.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert", - "site_exists": "Denne site_id er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert" }, "error": { "already_configured": "Enheten er allerede konfigurert", "could_not_connect": "Kunne ikke koble til solaredge API", "invalid_api_key": "Ugyldig API-n\u00f8kkel", - "site_exists": "Denne site_id er allerede konfigurert", "site_not_active": "Nettstedet er ikke aktivt" }, "step": { diff --git a/homeassistant/components/solaredge/translations/pl.json b/homeassistant/components/solaredge/translations/pl.json index 2fa4af72cb3..85eae5e6321 100644 --- a/homeassistant/components/solaredge/translations/pl.json +++ b/homeassistant/components/solaredge/translations/pl.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "site_exists": "Ten site_id jest ju\u017c skonfigurowany" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "could_not_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z API solaredge", "invalid_api_key": "Nieprawid\u0142owy klucz API", - "site_exists": "Ten site_id jest ju\u017c skonfigurowany", "site_not_active": "Strona nie jest aktywna" }, "step": { diff --git a/homeassistant/components/solaredge/translations/ru.json b/homeassistant/components/solaredge/translations/ru.json index fbda2e7eb18..d6854690c2d 100644 --- a/homeassistant/components/solaredge/translations/ru.json +++ b/homeassistant/components/solaredge/translations/ru.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", - "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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "could_not_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Solaredge.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", - "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_not_active": "\u0421\u0430\u0439\u0442 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u0435\u043d." }, "step": { diff --git a/homeassistant/components/solaredge/translations/sl.json b/homeassistant/components/solaredge/translations/sl.json index 3414e37a657..68d52b867ad 100644 --- a/homeassistant/components/solaredge/translations/sl.json +++ b/homeassistant/components/solaredge/translations/sl.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Naprava je \u017ee name\u0161\u010dena", - "site_exists": "Ta site_id je \u017ee nastavljen" + "already_configured": "Naprava je \u017ee name\u0161\u010dena" }, "error": { "already_configured": "Naprava je \u017ee name\u0161\u010dena", "could_not_connect": "Ni se bilo mogo\u010de povezati s Solaredge API", "invalid_api_key": "Neveljaven API klju\u010d", - "site_exists": "Ta site_id je \u017ee nastavljen", "site_not_active": "Stran ni aktivna" }, "step": { diff --git a/homeassistant/components/solaredge/translations/sv.json b/homeassistant/components/solaredge/translations/sv.json index 01c52eb7fb4..f09320388f2 100644 --- a/homeassistant/components/solaredge/translations/sv.json +++ b/homeassistant/components/solaredge/translations/sv.json @@ -1,11 +1,5 @@ { "config": { - "abort": { - "site_exists": "Denna site_id \u00e4r redan konfigurerad" - }, - "error": { - "site_exists": "Denna site_id \u00e4r redan konfigurerad" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/solaredge/translations/uk.json b/homeassistant/components/solaredge/translations/uk.json index 5ad67d87680..d27a73509de 100644 --- a/homeassistant/components/solaredge/translations/uk.json +++ b/homeassistant/components/solaredge/translations/uk.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", - "site_exists": "\u0426\u0435\u0439 site_id \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439." + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." }, "error": { "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", "could_not_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 API Solaredge.", "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", - "site_exists": "\u0426\u0435\u0439 site_id \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439.", "site_not_active": "\u0421\u0430\u0439\u0442 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0439." }, "step": { diff --git a/homeassistant/components/solaredge/translations/zh-Hant.json b/homeassistant/components/solaredge/translations/zh-Hant.json index 24dbeccdf47..ff1a01b3567 100644 --- a/homeassistant/components/solaredge/translations/zh-Hant.json +++ b/homeassistant/components/solaredge/translations/zh-Hant.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "could_not_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 solaredge API", "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", - "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "site_not_active": "\u7db2\u7ad9\u672a\u555f\u7528" }, "step": { diff --git a/homeassistant/components/somfy_mylink/translations/ca.json b/homeassistant/components/somfy_mylink/translations/ca.json index 93ae58ca2bf..a9181aa4605 100644 --- a/homeassistant/components/somfy_mylink/translations/ca.json +++ b/homeassistant/components/somfy_mylink/translations/ca.json @@ -8,7 +8,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/en.json b/homeassistant/components/somfy_mylink/translations/en.json index e646bf9abcf..cb373008778 100644 --- a/homeassistant/components/somfy_mylink/translations/en.json +++ b/homeassistant/components/somfy_mylink/translations/en.json @@ -25,8 +25,17 @@ "cannot_connect": "Failed to connect" }, "step": { + "entity_config": { + "data": { + "reverse": "Cover is reversed" + }, + "description": "Configure options for `{entity_id}`", + "title": "Configure Entity" + }, "init": { "data": { + "default_reverse": "Default reversal status for unconfigured covers", + "entity_id": "Configure a specific entity.", "target_id": "Configure options for a cover." }, "title": "Configure MyLink Options" @@ -39,5 +48,6 @@ "title": "Configure MyLink Cover" } } - } + }, + "title": "Somfy MyLink" } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/et.json b/homeassistant/components/somfy_mylink/translations/et.json index 6d965220d7e..17122eb449e 100644 --- a/homeassistant/components/somfy_mylink/translations/et.json +++ b/homeassistant/components/somfy_mylink/translations/et.json @@ -8,7 +8,7 @@ "invalid_auth": "Vigane autentimine", "unknown": "Ootamatu t\u00f5rge" }, - "flow_title": "Somfy MyLink {mac} ( {ip} )", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/no.json b/homeassistant/components/somfy_mylink/translations/no.json index 5b9b6608c25..2b629015f36 100644 --- a/homeassistant/components/somfy_mylink/translations/no.json +++ b/homeassistant/components/somfy_mylink/translations/no.json @@ -8,7 +8,7 @@ "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "{mac} ( {ip} )", "step": { "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/ru.json b/homeassistant/components/somfy_mylink/translations/ru.json index 7c981664335..987c00a5a2e 100644 --- a/homeassistant/components/somfy_mylink/translations/ru.json +++ b/homeassistant/components/somfy_mylink/translations/ru.json @@ -8,7 +8,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/zh-Hant.json b/homeassistant/components/somfy_mylink/translations/zh-Hant.json index 7e495cfacee..0aa72fb0c61 100644 --- a/homeassistant/components/somfy_mylink/translations/zh-Hant.json +++ b/homeassistant/components/somfy_mylink/translations/zh-Hant.json @@ -8,7 +8,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/ca.json b/homeassistant/components/sonarr/translations/ca.json index 7f9d34d7ce0..10930df8525 100644 --- a/homeassistant/components/sonarr/translations/ca.json +++ b/homeassistant/components/sonarr/translations/ca.json @@ -9,7 +9,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "La integraci\u00f3 de Sonarr ha de tornar a autenticar-se manualment amb l'API de Sonarr allotjada a: {host}", diff --git a/homeassistant/components/sonarr/translations/et.json b/homeassistant/components/sonarr/translations/et.json index c95b2e9dc88..ba8f96413c3 100644 --- a/homeassistant/components/sonarr/translations/et.json +++ b/homeassistant/components/sonarr/translations/et.json @@ -9,7 +9,7 @@ "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus" }, - "flow_title": "", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "Sonarr-i sidumine tuleb k\u00e4sitsi taastuvastada Sonarr API abil: {host}", diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json index 88fe5330ee0..312d4d5e91a 100644 --- a/homeassistant/components/sonarr/translations/no.json +++ b/homeassistant/components/sonarr/translations/no.json @@ -9,7 +9,7 @@ "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "Sonarr-integrasjonen m\u00e5 autentiseres p\u00e5 nytt med Sonarr API vert p\u00e5: {host}", diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json index 6bbb204b69e..3f3b9593a09 100644 --- a/homeassistant/components/sonarr/translations/ru.json +++ b/homeassistant/components/sonarr/translations/ru.json @@ -9,7 +9,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Sonarr \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {host}", diff --git a/homeassistant/components/sonarr/translations/zh-Hant.json b/homeassistant/components/sonarr/translations/zh-Hant.json index ec79d26532d..c6f97c1892e 100644 --- a/homeassistant/components/sonarr/translations/zh-Hant.json +++ b/homeassistant/components/sonarr/translations/zh-Hant.json @@ -9,7 +9,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "Sonarr\uff1a{name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "Sonarr \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49 Sonarr API\uff1a{host}", diff --git a/homeassistant/components/songpal/translations/ca.json b/homeassistant/components/songpal/translations/ca.json index 02f3f4a4d05..4c3fafa0585 100644 --- a/homeassistant/components/songpal/translations/ca.json +++ b/homeassistant/components/songpal/translations/ca.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "Vols configurar {name} ({host})?" diff --git a/homeassistant/components/songpal/translations/et.json b/homeassistant/components/songpal/translations/et.json index 337f32b5b59..87c29ae2c5d 100644 --- a/homeassistant/components/songpal/translations/et.json +++ b/homeassistant/components/songpal/translations/et.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "Sony Songpal {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "init": { "description": "Kas soovite seadistada {name}({host})?" diff --git a/homeassistant/components/songpal/translations/no.json b/homeassistant/components/songpal/translations/no.json index 645816d1f13..583de17ad91 100644 --- a/homeassistant/components/songpal/translations/no.json +++ b/homeassistant/components/songpal/translations/no.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "{name} ( {host} )", "step": { "init": { "description": "Vil du sette opp {name} ({host})?" diff --git a/homeassistant/components/songpal/translations/ru.json b/homeassistant/components/songpal/translations/ru.json index b572236b7f7..7372a863850 100644 --- a/homeassistant/components/songpal/translations/ru.json +++ b/homeassistant/components/songpal/translations/ru.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?" diff --git a/homeassistant/components/songpal/translations/zh-Hant.json b/homeassistant/components/songpal/translations/zh-Hant.json index 857daf2bce5..417e8c36f6c 100644 --- a/homeassistant/components/songpal/translations/zh-Hant.json +++ b/homeassistant/components/songpal/translations/zh-Hant.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f" diff --git a/homeassistant/components/squeezebox/translations/ca.json b/homeassistant/components/squeezebox/translations/ca.json index edd34c8454a..754b52d2210 100644 --- a/homeassistant/components/squeezebox/translations/ca.json +++ b/homeassistant/components/squeezebox/translations/ca.json @@ -10,7 +10,7 @@ "no_server_found": "No s'ha pogut descobrir el servidor autom\u00e0ticament.", "unknown": "Error inesperat" }, - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/squeezebox/translations/et.json b/homeassistant/components/squeezebox/translations/et.json index 44e075c83ca..0f1ca623b35 100644 --- a/homeassistant/components/squeezebox/translations/et.json +++ b/homeassistant/components/squeezebox/translations/et.json @@ -10,7 +10,7 @@ "no_server_found": "Serverit ei \u00f5nnestunud automaatselt tuvastada.", "unknown": "Tundmatu viga" }, - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/squeezebox/translations/no.json b/homeassistant/components/squeezebox/translations/no.json index d33ec911948..6bd0ecddb6a 100644 --- a/homeassistant/components/squeezebox/translations/no.json +++ b/homeassistant/components/squeezebox/translations/no.json @@ -10,7 +10,7 @@ "no_server_found": "Kan ikke automatisk oppdage serveren.", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/squeezebox/translations/ru.json b/homeassistant/components/squeezebox/translations/ru.json index 3b144adc04e..7753ec5167d 100644 --- a/homeassistant/components/squeezebox/translations/ru.json +++ b/homeassistant/components/squeezebox/translations/ru.json @@ -10,7 +10,7 @@ "no_server_found": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/squeezebox/translations/zh-Hant.json b/homeassistant/components/squeezebox/translations/zh-Hant.json index f2239e98dba..c34d37d03ee 100644 --- a/homeassistant/components/squeezebox/translations/zh-Hant.json +++ b/homeassistant/components/squeezebox/translations/zh-Hant.json @@ -10,7 +10,7 @@ "no_server_found": "\u7121\u6cd5\u81ea\u52d5\u63a2\u7d22\u4f3a\u670d\u5668\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "\u7f85\u6280 Squeezebox\uff1a{host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index c3dc8345ecd..00b879eca0d 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/subaru/translations/ca.json b/homeassistant/components/subaru/translations/ca.json index 51cd44b4ce2..310747f613e 100644 --- a/homeassistant/components/subaru/translations/ca.json +++ b/homeassistant/components/subaru/translations/ca.json @@ -8,8 +8,7 @@ "bad_pin_format": "El PIN ha de tenir 4 d\u00edgits", "cannot_connect": "Ha fallat la connexi\u00f3", "incorrect_pin": "PIN incorrecte", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/cs.json b/homeassistant/components/subaru/translations/cs.json index ee3bf7347ca..e127a0f4106 100644 --- a/homeassistant/components/subaru/translations/cs.json +++ b/homeassistant/components/subaru/translations/cs.json @@ -8,8 +8,7 @@ "bad_pin_format": "PIN by m\u011bl m\u00edt 4 \u010d\u00edslice", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "incorrect_pin": "Nespr\u00e1vn\u00fd PIN", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json index 135b80b64ab..dd2fb797dbc 100644 --- a/homeassistant/components/subaru/translations/de.json +++ b/homeassistant/components/subaru/translations/de.json @@ -8,8 +8,7 @@ "bad_pin_format": "Die PIN sollte 4-stellig sein", "cannot_connect": "Verbindung fehlgeschlagen", "incorrect_pin": "Falsche PIN", - "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/el.json b/homeassistant/components/subaru/translations/el.json index 17ede1af4e7..f0ad6a402d8 100644 --- a/homeassistant/components/subaru/translations/el.json +++ b/homeassistant/components/subaru/translations/el.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, "step": { "pin": { "data": { diff --git a/homeassistant/components/subaru/translations/en.json b/homeassistant/components/subaru/translations/en.json index ea15ff00552..722363d4d74 100644 --- a/homeassistant/components/subaru/translations/en.json +++ b/homeassistant/components/subaru/translations/en.json @@ -8,8 +8,7 @@ "bad_pin_format": "PIN should be 4 digits", "cannot_connect": "Failed to connect", "incorrect_pin": "Incorrect PIN", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "invalid_auth": "Invalid authentication" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json index ff8c5720781..42a13a37a7a 100644 --- a/homeassistant/components/subaru/translations/es.json +++ b/homeassistant/components/subaru/translations/es.json @@ -8,8 +8,7 @@ "bad_pin_format": "El PIN debe tener 4 d\u00edgitos", "cannot_connect": "No se pudo conectar", "incorrect_pin": "PIN incorrecto", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "unknown": "Error inesperado" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/et.json b/homeassistant/components/subaru/translations/et.json index 30dd690f849..e7390b3b77f 100644 --- a/homeassistant/components/subaru/translations/et.json +++ b/homeassistant/components/subaru/translations/et.json @@ -8,8 +8,7 @@ "bad_pin_format": "PIN-kood peaks olema 4-kohaline", "cannot_connect": "\u00dchendamine nurjus", "incorrect_pin": "Vale PIN-kood", - "invalid_auth": "Vigane autentimine", - "unknown": "Ootamatu t\u00f5rge" + "invalid_auth": "Vigane autentimine" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/fr.json b/homeassistant/components/subaru/translations/fr.json index 25544534297..473492ddd07 100644 --- a/homeassistant/components/subaru/translations/fr.json +++ b/homeassistant/components/subaru/translations/fr.json @@ -8,8 +8,7 @@ "bad_pin_format": "Le code PIN doit \u00eatre compos\u00e9 de 4 chiffres", "cannot_connect": "\u00c9chec de connexion", "incorrect_pin": "PIN incorrect", - "invalid_auth": "Authentification invalide", - "unknown": "Erreur inattendue" + "invalid_auth": "Authentification invalide" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/hu.json b/homeassistant/components/subaru/translations/hu.json index d92ca24b7a1..3be1db59239 100644 --- a/homeassistant/components/subaru/translations/hu.json +++ b/homeassistant/components/subaru/translations/hu.json @@ -8,8 +8,7 @@ "bad_pin_format": "A PIN-nek 4 sz\u00e1mjegy\u0171nek kell lennie", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "incorrect_pin": "Helytelen PIN", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/id.json b/homeassistant/components/subaru/translations/id.json index 1ae1506fe09..2a44ec16b92 100644 --- a/homeassistant/components/subaru/translations/id.json +++ b/homeassistant/components/subaru/translations/id.json @@ -8,8 +8,7 @@ "bad_pin_format": "PIN harus terdiri dari 4 angka", "cannot_connect": "Gagal terhubung", "incorrect_pin": "PIN salah", - "invalid_auth": "Autentikasi tidak valid", - "unknown": "Kesalahan yang tidak diharapkan" + "invalid_auth": "Autentikasi tidak valid" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/it.json b/homeassistant/components/subaru/translations/it.json index c585834f12b..a6752b810eb 100644 --- a/homeassistant/components/subaru/translations/it.json +++ b/homeassistant/components/subaru/translations/it.json @@ -8,8 +8,7 @@ "bad_pin_format": "Il PIN deve essere di 4 cifre", "cannot_connect": "Impossibile connettersi", "incorrect_pin": "PIN errato", - "invalid_auth": "Autenticazione non valida", - "unknown": "Errore imprevisto" + "invalid_auth": "Autenticazione non valida" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/ko.json b/homeassistant/components/subaru/translations/ko.json index 8fe12309812..2645897b0b7 100644 --- a/homeassistant/components/subaru/translations/ko.json +++ b/homeassistant/components/subaru/translations/ko.json @@ -8,8 +8,7 @@ "bad_pin_format": "PIN\uc740 4\uc790\ub9ac\uc5ec\uc57c \ud569\ub2c8\ub2e4", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "incorrect_pin": "PIN\uc774 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/nl.json b/homeassistant/components/subaru/translations/nl.json index 339c990ca0b..931ed6f967f 100644 --- a/homeassistant/components/subaru/translations/nl.json +++ b/homeassistant/components/subaru/translations/nl.json @@ -8,8 +8,7 @@ "bad_pin_format": "De pincode moet uit 4 cijfers bestaan", "cannot_connect": "Kan geen verbinding maken", "incorrect_pin": "Onjuiste PIN", - "invalid_auth": "Ongeldige authenticatie", - "unknown": "Onverwachte fout" + "invalid_auth": "Ongeldige authenticatie" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/no.json b/homeassistant/components/subaru/translations/no.json index 25b0f7bec29..6ac200ec131 100644 --- a/homeassistant/components/subaru/translations/no.json +++ b/homeassistant/components/subaru/translations/no.json @@ -8,8 +8,7 @@ "bad_pin_format": "PIN-koden skal best\u00e5 av fire sifre", "cannot_connect": "Tilkobling mislyktes", "incorrect_pin": "Feil PIN", - "invalid_auth": "Ugyldig godkjenning", - "unknown": "Uventet feil" + "invalid_auth": "Ugyldig godkjenning" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/pl.json b/homeassistant/components/subaru/translations/pl.json index 99415cdeea7..8bc9976fa2e 100644 --- a/homeassistant/components/subaru/translations/pl.json +++ b/homeassistant/components/subaru/translations/pl.json @@ -8,8 +8,7 @@ "bad_pin_format": "PIN powinien sk\u0142ada\u0107 si\u0119 z 4 cyfr", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "incorrect_pin": "Nieprawid\u0142owy PIN", - "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/pt.json b/homeassistant/components/subaru/translations/pt.json index 7f3e1ec8e3b..b5563108ab3 100644 --- a/homeassistant/components/subaru/translations/pt.json +++ b/homeassistant/components/subaru/translations/pt.json @@ -6,8 +6,7 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { diff --git a/homeassistant/components/subaru/translations/ru.json b/homeassistant/components/subaru/translations/ru.json index c414ebb385f..87f56f8ac8a 100644 --- a/homeassistant/components/subaru/translations/ru.json +++ b/homeassistant/components/subaru/translations/ru.json @@ -8,8 +8,7 @@ "bad_pin_format": "PIN-\u043a\u043e\u0434 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 4 \u0446\u0438\u0444\u0440.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "incorrect_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/zh-Hant.json b/homeassistant/components/subaru/translations/zh-Hant.json index 22eaa589fe2..15799492d85 100644 --- a/homeassistant/components/subaru/translations/zh-Hant.json +++ b/homeassistant/components/subaru/translations/zh-Hant.json @@ -8,8 +8,7 @@ "bad_pin_format": "PIN \u78bc\u61c9\u8a72\u70ba 4 \u4f4d\u6578\u5b57", "cannot_connect": "\u9023\u7dda\u5931\u6557", "incorrect_pin": "PIN \u78bc\u932f\u8aa4", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { "pin": { diff --git a/homeassistant/components/syncthing/translations/de.json b/homeassistant/components/syncthing/translations/de.json index 25562c2b4f9..bef066f385b 100644 --- a/homeassistant/components/syncthing/translations/de.json +++ b/homeassistant/components/syncthing/translations/de.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "token": "Token", "url": "URL", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } diff --git a/homeassistant/components/syncthru/translations/ca.json b/homeassistant/components/syncthru/translations/ca.json index 1664415d2b4..18c2970018f 100644 --- a/homeassistant/components/syncthru/translations/ca.json +++ b/homeassistant/components/syncthru/translations/ca.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "El dispositiu no \u00e9s compatible amb SyncThru", "unknown_state": "Estat de la impressora desconegut, comprova l'URL i la connecci\u00f3 de la xarxa" }, - "flow_title": "Impressora Samsung SyncThru: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/syncthru/translations/et.json b/homeassistant/components/syncthru/translations/et.json index 85fae03ef3e..158bf53c25e 100644 --- a/homeassistant/components/syncthru/translations/et.json +++ b/homeassistant/components/syncthru/translations/et.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "Seade ei toeta SyncThru-d", "unknown_state": "Printeri olek teadmata, kontrolli URL-i ja v\u00f5rgu\u00fchendust" }, - "flow_title": "", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/syncthru/translations/no.json b/homeassistant/components/syncthru/translations/no.json index db24ef5abc7..a0c5aff9abf 100644 --- a/homeassistant/components/syncthru/translations/no.json +++ b/homeassistant/components/syncthru/translations/no.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "Enheten st\u00f8tter ikke SyncThru", "unknown_state": "Skrivertilstand ukjent, kontroller URL-adresse og nettverkstilkobling" }, - "flow_title": "Samsung SyncThru-skriver: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/syncthru/translations/ru.json b/homeassistant/components/syncthru/translations/ru.json index 3e904f3d32a..06cfd947cbd 100644 --- a/homeassistant/components/syncthru/translations/ru.json +++ b/homeassistant/components/syncthru/translations/ru.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 SyncThru.", "unknown_state": "\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0441\u0435\u0442\u0438." }, - "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Samsung SyncThru: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/syncthru/translations/zh-Hant.json b/homeassistant/components/syncthru/translations/zh-Hant.json index 6d2cbec0f0c..8419cbd04a4 100644 --- a/homeassistant/components/syncthru/translations/zh-Hant.json +++ b/homeassistant/components/syncthru/translations/zh-Hant.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4 SyncThru", "unknown_state": "\u5370\u8868\u6a5f\u72c0\u614b\u672a\u77e5\uff0c\u8acb\u78ba\u8a8d URL \u8207\u7db2\u8def\u9023\u7dda" }, - "flow_title": "Samsung SyncThru \u5370\u8868\u6a5f\uff1a{name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/ca.json b/homeassistant/components/synology_dsm/translations/ca.json index 05ea3f78801..e08ed1d74ce 100644 --- a/homeassistant/components/synology_dsm/translations/ca.json +++ b/homeassistant/components/synology_dsm/translations/ca.json @@ -10,7 +10,7 @@ "otp_failed": "L'autenticaci\u00f3 en dos passos ha fallat, torna-ho a provar amb un nou codi", "unknown": "Error inesperat" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/et.json b/homeassistant/components/synology_dsm/translations/et.json index 7b7b14c17ea..7d192828d67 100644 --- a/homeassistant/components/synology_dsm/translations/et.json +++ b/homeassistant/components/synology_dsm/translations/et.json @@ -10,7 +10,7 @@ "otp_failed": "Kaheastmeline autentimine nurjus, proovi uuesti uue p\u00e4\u00e4sukoodiga", "unknown": "Tundmatu viga" }, - "flow_title": "", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index c66ccdcbf45..c8bb60bcb3e 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -10,7 +10,7 @@ "otp_failed": "Totrinnsgodkjenning mislyktes, pr\u00f8v p\u00e5 nytt med en ny passord", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "{name} ( {host} )", "step": { "2sa": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json index 98a7522b27d..9a7157b6dc3 100644 --- a/homeassistant/components/synology_dsm/translations/ru.json +++ b/homeassistant/components/synology_dsm/translations/ru.json @@ -10,7 +10,7 @@ "otp_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u0441 \u043d\u043e\u0432\u044b\u043c \u043f\u0430\u0440\u043e\u043b\u0435\u043c.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index af8103b8189..9b2a4726fc4 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -10,7 +10,7 @@ "otp_failed": "\u96d9\u91cd\u8a8d\u8b49\u5931\u6557\uff0c\u8acb\u91cd\u65b0\u53d6\u5f97\u4ee3\u78bc\u5f8c\u91cd\u8a66", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "\u7fa4\u6689 DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/system_bridge/translations/ca.json b/homeassistant/components/system_bridge/translations/ca.json index 09a3f9922ed..aac2e139db0 100644 --- a/homeassistant/components/system_bridge/translations/ca.json +++ b/homeassistant/components/system_bridge/translations/ca.json @@ -10,7 +10,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, - "flow_title": "Enlla\u00e7 de sistema: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/system_bridge/translations/de.json b/homeassistant/components/system_bridge/translations/de.json index abb9dd3f7f5..d54c4a23e4f 100644 --- a/homeassistant/components/system_bridge/translations/de.json +++ b/homeassistant/components/system_bridge/translations/de.json @@ -14,14 +14,16 @@ "authenticate": { "data": { "api_key": "API-Schl\u00fcssel" - } + }, + "description": "Bitte gib den API-Schl\u00fcssel ein, den du in deiner Konfiguration f\u00fcr {Name} festgelegt hast." }, "user": { "data": { "api_key": "API-Schl\u00fcssel", "host": "Host", "port": "Port" - } + }, + "description": "Bitte gib Verbindungsdaten ein." } } } diff --git a/homeassistant/components/system_bridge/translations/en.json b/homeassistant/components/system_bridge/translations/en.json index dc94dfe2ac6..3ebcfc6959e 100644 --- a/homeassistant/components/system_bridge/translations/en.json +++ b/homeassistant/components/system_bridge/translations/en.json @@ -27,5 +27,6 @@ "description": "Please enter your connection details." } } - } + }, + "title": "System Bridge" } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/et.json b/homeassistant/components/system_bridge/translations/et.json index d4aff33d335..3724895f147 100644 --- a/homeassistant/components/system_bridge/translations/et.json +++ b/homeassistant/components/system_bridge/translations/et.json @@ -10,7 +10,7 @@ "invalid_auth": "Tuvastamine nurjus", "unknown": "Tundmatu t\u00f5rge" }, - "flow_title": "S\u00fcsteemi sild: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/system_bridge/translations/no.json b/homeassistant/components/system_bridge/translations/no.json index bd46c0e1824..5651dd7cb67 100644 --- a/homeassistant/components/system_bridge/translations/no.json +++ b/homeassistant/components/system_bridge/translations/no.json @@ -10,7 +10,7 @@ "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, - "flow_title": "System Bridge: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/system_bridge/translations/ru.json b/homeassistant/components/system_bridge/translations/ru.json index 35c576620b4..be2cddfdc69 100644 --- a/homeassistant/components/system_bridge/translations/ru.json +++ b/homeassistant/components/system_bridge/translations/ru.json @@ -10,7 +10,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "System Bridge: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/system_bridge/translations/zh-Hant.json b/homeassistant/components/system_bridge/translations/zh-Hant.json index b123cc6e9dd..2ca44e48710 100644 --- a/homeassistant/components/system_bridge/translations/zh-Hant.json +++ b/homeassistant/components/system_bridge/translations/zh-Hant.json @@ -10,7 +10,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "System Bridge\uff1a{name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/tado/translations/de.json b/homeassistant/components/tado/translations/de.json index 9dc410b670e..f531a428be3 100644 --- a/homeassistant/components/tado/translations/de.json +++ b/homeassistant/components/tado/translations/de.json @@ -15,7 +15,7 @@ "password": "Passwort", "username": "Benutzername" }, - "title": "Stellen eine Verbindung zu deinem Tado-Konto her" + "title": "Stelle eine Verbindung zu deinem Tado-Konto her" } } }, diff --git a/homeassistant/components/tellduslive/translations/bg.json b/homeassistant/components/tellduslive/translations/bg.json index 232d51f0713..6e770c58b30 100644 --- a/homeassistant/components/tellduslive/translations/bg.json +++ b/homeassistant/components/tellduslive/translations/bg.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/tellduslive/translations/ca.json b/homeassistant/components/tellduslive/translations/ca.json index b0fb93241b9..5c7e04684e5 100644 --- a/homeassistant/components/tellduslive/translations/ca.json +++ b/homeassistant/components/tellduslive/translations/ca.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "El servei ja est\u00e0 configurat", - "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "unknown": "Error inesperat", "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3." diff --git a/homeassistant/components/tellduslive/translations/cs.json b/homeassistant/components/tellduslive/translations/cs.json index e0780ece8be..188998c3080 100644 --- a/homeassistant/components/tellduslive/translations/cs.json +++ b/homeassistant/components/tellduslive/translations/cs.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Slu\u017eba je ji\u017e nastavena", - "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy." diff --git a/homeassistant/components/tellduslive/translations/da.json b/homeassistant/components/tellduslive/translations/da.json index e9a3c1fdd63..dd295fd7bcc 100644 --- a/homeassistant/components/tellduslive/translations/da.json +++ b/homeassistant/components/tellduslive/translations/da.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", "authorize_url_timeout": "Timeout ved generering af autoriseret url.", "unknown": "Ukendt fejl opstod" }, diff --git a/homeassistant/components/tellduslive/translations/de.json b/homeassistant/components/tellduslive/translations/de.json index 098ad9c17be..0a952ba013b 100644 --- a/homeassistant/components/tellduslive/translations/de.json +++ b/homeassistant/components/tellduslive/translations/de.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Dienst ist bereits konfiguriert", - "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "unknown": "Unerwarteter Fehler", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" diff --git a/homeassistant/components/tellduslive/translations/en.json b/homeassistant/components/tellduslive/translations/en.json index b1b9cd9ab10..5dbc96d575d 100644 --- a/homeassistant/components/tellduslive/translations/en.json +++ b/homeassistant/components/tellduslive/translations/en.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Service is already configured", - "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize URL.", "unknown": "Unexpected error", "unknown_authorize_url_generation": "Unknown error generating an authorize URL." diff --git a/homeassistant/components/tellduslive/translations/es-419.json b/homeassistant/components/tellduslive/translations/es-419.json index 064bd4edd33..2af310c4846 100644 --- a/homeassistant/components/tellduslive/translations/es-419.json +++ b/homeassistant/components/tellduslive/translations/es-419.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", "unknown": "Se produjo un error desconocido" }, diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json index 7b39b7fe042..882e553d46e 100644 --- a/homeassistant/components/tellduslive/translations/es.json +++ b/homeassistant/components/tellduslive/translations/es.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive ya est\u00e1 configurado", - "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", "unknown": "Se produjo un error desconocido", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." diff --git a/homeassistant/components/tellduslive/translations/et.json b/homeassistant/components/tellduslive/translations/et.json index bab010bd845..11685372e22 100644 --- a/homeassistant/components/tellduslive/translations/et.json +++ b/homeassistant/components/tellduslive/translations/et.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Teenus on juba seadistatud", - "authorize_url_fail": "Tundmatu viga tuvastamise URL-i loomisel.", "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp.", "unknown": "Tundmatu viga", "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel." diff --git a/homeassistant/components/tellduslive/translations/fr.json b/homeassistant/components/tellduslive/translations/fr.json index ef4d7bc44dd..02e05d2c869 100644 --- a/homeassistant/components/tellduslive/translations/fr.json +++ b/homeassistant/components/tellduslive/translations/fr.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive est d\u00e9j\u00e0 configur\u00e9", - "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "unknown": "Une erreur inconnue s'est produite", "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." diff --git a/homeassistant/components/tellduslive/translations/hu.json b/homeassistant/components/tellduslive/translations/hu.json index 2e59375369d..a496f1f2e45 100644 --- a/homeassistant/components/tellduslive/translations/hu.json +++ b/homeassistant/components/tellduslive/translations/hu.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." diff --git a/homeassistant/components/tellduslive/translations/id.json b/homeassistant/components/tellduslive/translations/id.json index 1a405e794fe..c78304d2d8c 100644 --- a/homeassistant/components/tellduslive/translations/id.json +++ b/homeassistant/components/tellduslive/translations/id.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Layanan sudah dikonfigurasi", - "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", "unknown": "Kesalahan yang tidak diharapkan", "unknown_authorize_url_generation": "Kesalahan tidak dikenal ketika menghasilkan URL otorisasi." diff --git a/homeassistant/components/tellduslive/translations/it.json b/homeassistant/components/tellduslive/translations/it.json index 431b986fdae..8c879798a45 100644 --- a/homeassistant/components/tellduslive/translations/it.json +++ b/homeassistant/components/tellduslive/translations/it.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", - "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "unknown": "Errore imprevisto", "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione." diff --git a/homeassistant/components/tellduslive/translations/ko.json b/homeassistant/components/tellduslive/translations/ko.json index 6107245c088..a9ce69e5128 100644 --- a/homeassistant/components/tellduslive/translations/ko.json +++ b/homeassistant/components/tellduslive/translations/ko.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/tellduslive/translations/lb.json b/homeassistant/components/tellduslive/translations/lb.json index 2b809050677..70fcdabe05f 100644 --- a/homeassistant/components/tellduslive/translations/lb.json +++ b/homeassistant/components/tellduslive/translations/lb.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Service ass scho konfigur\u00e9iert", - "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", "unknown": "Onerwaarte Feeler", "unknown_authorize_url_generation": "Onbekannte Feeler beim erstellen vun der Authorisatiouns URL." diff --git a/homeassistant/components/tellduslive/translations/nl.json b/homeassistant/components/tellduslive/translations/nl.json index c34911553ab..d0c03a341f0 100644 --- a/homeassistant/components/tellduslive/translations/nl.json +++ b/homeassistant/components/tellduslive/translations/nl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie url.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "unknown": "Onverwachte fout", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." diff --git a/homeassistant/components/tellduslive/translations/no.json b/homeassistant/components/tellduslive/translations/no.json index 563359d266a..776669d743b 100644 --- a/homeassistant/components/tellduslive/translations/no.json +++ b/homeassistant/components/tellduslive/translations/no.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "unknown": "Uventet feil", "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL." diff --git a/homeassistant/components/tellduslive/translations/pl.json b/homeassistant/components/tellduslive/translations/pl.json index 81d59e02a12..4f42a4d3810 100644 --- a/homeassistant/components/tellduslive/translations/pl.json +++ b/homeassistant/components/tellduslive/translations/pl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", - "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "unknown": "Nieoczekiwany b\u0142\u0105d", "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji" diff --git a/homeassistant/components/tellduslive/translations/pt-BR.json b/homeassistant/components/tellduslive/translations/pt-BR.json index 44572d61884..036af4e1c45 100644 --- a/homeassistant/components/tellduslive/translations/pt-BR.json +++ b/homeassistant/components/tellduslive/translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo limite de gera\u00e7\u00e3o de url de autoriza\u00e7\u00e3o.", "unknown": "Ocorreu um erro desconhecido" }, diff --git a/homeassistant/components/tellduslive/translations/pt.json b/homeassistant/components/tellduslive/translations/pt.json index cde0a2ad9c7..1f06d33d356 100644 --- a/homeassistant/components/tellduslive/translations/pt.json +++ b/homeassistant/components/tellduslive/translations/pt.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", - "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", "unknown": "Ocorreu um erro desconhecido", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." diff --git a/homeassistant/components/tellduslive/translations/ru.json b/homeassistant/components/tellduslive/translations/ru.json index 95a16fa205f..b918c555eb9 100644 --- a/homeassistant/components/tellduslive/translations/ru.json +++ b/homeassistant/components/tellduslive/translations/ru.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", - "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\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "unknown_authorize_url_generation": "\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." diff --git a/homeassistant/components/tellduslive/translations/sl.json b/homeassistant/components/tellduslive/translations/sl.json index ec945015278..7e1e8af7d21 100644 --- a/homeassistant/components/tellduslive/translations/sl.json +++ b/homeassistant/components/tellduslive/translations/sl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje URL-ja je potekla.", "unknown": "Pri\u0161lo je do neznane napake", "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." diff --git a/homeassistant/components/tellduslive/translations/sv.json b/homeassistant/components/tellduslive/translations/sv.json index 48c61bb2b7d..9b45e05fe9c 100644 --- a/homeassistant/components/tellduslive/translations/sv.json +++ b/homeassistant/components/tellduslive/translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ok\u00e4nt fel n\u00e4r genererar en url f\u00f6r att auktorisera.", "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", "unknown": "Ok\u00e4nt fel intr\u00e4ffade" }, diff --git a/homeassistant/components/tellduslive/translations/uk.json b/homeassistant/components/tellduslive/translations/uk.json index ff7b3337bb9..d7a5ff06d4b 100644 --- a/homeassistant/components/tellduslive/translations/uk.json +++ b/homeassistant/components/tellduslive/translations/uk.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", - "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." diff --git a/homeassistant/components/tellduslive/translations/zh-Hans.json b/homeassistant/components/tellduslive/translations/zh-Hans.json index 8dcbc144b90..7dcc50e10ac 100644 --- a/homeassistant/components/tellduslive/translations/zh-Hans.json +++ b/homeassistant/components/tellduslive/translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", "unknown": "\u53d1\u751f\u672a\u77e5\u7684\u9519\u8bef" }, diff --git a/homeassistant/components/tellduslive/translations/zh-Hant.json b/homeassistant/components/tellduslive/translations/zh-Hant.json index 4ce3d2c478e..abd5ca786df 100644 --- a/homeassistant/components/tellduslive/translations/zh-Hant.json +++ b/homeassistant/components/tellduslive/translations/zh-Hant.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" diff --git a/homeassistant/components/toon/translations/ca.json b/homeassistant/components/toon/translations/ca.json index b67ff359ceb..04dd3119e12 100644 --- a/homeassistant/components/toon/translations/ca.json +++ b/homeassistant/components/toon/translations/ca.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "L'acord seleccionat ja est\u00e0 configurat.", - "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_agreements": "Aquest compte no t\u00e9 pantalles Toon.", diff --git a/homeassistant/components/toon/translations/cs.json b/homeassistant/components/toon/translations/cs.json index bf4de080873..465f491ab69 100644 --- a/homeassistant/components/toon/translations/cs.json +++ b/homeassistant/components/toon/translations/cs.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json index 58ed0d65ce1..daeead855c3 100644 --- a/homeassistant/components/toon/translations/de.json +++ b/homeassistant/components/toon/translations/de.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Die ausgew\u00e4hlte Vereinbarung ist bereits konfiguriert.", - "authorize_url_fail": "Unbekannter Fehler beim Generieren einer Autorisierungs-URL.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_agreements": "Dieses Konto hat keine Toon-Anzeigen.", diff --git a/homeassistant/components/toon/translations/en.json b/homeassistant/components/toon/translations/en.json index 3351c16d8d8..61224920400 100644 --- a/homeassistant/components/toon/translations/en.json +++ b/homeassistant/components/toon/translations/en.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "The selected agreement is already configured.", - "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_agreements": "This account has no Toon displays.", diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json index 6539388f76a..d048e53ec90 100644 --- a/homeassistant/components/toon/translations/es.json +++ b/homeassistant/components/toon/translations/es.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "El acuerdo seleccionado ya est\u00e1 configurado.", - "authorize_url_fail": "Error desconocido generando una url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_agreements": "Esta cuenta no tiene pantallas Toon.", diff --git a/homeassistant/components/toon/translations/et.json b/homeassistant/components/toon/translations/et.json index f93fb684b25..aee082f3650 100644 --- a/homeassistant/components/toon/translations/et.json +++ b/homeassistant/components/toon/translations/et.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Valitud n\u00f5usolek on juba seadistatud.", - "authorize_url_fail": "Tundmatu viga tuvastamise URL-i loomisel.", "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp.", "missing_configuration": "Osis pole seadistatud. Palun vaata dokumentatsiooni.", "no_agreements": "Sellel kontol ei ole Toon-i kuvasid.", diff --git a/homeassistant/components/toon/translations/fr.json b/homeassistant/components/toon/translations/fr.json index 3fa6059a58f..3aa36d8a554 100644 --- a/homeassistant/components/toon/translations/fr.json +++ b/homeassistant/components/toon/translations/fr.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "L'accord s\u00e9lectionn\u00e9 est d\u00e9j\u00e0 configur\u00e9.", - "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "Timout de g\u00e9n\u00e9ration de l'URL d'autorisation.", "missing_configuration": "The composant n'est pas configur\u00e9. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation.", "no_agreements": "Ce compte n'a pas d'affichages Toon.", diff --git a/homeassistant/components/toon/translations/id.json b/homeassistant/components/toon/translations/id.json index 6e9d4a76683..ae06ef6a003 100644 --- a/homeassistant/components/toon/translations/id.json +++ b/homeassistant/components/toon/translations/id.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Perjanjian yang dipilih sudah dikonfigurasi.", - "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "no_agreements": "Akun ini tidak memiliki tampilan Toon.", diff --git a/homeassistant/components/toon/translations/it.json b/homeassistant/components/toon/translations/it.json index d757850a44f..72564e03674 100644 --- a/homeassistant/components/toon/translations/it.json +++ b/homeassistant/components/toon/translations/it.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "L'accordo selezionato \u00e8 gi\u00e0 configurato.", - "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_agreements": "Questo account non ha display Toon.", diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json index e36adba2ffb..945a164bff5 100644 --- a/homeassistant/components/toon/translations/ko.json +++ b/homeassistant/components/toon/translations/ko.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\uc120\ud0dd\ub41c \uc57d\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/toon/translations/lb.json b/homeassistant/components/toon/translations/lb.json index e21dfb0c996..4061b14ade4 100644 --- a/homeassistant/components/toon/translations/lb.json +++ b/homeassistant/components/toon/translations/lb.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Den ausgewielten Accord ass scho konfigur\u00e9iert.", - "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4itiwwerschraidung beim erstellen vun der Autorisatioun's URL.", "missing_configuration": "Komponent ass net konfigur\u00e9iert. Folleg der Dokumentatioun.", "no_agreements": "D\u00ebse Kont huet keen Toon Ecran.", diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json index 82f224e2514..021c6276b1a 100644 --- a/homeassistant/components/toon/translations/nl.json +++ b/homeassistant/components/toon/translations/nl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "De geselecteerde overeenkomst is al geconfigureerd.", - "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_agreements": "Dit account heeft geen Toon schermen.", diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json index 41246c42f0e..b09306cd2d9 100644 --- a/homeassistant/components/toon/translations/no.json +++ b/homeassistant/components/toon/translations/no.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Den valgte avtalen er allerede konfigurert.", - "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_agreements": "Denne kontoen har ingen Toon skjermer.", diff --git a/homeassistant/components/toon/translations/pl.json b/homeassistant/components/toon/translations/pl.json index a41865ddc34..10882695318 100644 --- a/homeassistant/components/toon/translations/pl.json +++ b/homeassistant/components/toon/translations/pl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", - "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_agreements": "To konto nie posiada wy\u015bwietlaczy Toon", diff --git a/homeassistant/components/toon/translations/pt.json b/homeassistant/components/toon/translations/pt.json index e4aaaa39138..c82065e2550 100644 --- a/homeassistant/components/toon/translations/pt.json +++ b/homeassistant/components/toon/translations/pt.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", diff --git a/homeassistant/components/toon/translations/ru.json b/homeassistant/components/toon/translations/ru.json index 1818c51bcdd..e0cbc14b87e 100644 --- a/homeassistant/components/toon/translations/ru.json +++ b/homeassistant/components/toon/translations/ru.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0433\u043b\u0430\u0448\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e.", - "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.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", diff --git a/homeassistant/components/toon/translations/uk.json b/homeassistant/components/toon/translations/uk.json index 51aa28f3984..5cca431c647 100644 --- a/homeassistant/components/toon/translations/uk.json +++ b/homeassistant/components/toon/translations/uk.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u041e\u0431\u0440\u0430\u043d\u0430 \u0443\u0433\u043e\u0434\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430.", - "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", "no_agreements": "\u0423 \u0446\u044c\u043e\u043c\u0443 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0456 \u043d\u0435\u043c\u0430\u0454 \u0434\u0438\u0441\u043f\u043b\u0435\u0457\u0432 Toon.", diff --git a/homeassistant/components/toon/translations/zh-Hant.json b/homeassistant/components/toon/translations/zh-Hant.json index 46f6f6cf162..e2217f1be54 100644 --- a/homeassistant/components/toon/translations/zh-Hant.json +++ b/homeassistant/components/toon/translations/zh-Hant.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u6240\u9078\u64c7\u7684\u5354\u8b70\u5730\u5740\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", - "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u986f\u793a\u88dd\u7f6e\u3002", diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index 8fa5af38f92..ee304ff30cd 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -8,6 +8,7 @@ "error": { "invalid_auth": "Invalid authentication" }, + "flow_title": "Tuya configuration", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json index f1cf4a6349b..1c6e95bd962 100644 --- a/homeassistant/components/unifi/translations/ca.json +++ b/homeassistant/components/unifi/translations/ca.json @@ -10,7 +10,7 @@ "service_unavailable": "[%key::common::config_flow::error::cannot_connect%]", "unknown_client_mac": "No hi ha cap client disponible en aquesta adre\u00e7a MAC" }, - "flow_title": "Xarxa UniFi {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/et.json b/homeassistant/components/unifi/translations/et.json index e9d76520435..443d036bd96 100644 --- a/homeassistant/components/unifi/translations/et.json +++ b/homeassistant/components/unifi/translations/et.json @@ -10,7 +10,7 @@ "service_unavailable": "\u00dchendamine nurjus", "unknown_client_mac": "Sellel MAC-aadressil pole \u00fchtegi klienti saadaval" }, - "flow_title": "UniFi Network {site} ( {host} )", + "flow_title": "{site} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index 72944a9d540..b1ecb706345 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -10,7 +10,7 @@ "service_unavailable": "Tilkobling mislyktes", "unknown_client_mac": "Ingen klient tilgjengelig p\u00e5 den MAC-adressen" }, - "flow_title": "UniFi-nettverk {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index 8c164272fdb..8a34f9fc17e 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -10,7 +10,7 @@ "service_unavailable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown_client_mac": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043d\u0430 \u044d\u0442\u043e\u043c MAC-\u0430\u0434\u0440\u0435\u0441\u0435." }, - "flow_title": "UniFi Network {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index add0a387309..22cfeed3d65 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -10,7 +10,7 @@ "service_unavailable": "\u9023\u7dda\u5931\u6557", "unknown_client_mac": "\u8a72 Mac \u4f4d\u5740\u7121\u53ef\u7528\u5ba2\u6236\u7aef" }, - "flow_title": "UniFi Network {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/translations/ca.json b/homeassistant/components/upnp/translations/ca.json index 5a3ed99a616..3b5763163b8 100644 --- a/homeassistant/components/upnp/translations/ca.json +++ b/homeassistant/components/upnp/translations/ca.json @@ -9,7 +9,7 @@ "one": "un", "other": "altre" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Vols configurar aquest dispositiu UPnP/IGD?" diff --git a/homeassistant/components/upnp/translations/en.json b/homeassistant/components/upnp/translations/en.json index 83741bd3195..98f6cb88b87 100644 --- a/homeassistant/components/upnp/translations/en.json +++ b/homeassistant/components/upnp/translations/en.json @@ -7,7 +7,6 @@ }, "flow_title": "{name}", "step": { - "init": {}, "ssdp_confirm": { "description": "Do you want to set up this UPnP/IGD device?" }, diff --git a/homeassistant/components/upnp/translations/et.json b/homeassistant/components/upnp/translations/et.json index d5ee1897f98..dc505d30ffc 100644 --- a/homeassistant/components/upnp/translations/et.json +++ b/homeassistant/components/upnp/translations/et.json @@ -9,7 +9,7 @@ "one": "\u00fcks", "other": "Teine" }, - "flow_title": "UPnP / IGD: {name}", + "flow_title": "{name}", "step": { "init": { "one": "\u00dcks", diff --git a/homeassistant/components/upnp/translations/no.json b/homeassistant/components/upnp/translations/no.json index 606fb8b6853..f3875a2c3ef 100644 --- a/homeassistant/components/upnp/translations/no.json +++ b/homeassistant/components/upnp/translations/no.json @@ -13,7 +13,7 @@ "two": "to", "zero": "ingen" }, - "flow_title": "", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "\u00d8nsker du \u00e5 sette opp denne UPnP/IGD-enheten?" diff --git a/homeassistant/components/upnp/translations/ru.json b/homeassistant/components/upnp/translations/ru.json index 518cd182339..869428eb921 100644 --- a/homeassistant/components/upnp/translations/ru.json +++ b/homeassistant/components/upnp/translations/ru.json @@ -11,7 +11,7 @@ "one": "\u043e\u0434\u0438\u043d", "other": "\u0434\u0440\u0443\u0433\u0438\u0435" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e UPnP / IGD?" diff --git a/homeassistant/components/upnp/translations/zh-Hant.json b/homeassistant/components/upnp/translations/zh-Hant.json index ceb8dda3263..aad03a803b0 100644 --- a/homeassistant/components/upnp/translations/zh-Hant.json +++ b/homeassistant/components/upnp/translations/zh-Hant.json @@ -5,7 +5,7 @@ "incomplete_discovery": "\u672a\u5b8c\u6210\u63a2\u7d22", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, - "flow_title": "UPnP/IGD\uff1a{name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD \u88dd\u7f6e\uff1f" diff --git a/homeassistant/components/wilight/translations/ca.json b/homeassistant/components/wilight/translations/ca.json index 5920e54d258..0ace653e050 100644 --- a/homeassistant/components/wilight/translations/ca.json +++ b/homeassistant/components/wilight/translations/ca.json @@ -5,7 +5,7 @@ "not_supported_device": "Actualment aquest WiLight no \u00e9s compatible", "not_wilight_device": "Aquest dispositiu no \u00e9s WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Voleu configurar el WiLight {name}? \n\n Admet: {components}", diff --git a/homeassistant/components/wilight/translations/et.json b/homeassistant/components/wilight/translations/et.json index 9f3236ded62..1be23313837 100644 --- a/homeassistant/components/wilight/translations/et.json +++ b/homeassistant/components/wilight/translations/et.json @@ -5,7 +5,7 @@ "not_supported_device": "Seda WiLight'i sidumist ei toetata", "not_wilight_device": "See seade ei ole WiLight" }, - "flow_title": "", + "flow_title": "{name}", "step": { "confirm": { "description": "Kas soovid seadistada WiLight'i {name} ?\n\n See toetab: {components}", diff --git a/homeassistant/components/wilight/translations/no.json b/homeassistant/components/wilight/translations/no.json index 89cf91cbe63..170739145da 100644 --- a/homeassistant/components/wilight/translations/no.json +++ b/homeassistant/components/wilight/translations/no.json @@ -5,7 +5,7 @@ "not_supported_device": "Dette WiLight st\u00f8ttes forel\u00f8pig ikke", "not_wilight_device": "Denne enheten er ikke WiLight" }, - "flow_title": "", + "flow_title": "{name}", "step": { "confirm": { "description": "Vil du konfigurere WiLight {name} ? \n\n Den st\u00f8tter: {components}", diff --git a/homeassistant/components/wilight/translations/ru.json b/homeassistant/components/wilight/translations/ru.json index 9842b810f38..7d04a13518d 100644 --- a/homeassistant/components/wilight/translations/ru.json +++ b/homeassistant/components/wilight/translations/ru.json @@ -5,7 +5,7 @@ "not_supported_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "not_wilight_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 WiLight." }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c WiLight {name}? \n\n\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442: {components}", diff --git a/homeassistant/components/wilight/translations/zh-Hant.json b/homeassistant/components/wilight/translations/zh-Hant.json index fed2fb77904..fe6c36f21d2 100644 --- a/homeassistant/components/wilight/translations/zh-Hant.json +++ b/homeassistant/components/wilight/translations/zh-Hant.json @@ -5,7 +5,7 @@ "not_supported_device": "\u4e0d\u652f\u63f4\u6b64\u6b3e WiLight \u88dd\u7f6e\u3002", "not_wilight_device": "\u6b64\u88dd\u7f6e\u4e26\u975e WiLight" }, - "flow_title": "WiLight\uff1a{name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a WiLight {name}\uff1f\n\n\u652f\u63f4\uff1a{components}", diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index 2d299c659ef..b548735d426 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -12,7 +12,7 @@ "error": { "already_configured": "El compte ja ha estat configurat" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" diff --git a/homeassistant/components/withings/translations/et.json b/homeassistant/components/withings/translations/et.json index 3cb42f8cf7e..5395069522f 100644 --- a/homeassistant/components/withings/translations/et.json +++ b/homeassistant/components/withings/translations/et.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Konto on juba seadistatud" }, - "flow_title": "", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Vali tuvastusmeetod" diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index 2dd7407ad92..ff8d18eec64 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Kontoen er allerede konfigurert" }, - "flow_title": "", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Velg godkjenningsmetode" diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index 15462008965..7127f9545fa 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -12,7 +12,7 @@ "error": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" diff --git a/homeassistant/components/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json index cd917f42b47..2ee7ce3d3da 100644 --- a/homeassistant/components/withings/translations/zh-Hant.json +++ b/homeassistant/components/withings/translations/zh-Hant.json @@ -12,7 +12,7 @@ "error": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, - "flow_title": "Withings\uff1a{profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" diff --git a/homeassistant/components/wled/translations/ca.json b/homeassistant/components/wled/translations/ca.json index 512a7de3ca4..bbc5b5232cf 100644 --- a/homeassistant/components/wled/translations/ca.json +++ b/homeassistant/components/wled/translations/ca.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/et.json b/homeassistant/components/wled/translations/et.json index 9fb057a43d8..4771c0b3af4 100644 --- a/homeassistant/components/wled/translations/et.json +++ b/homeassistant/components/wled/translations/et.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/no.json b/homeassistant/components/wled/translations/no.json index a81683ce4c8..0e5df905e29 100644 --- a/homeassistant/components/wled/translations/no.json +++ b/homeassistant/components/wled/translations/no.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/ru.json b/homeassistant/components/wled/translations/ru.json index deef7e358f1..1aefafca5f1 100644 --- a/homeassistant/components/wled/translations/ru.json +++ b/homeassistant/components/wled/translations/ru.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json index 8841f15a425..0980bcf59aa 100644 --- a/homeassistant/components/wled/translations/zh-Hant.json +++ b/homeassistant/components/wled/translations/zh-Hant.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "WLED\uff1a{name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/ca.json b/homeassistant/components/xiaomi_aqara/translations/ca.json index 6c43f026e2a..46037fa5eea 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ca.json +++ b/homeassistant/components/xiaomi_aqara/translations/ca.json @@ -12,7 +12,7 @@ "invalid_key": "Clau de la passarel\u00b7la no v\u00e0lida", "invalid_mac": "Adre\u00e7a MAC no v\u00e0lida" }, - "flow_title": "Passarel\u00b7la Xiaomi Aqara: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/et.json b/homeassistant/components/xiaomi_aqara/translations/et.json index df00de81790..cc94b0e9b95 100644 --- a/homeassistant/components/xiaomi_aqara/translations/et.json +++ b/homeassistant/components/xiaomi_aqara/translations/et.json @@ -12,7 +12,7 @@ "invalid_key": "Vigane l\u00fc\u00fcsi v\u00f5ti", "invalid_mac": "Vigane MAC aadress" }, - "flow_title": "Xiaomi Aqara l\u00fc\u00fcs: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/no.json b/homeassistant/components/xiaomi_aqara/translations/no.json index 5a46d66fcf0..081b0e5e990 100644 --- a/homeassistant/components/xiaomi_aqara/translations/no.json +++ b/homeassistant/components/xiaomi_aqara/translations/no.json @@ -12,7 +12,7 @@ "invalid_key": "Ugyldig gateway-n\u00f8kkel", "invalid_mac": "Ugyldig MAC-adresse" }, - "flow_title": "", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index 4ede8019a4f..46b46e2beec 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -12,7 +12,7 @@ "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0448\u043b\u044e\u0437\u0430.", "invalid_mac": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 MAC-\u0430\u0434\u0440\u0435\u0441." }, - "flow_title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index 56c530682a3..26c49e82c16 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -12,7 +12,7 @@ "invalid_key": "\u7db2\u95dc\u5bc6\u9470\u7121\u6548", "invalid_mac": "\u7121\u6548\u7684 Mac \u4f4d\u5740" }, - "flow_title": "\u5c0f\u7c73 Aqara \u7db2\u95dc\uff1a{name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index f8dee0efe69..6ee0b1e16fd 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -9,7 +9,7 @@ "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un.", "unknown_device": "No es reconeix el model del dispositiu, no es pot configurar el dispositiu mitjan\u00e7ant el flux de configuraci\u00f3." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index bb1485d9fde..f5629a86eca 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "no_device_selected": "No device selected, please select one device.", "unknown_device": "The device model is not known, not able to setup the device using config flow." }, "flow_title": "{name}", @@ -14,10 +15,27 @@ "data": { "host": "IP Address", "model": "Device model (Optional)", + "name": "Name of the device", "token": "API Token" }, "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "gateway": { + "data": { + "host": "IP Address", + "name": "Name of the Gateway", + "token": "API Token" + }, + "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", + "title": "Connect to a Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Connect to a Xiaomi Gateway" + }, + "description": "Select to which device you want to connect.", + "title": "Xiaomi Miio" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json index a290f80ad31..acc03463883 100644 --- a/homeassistant/components/xiaomi_miio/translations/et.json +++ b/homeassistant/components/xiaomi_miio/translations/et.json @@ -9,7 +9,7 @@ "no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade.", "unknown_device": "Seadme mudel pole teada, seadet ei saa seadistamisvoo abil seadistada." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index 74a398a9ba6..e5ca4d2d004 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -9,7 +9,7 @@ "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet.", "unknown_device": "Enhetsmodellen er ikke kjent, kan ikke konfigurere enheten ved hjelp av konfigurasjonsflyt." }, - "flow_title": "", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index b17291746b3..ef729f33a1e 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -9,7 +9,7 @@ "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", "unknown_device": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0430\u0441\u0442\u0435\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index 8dc36f11f55..c79f6906a45 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -9,7 +9,7 @@ "no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002", "unknown_device": "\u88dd\u7f6e\u578b\u865f\u672a\u77e5\uff0c\u7121\u6cd5\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u3002" }, - "flow_title": "Xiaomi Miio\uff1a{name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index ce0d90564da..0320ea34f2c 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 3a0ac9dbb29..afbf2180ef6 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index bbf7ae8d229..087b4adb2c3 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 3b66c9da996..291f5c0eea1 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index ee52575dacd..43cc366ca35 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "ZHA\uff1a{name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zwave/translations/bg.json b/homeassistant/components/zwave/translations/bg.json index 191640ad0a0..ae82e98d705 100644 --- a/homeassistant/components/zwave/translations/bg.json +++ b/homeassistant/components/zwave/translations/bg.json @@ -12,8 +12,7 @@ "network_key": "\u041c\u0440\u0435\u0436\u043e\u0432 \u043a\u043b\u044e\u0447 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435)", "usb_path": "USB \u043f\u044a\u0442" }, - "description": "\u0412\u0438\u0436\u0442\u0435 https://www.home-assistant.io/docs/z-wave/installation/ \u0437\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0442\u043d\u043e\u0441\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u043b\u0438\u0432\u0438", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 Z-Wave" + "description": "\u0412\u0438\u0436\u0442\u0435 https://www.home-assistant.io/docs/z-wave/installation/ \u0437\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0442\u043d\u043e\u0441\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u043b\u0438\u0432\u0438" } } }, diff --git a/homeassistant/components/zwave/translations/ca.json b/homeassistant/components/zwave/translations/ca.json index 13805a2d1ed..9a5ef2b5010 100644 --- a/homeassistant/components/zwave/translations/ca.json +++ b/homeassistant/components/zwave/translations/ca.json @@ -13,8 +13,7 @@ "network_key": "Clau de xarxa (deixa-ho en blanc per generar-la autom\u00e0ticament)", "usb_path": "Ruta del port USB del dispositiu" }, - "description": "Aquesta integraci\u00f3 ja no s'actualitzar\u00e0. Utilitza Z-Wave JS per a instal\u00b7lacions noves.\n\nConsulta https://www.home-assistant.io/docs/z-wave/installation/ per a m\u00e9s informaci\u00f3 sobre les variables de configuraci\u00f3", - "title": "Configuraci\u00f3 de Z-Wave" + "description": "Aquesta integraci\u00f3 ja no s'actualitzar\u00e0. Utilitza Z-Wave JS per a instal\u00b7lacions noves.\n\nConsulta https://www.home-assistant.io/docs/z-wave/installation/ per a m\u00e9s informaci\u00f3 sobre les variables de configuraci\u00f3" } } }, diff --git a/homeassistant/components/zwave/translations/cs.json b/homeassistant/components/zwave/translations/cs.json index 40488adfe52..320feafe08a 100644 --- a/homeassistant/components/zwave/translations/cs.json +++ b/homeassistant/components/zwave/translations/cs.json @@ -13,8 +13,7 @@ "network_key": "S\u00ed\u0165ov\u00fd kl\u00ed\u010d (ponechte pr\u00e1zdn\u00e9 pro automatick\u00e9 generov\u00e1n\u00ed)", "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" }, - "description": "Viz https://www.home-assistant.io/docs/z-wave/installation/ pro informace o konfigura\u010dn\u00edch prom\u011bnn\u00fdch", - "title": "Nastavit Z-Wave" + "description": "Viz https://www.home-assistant.io/docs/z-wave/installation/ pro informace o konfigura\u010dn\u00edch prom\u011bnn\u00fdch" } } }, diff --git a/homeassistant/components/zwave/translations/da.json b/homeassistant/components/zwave/translations/da.json index effd2d5a98a..7bad51c4f8e 100644 --- a/homeassistant/components/zwave/translations/da.json +++ b/homeassistant/components/zwave/translations/da.json @@ -12,8 +12,7 @@ "network_key": "Netv\u00e6rksn\u00f8gle (efterlad blank for autogenerering)", "usb_path": "Sti til USB-enhed" }, - "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ for oplysninger om konfigurationsvariabler", - "title": "Ops\u00e6t Z-Wave" + "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ for oplysninger om konfigurationsvariabler" } } }, diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json index f592c2243ac..488580cb18a 100644 --- a/homeassistant/components/zwave/translations/de.json +++ b/homeassistant/components/zwave/translations/de.json @@ -13,8 +13,7 @@ "network_key": "Netzwerkschl\u00fcssel (leer lassen, um automatisch zu generieren)", "usb_path": "USB-Ger\u00e4t Pfad" }, - "description": "Informationen zu den Konfigurationsvariablen findest du unter https://www.home-assistant.io/docs/z-wave/installation/", - "title": "Z-Wave einrichten" + "description": "Informationen zu den Konfigurationsvariablen findest du unter https://www.home-assistant.io/docs/z-wave/installation/" } } }, diff --git a/homeassistant/components/zwave/translations/en.json b/homeassistant/components/zwave/translations/en.json index 2fe3e15646a..5c7442fea05 100644 --- a/homeassistant/components/zwave/translations/en.json +++ b/homeassistant/components/zwave/translations/en.json @@ -13,8 +13,7 @@ "network_key": "Network Key (leave blank to auto-generate)", "usb_path": "USB Device Path" }, - "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", - "title": "Set up Z-Wave" + "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables" } } }, diff --git a/homeassistant/components/zwave/translations/es-419.json b/homeassistant/components/zwave/translations/es-419.json index 0376714dd84..abcf85fffa1 100644 --- a/homeassistant/components/zwave/translations/es-419.json +++ b/homeassistant/components/zwave/translations/es-419.json @@ -12,8 +12,7 @@ "network_key": "Clave de red (dejar en blanco para auto-generar)", "usb_path": "Ruta USB" }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n", - "title": "Configurar Z-Wave" + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n" } } }, diff --git a/homeassistant/components/zwave/translations/es.json b/homeassistant/components/zwave/translations/es.json index 08408bf9d92..0d8b9a3020c 100644 --- a/homeassistant/components/zwave/translations/es.json +++ b/homeassistant/components/zwave/translations/es.json @@ -13,8 +13,7 @@ "network_key": "Clave de red (d\u00e9jelo en blanco para generar autom\u00e1ticamente)", "usb_path": "Ruta del dispositivo USB" }, - "description": "Consulta https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n", - "title": "Configurar Z-Wave" + "description": "Consulta https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n" } } }, diff --git a/homeassistant/components/zwave/translations/et.json b/homeassistant/components/zwave/translations/et.json index b1fa6127076..922126e0d86 100644 --- a/homeassistant/components/zwave/translations/et.json +++ b/homeassistant/components/zwave/translations/et.json @@ -13,8 +13,7 @@ "network_key": "V\u00f5rguv\u00f5ti (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)", "usb_path": "USB seadme rada" }, - "description": "Seda sidumist enam ei hallata. Uueks sidumiseks kasuta Z-Wave JS.\n\nKonfiguratsioonimuutujate kohta leiad teavet https://www.home-assistant.io/docs/z-wave/installation/", - "title": "Seadista Z-Wave" + "description": "Seda sidumist enam ei hallata. Uueks sidumiseks kasuta Z-Wave JS.\n\nKonfiguratsioonimuutujate kohta leiad teavet https://www.home-assistant.io/docs/z-wave/installation/" } } }, diff --git a/homeassistant/components/zwave/translations/fi.json b/homeassistant/components/zwave/translations/fi.json index 5cddea71d32..90fb77b49e1 100644 --- a/homeassistant/components/zwave/translations/fi.json +++ b/homeassistant/components/zwave/translations/fi.json @@ -4,8 +4,7 @@ "user": { "data": { "usb_path": "USB-polku" - }, - "title": "Z-Waven m\u00e4\u00e4ritt\u00e4minen" + } } } }, diff --git a/homeassistant/components/zwave/translations/fr.json b/homeassistant/components/zwave/translations/fr.json index 6c23d35ac4e..03f6f9823ad 100644 --- a/homeassistant/components/zwave/translations/fr.json +++ b/homeassistant/components/zwave/translations/fr.json @@ -13,8 +13,7 @@ "network_key": "Cl\u00e9 r\u00e9seau (laisser vide pour g\u00e9n\u00e9rer automatiquement)", "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" }, - "description": "Voir https://www.home-assistant.io/docs/z-wave/installation/ pour plus d'informations sur les variables de configuration.", - "title": "Configurer Z-Wave" + "description": "Voir https://www.home-assistant.io/docs/z-wave/installation/ pour plus d'informations sur les variables de configuration." } } }, diff --git a/homeassistant/components/zwave/translations/hu.json b/homeassistant/components/zwave/translations/hu.json index 68a19863b53..7269ee32daf 100644 --- a/homeassistant/components/zwave/translations/hu.json +++ b/homeassistant/components/zwave/translations/hu.json @@ -13,8 +13,7 @@ "network_key": "H\u00e1l\u00f3zati kulcs (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, - "description": "A konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3kr\u00f3l az inform\u00e1ci\u00f3kat l\u00e1sd a https://www.home-assistant.io/docs/z-wave/installation/ oldalon.", - "title": "Z-Wave be\u00e1ll\u00edt\u00e1sa" + "description": "A konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3kr\u00f3l az inform\u00e1ci\u00f3kat l\u00e1sd a https://www.home-assistant.io/docs/z-wave/installation/ oldalon." } } }, diff --git a/homeassistant/components/zwave/translations/id.json b/homeassistant/components/zwave/translations/id.json index 99bd6270326..91301f6e00e 100644 --- a/homeassistant/components/zwave/translations/id.json +++ b/homeassistant/components/zwave/translations/id.json @@ -13,8 +13,7 @@ "network_key": "Kunci Jaringan (biarkan kosong untuk dibuat secara otomatis)", "usb_path": "Jalur Perangkat USB" }, - "description": "Integrasi ini tidak lagi dipertahankan. Untuk instalasi baru, gunakan Z-Wave JS sebagai gantinya.\n\nBaca https://www.home-assistant.io/docs/z-wave/installation/ untuk informasi tentang variabel konfigurasi", - "title": "Siapkan Z-Wave" + "description": "Integrasi ini tidak lagi dipertahankan. Untuk instalasi baru, gunakan Z-Wave JS sebagai gantinya.\n\nBaca https://www.home-assistant.io/docs/z-wave/installation/ untuk informasi tentang variabel konfigurasi" } } }, diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json index d3522cf0889..a99cc241633 100644 --- a/homeassistant/components/zwave/translations/it.json +++ b/homeassistant/components/zwave/translations/it.json @@ -13,8 +13,7 @@ "network_key": "Chiave di rete (lascia vuoto per generare automaticamente)", "usb_path": "Percorso del dispositivo USB" }, - "description": "Questa integrazione non viene pi\u00f9 mantenuta. Per le nuove installazioni, usa invece Z-Wave JS. \n\nVedere https://www.home-assistant.io/docs/z-wave/installation/ per informazioni sulle variabili di configurazione", - "title": "Configura Z-Wave" + "description": "Questa integrazione non viene pi\u00f9 mantenuta. Per le nuove installazioni, usa invece Z-Wave JS. \n\nVedere https://www.home-assistant.io/docs/z-wave/installation/ per informazioni sulle variabili di configurazione" } } }, diff --git a/homeassistant/components/zwave/translations/ko.json b/homeassistant/components/zwave/translations/ko.json index 674476ac759..613b4108d22 100644 --- a/homeassistant/components/zwave/translations/ko.json +++ b/homeassistant/components/zwave/translations/ko.json @@ -13,8 +13,7 @@ "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" }, - "description": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ub354 \uc774\uc0c1 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \uc124\uce58\uc758 \uacbd\uc6b0 Z-Wave JS \ub97c \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.\n\n\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694", - "title": "Z-Wave \uc124\uc815" + "description": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ub354 \uc774\uc0c1 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \uc124\uce58\uc758 \uacbd\uc6b0 Z-Wave JS \ub97c \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.\n\n\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694" } } }, diff --git a/homeassistant/components/zwave/translations/lb.json b/homeassistant/components/zwave/translations/lb.json index d2359ff4c61..e41d37b2025 100644 --- a/homeassistant/components/zwave/translations/lb.json +++ b/homeassistant/components/zwave/translations/lb.json @@ -13,8 +13,7 @@ "network_key": "Netzwierk Schl\u00ebssel (eidel loossen fir een automatesch z'erstellen)", "usb_path": "Pad zum USB Apparat" }, - "description": "Lies op https://www.home-assistant.io/docs/z-wave/installation/ fir weider Informatiounen iwwert d'Konfiguratioun vun den Variabelen", - "title": "Z-Wave konfigur\u00e9ieren" + "description": "Lies op https://www.home-assistant.io/docs/z-wave/installation/ fir weider Informatiounen iwwert d'Konfiguratioun vun den Variabelen" } } }, diff --git a/homeassistant/components/zwave/translations/nl.json b/homeassistant/components/zwave/translations/nl.json index a366d1d50df..d8c58fe784c 100644 --- a/homeassistant/components/zwave/translations/nl.json +++ b/homeassistant/components/zwave/translations/nl.json @@ -13,8 +13,7 @@ "network_key": "Netwerksleutel (laat leeg om automatisch te genereren)", "usb_path": "USB-apparaatpad" }, - "description": "Deze integratie wordt niet langer onderhouden. Voor nieuwe installaties, gebruik Z-Wave JS in plaats daarvan.\n\nZie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen", - "title": "Stel Z-Wave in" + "description": "Deze integratie wordt niet langer onderhouden. Voor nieuwe installaties, gebruik Z-Wave JS in plaats daarvan.\n\nZie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen" } } }, diff --git a/homeassistant/components/zwave/translations/no.json b/homeassistant/components/zwave/translations/no.json index ab5a405f975..8582f906b57 100644 --- a/homeassistant/components/zwave/translations/no.json +++ b/homeassistant/components/zwave/translations/no.json @@ -13,8 +13,7 @@ "network_key": "Nettverksn\u00f8kkel (la v\u00e6re tom for automatisk oppretting)", "usb_path": "USB enhetsbane" }, - "description": "Denne integrasjonen opprettholdes ikke lenger. For nye installasjoner, bruk Z-Wave JS i stedet. \n\n Se https://www.home-assistant.io/docs/z-wave/installation/ for informasjon om konfigurasjonsvariablene", - "title": "Sett opp Z-Wave" + "description": "Denne integrasjonen opprettholdes ikke lenger. For nye installasjoner, bruk Z-Wave JS i stedet. \n\n Se https://www.home-assistant.io/docs/z-wave/installation/ for informasjon om konfigurasjonsvariablene" } } }, diff --git a/homeassistant/components/zwave/translations/pl.json b/homeassistant/components/zwave/translations/pl.json index 0a4b6a4828c..9706fb1721f 100644 --- a/homeassistant/components/zwave/translations/pl.json +++ b/homeassistant/components/zwave/translations/pl.json @@ -13,8 +13,7 @@ "network_key": "Klucz sieciowy (pozostaw pusty, by generowa\u0107 automatycznie)", "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" }, - "description": "Ta integracja nie jest ju\u017c wspierana. Dla nowych instalacji, u\u017cyj Z-Wave JS.\n\nPrzejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych", - "title": "Konfiguracja Z-Wave" + "description": "Ta integracja nie jest ju\u017c wspierana. Dla nowych instalacji, u\u017cyj Z-Wave JS.\n\nPrzejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych" } } }, diff --git a/homeassistant/components/zwave/translations/pt-BR.json b/homeassistant/components/zwave/translations/pt-BR.json index e46a1bb14cb..8c20db13830 100644 --- a/homeassistant/components/zwave/translations/pt-BR.json +++ b/homeassistant/components/zwave/translations/pt-BR.json @@ -12,8 +12,7 @@ "network_key": "Chave de rede (deixe em branco para gerar automaticamente)", "usb_path": "Caminho do USB" }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o", - "title": "Configurar o Z-Wave" + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o" } } }, diff --git a/homeassistant/components/zwave/translations/pt.json b/homeassistant/components/zwave/translations/pt.json index 74942081884..27fc303f08b 100644 --- a/homeassistant/components/zwave/translations/pt.json +++ b/homeassistant/components/zwave/translations/pt.json @@ -13,8 +13,7 @@ "network_key": "Network Key (deixe em branco para auto-gera\u00e7\u00e3o)", "usb_path": "Endere\u00e7o USB" }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o", - "title": "Configurar o Z-Wave" + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o" } } }, diff --git a/homeassistant/components/zwave/translations/ro.json b/homeassistant/components/zwave/translations/ro.json index aa644cde3fa..de199bea03b 100644 --- a/homeassistant/components/zwave/translations/ro.json +++ b/homeassistant/components/zwave/translations/ro.json @@ -12,8 +12,7 @@ "network_key": "Cheie de re\u021bea (l\u0103sa\u021bi necompletat pentru a genera automat)", "usb_path": "Cale USB" }, - "description": "Vede\u021bi https://www.home-assistant.io/docs/z-wave/installation/ pentru informa\u021bii despre variabilele de configurare", - "title": "Configura\u021bi Z-Wave" + "description": "Vede\u021bi https://www.home-assistant.io/docs/z-wave/installation/ pentru informa\u021bii despre variabilele de configurare" } } }, diff --git a/homeassistant/components/zwave/translations/ru.json b/homeassistant/components/zwave/translations/ru.json index 5188bb8330e..7b7f0c6733d 100644 --- a/homeassistant/components/zwave/translations/ru.json +++ b/homeassistant/components/zwave/translations/ru.json @@ -13,8 +13,7 @@ "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0451 Z-Wave JS.\n\n\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](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", - "title": "Z-Wave" + "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0451 Z-Wave JS.\n\n\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](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430." } } }, diff --git a/homeassistant/components/zwave/translations/sl.json b/homeassistant/components/zwave/translations/sl.json index f84f4c926ee..38e70c59652 100644 --- a/homeassistant/components/zwave/translations/sl.json +++ b/homeassistant/components/zwave/translations/sl.json @@ -12,8 +12,7 @@ "network_key": "Omre\u017eni klju\u010d (pustite prazno za samodejno generiranje)", "usb_path": "USB Pot" }, - "description": "Za informacije o konfiguracijskih spremenljivka si oglejte https://www.home-assistant.io/docs/z-wave/installation/", - "title": "Nastavite Z-Wave" + "description": "Za informacije o konfiguracijskih spremenljivka si oglejte https://www.home-assistant.io/docs/z-wave/installation/" } } }, diff --git a/homeassistant/components/zwave/translations/sv.json b/homeassistant/components/zwave/translations/sv.json index 5d5e5adc210..6d3af30a057 100644 --- a/homeassistant/components/zwave/translations/sv.json +++ b/homeassistant/components/zwave/translations/sv.json @@ -12,8 +12,7 @@ "network_key": "N\u00e4tverksnyckel (l\u00e4mna blank f\u00f6r automatisk generering)", "usb_path": "USB-s\u00f6kv\u00e4g" }, - "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ f\u00f6r information om konfigurationsvariabler", - "title": "St\u00e4lla in Z-Wave" + "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ f\u00f6r information om konfigurationsvariabler" } } }, diff --git a/homeassistant/components/zwave/translations/uk.json b/homeassistant/components/zwave/translations/uk.json index 5cdd6060cc4..696c0caccd2 100644 --- a/homeassistant/components/zwave/translations/uk.json +++ b/homeassistant/components/zwave/translations/uk.json @@ -13,8 +13,7 @@ "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" }, - "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", - "title": "Z-Wave" + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430." } } }, diff --git a/homeassistant/components/zwave/translations/zh-Hans.json b/homeassistant/components/zwave/translations/zh-Hans.json index c6a220617c1..64af99e21ff 100644 --- a/homeassistant/components/zwave/translations/zh-Hans.json +++ b/homeassistant/components/zwave/translations/zh-Hans.json @@ -12,8 +12,7 @@ "network_key": "\u7f51\u7edc\u5bc6\u94a5\uff08\u7559\u7a7a\u5c06\u81ea\u52a8\u751f\u6210\uff09", "usb_path": "USB \u8def\u5f84" }, - "description": "\u6709\u5173\u914d\u7f6e\u7684\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.home-assistant.io/docs/z-wave/installation/", - "title": "\u8bbe\u7f6e Z-Wave" + "description": "\u6709\u5173\u914d\u7f6e\u7684\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.home-assistant.io/docs/z-wave/installation/" } } }, diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json index d786a6b881e..4be9b77a8c6 100644 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ b/homeassistant/components/zwave/translations/zh-Hant.json @@ -13,8 +13,7 @@ "network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, - "description": "\u6b64\u6574\u5408\u5df2\u7d93\u4e0d\u518d\u9032\u884c\u7dad\u8b77\uff0c\u8acb\u4f7f\u7528 Z-Wave JS \u53d6\u4ee3\u70ba\u65b0\u5b89\u88dd\u65b9\u5f0f\u3002\n\n\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/ \u4ee5\n\u7372\u5f97\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a", - "title": "\u8a2d\u5b9a Z-Wave" + "description": "\u6b64\u6574\u5408\u5df2\u7d93\u4e0d\u518d\u9032\u884c\u7dad\u8b77\uff0c\u8acb\u4f7f\u7528 Z-Wave JS \u53d6\u4ee3\u70ba\u65b0\u5b89\u88dd\u65b9\u5f0f\u3002\n\n\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/ \u4ee5\n\u7372\u5f97\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a" } } }, diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json index abf89f00513..cc046c009d8 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -8,11 +8,6 @@ "data": { "url": "URL" } - }, - "user": { - "data": { - "url": "URL" - } } } } diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index 731c0bbcea8..af39281c06b 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "No s'ha pogut obtenir la informaci\u00f3 de descobriment del complement Z-Wave JS.", "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement Z-Wave JS.", "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement Z-Wave JS.", - "addon_missing_discovery_info": "Falta la informaci\u00f3 de descobriment del complement Z-Wave JS.", "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 de Z-Wave JS.", "addon_start_failed": "No s'ha pogut iniciar el complement Z-Wave JS.", "already_configured": "El dispositiu ja est\u00e0 configurat", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "El complement Z-Wave JS s'est\u00e0 iniciant." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json index 57e7a6b74db..9f8af44c451 100644 --- a/homeassistant/components/zwave_js/translations/cs.json +++ b/homeassistant/components/zwave_js/translations/cs.json @@ -19,11 +19,6 @@ "data": { "url": "URL" } - }, - "user": { - "data": { - "url": "URL" - } } } } diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index b0af15e6637..c4672112fe5 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Z-Wave-JS-Add-on-Discovery-Informationen konnten nicht abgerufen werden.", "addon_info_failed": "Fehler beim Abrufen von Z-Wave JS Add-on Informationen.", "addon_install_failed": "Installation des Z-Wave JS Add-Ons fehlgeschlagen.", - "addon_missing_discovery_info": "Fehlende Informationen zur Erkennung des Z-Wave JS-Add-Ons.", "addon_set_config_failed": "Setzen der Z-Wave JS Konfiguration fehlgeschlagen", "addon_start_failed": "Starten des Z-Wave JS Add-ons fehlgeschlagen.", "already_configured": "Ger\u00e4t ist bereits konfiguriert", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Z-Wave JS Add-on wird gestartet." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 5be980d52cb..101942dc717 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", "addon_info_failed": "Failed to get Z-Wave JS add-on info.", "addon_install_failed": "Failed to install the Z-Wave JS add-on.", - "addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.", "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", "addon_start_failed": "Failed to start the Z-Wave JS add-on.", "already_configured": "Device is already configured", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "The Z-Wave JS add-on is starting." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 26fd155a0ad..1e5b07ec171 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Fallo en la obtenci\u00f3n de la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento Z-Wave JS.", "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.", - "addon_missing_discovery_info": "Falta informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.", "already_configured": "El dispositivo ya est\u00e1 configurado", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Se est\u00e1 iniciando el complemento Z-Wave JS." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index 4c68e63530f..2ae0a0f47c6 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Z-Wave JS lisandmooduli tuvastusteabe hankimine nurjus.", "addon_info_failed": "Z-Wave JS lisandmooduli teabe hankimine nurjus.", "addon_install_failed": "Z-Wave JS lisandmooduli paigaldamine nurjus.", - "addon_missing_discovery_info": "Z-Wave JS lisandmooduli tuvastusteave puudub.", "addon_set_config_failed": "Z-Wave JS konfiguratsiooni m\u00e4\u00e4ramine nurjus.", "addon_start_failed": "Z-Wave JS-i lisandmooduli k\u00e4ivitamine nurjus.", "already_configured": "Seade on juba h\u00e4\u00e4lestatud", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Z-Wave JS lisandmoodul k\u00e4ivitub." - }, - "user": { - "data": { - "url": "" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index ce9f2f8b501..33571f12d60 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Impossible d'obtenir les informations de d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", "addon_info_failed": "Impossible d'obtenir les informations sur le module compl\u00e9mentaire Z-Wave JS.", "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire Z-Wave JS.", - "addon_missing_discovery_info": "Informations manquantes sur la d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", "addon_set_config_failed": "\u00c9chec de la d\u00e9finition de la configuration Z-Wave JS.", "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS.", "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Le module compl\u00e9mentaire Z-Wave JS est d\u00e9marr\u00e9." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 6732251f3a0..87629666b09 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -37,11 +37,6 @@ }, "start_addon": { "title": "Indul a Z-Wave JS b\u0151v\u00edtm\u00e9ny." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index e8ea9381544..046cdd59485 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Gagal mendapatkan info penemuan add-on Z-Wave JS.", "addon_info_failed": "Gagal mendapatkan info add-on Z-Wave JS.", "addon_install_failed": "Gagal menginstal add-on Z-Wave JS.", - "addon_missing_discovery_info": "Info penemuan add-on Z-Wave JS tidak ada.", "addon_set_config_failed": "Gagal menyetel konfigurasi Z-Wave JS.", "addon_start_failed": "Gagal memulai add-on Z-Wave JS.", "already_configured": "Perangkat sudah dikonfigurasi", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Add-on Z-Wave JS sedang dimulai." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index abe0ab066fb..f3005fc1651 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Impossibile ottenere le informazioni sul rilevamento del componente aggiuntivo Z-Wave JS.", "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo Z-Wave JS.", "addon_install_failed": "Impossibile installare il componente aggiuntivo Z-Wave JS.", - "addon_missing_discovery_info": "Informazioni sul rilevamento del componente aggiuntivo Z-Wave JS mancanti.", "addon_set_config_failed": "Impossibile impostare la configurazione di Z-Wave JS.", "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS.", "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Il componente aggiuntivo Z-Wave JS si sta avviando." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/ko.json b/homeassistant/components/zwave_js/translations/ko.json index 22149af7496..08e3dc8c7d7 100644 --- a/homeassistant/components/zwave_js/translations/ko.json +++ b/homeassistant/components/zwave_js/translations/ko.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uac80\uc0c9 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "addon_info_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "addon_install_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc124\uce58\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", - "addon_missing_discovery_info": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uac80\uc0c9 \uc815\ubcf4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", "addon_set_config_failed": "Z-Wave JS \uad6c\uc131\uc744 \uc124\uc815\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "addon_start_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Z-Wave JS \uc560\ub4dc\uc628\uc774 \uc2dc\uc791\ud558\ub294 \uc911\uc785\ub2c8\ub2e4." - }, - "user": { - "data": { - "url": "URL \uc8fc\uc18c" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/lb.json b/homeassistant/components/zwave_js/translations/lb.json index 302addbd7cf..d84c5323fb9 100644 --- a/homeassistant/components/zwave_js/translations/lb.json +++ b/homeassistant/components/zwave_js/translations/lb.json @@ -7,13 +7,6 @@ "cannot_connect": "Feeler beim verbannen", "invalid_ws_url": "Ong\u00eblteg Websocket URL", "unknown": "Onerwaarte Feeler" - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } } }, "title": "Z-Wave JS" diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index f50c9c8ceba..090733da15b 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Ophalen van ontdekkingsinformatie voor Z-Wave JS-add-on is mislukt.", "addon_info_failed": "Ophalen van Z-Wave JS add-on-info is mislukt.", "addon_install_failed": "Kan de Z-Wave JS add-on niet installeren.", - "addon_missing_discovery_info": "De Z-Wave JS addon mist ontdekkings informatie", "addon_set_config_failed": "Instellen van de Z-Wave JS configuratie is mislukt.", "addon_start_failed": "Kan de Z-Wave JS add-on niet starten.", "already_configured": "Apparaat is al geconfigureerd", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "De add-on Z-Wave JS wordt gestart." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index f893d2d7684..aa0fa2451aa 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Kunne ikke hente oppdagelsesinformasjon om Z-Wave JS-tillegg", "addon_info_failed": "Kunne ikke hente informasjon om Z-Wave JS-tillegg", "addon_install_failed": "Kunne ikke installere Z-Wave JS-tillegg", - "addon_missing_discovery_info": "Manglende oppdagelsesinformasjon for Z-Wave JS-tillegg", "addon_set_config_failed": "Kunne ikke angi Z-Wave JS-konfigurasjon", "addon_start_failed": "Kunne ikke starte Z-Wave JS-tillegget.", "already_configured": "Enheten er allerede konfigurert", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Z-Wave JS-tillegget starter" - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 9fbd66de4f2..b729e8db3da 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji wykrywania dodatku Z-Wave JS", "addon_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji o dodatku Z-Wave JS", "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku Z-Wave JS", - "addon_missing_discovery_info": "Brak informacji wykrywania dodatku Z-Wave JS", "addon_set_config_failed": "Nie uda\u0142o si\u0119 skonfigurowa\u0107 Z-Wave JS", "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku Z-Wave JS.", "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Dodatek Z-Wave JS uruchamia si\u0119..." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 1a65ce3ea71..b0b3745fac4 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS.", "addon_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.", "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", - "addon_missing_discovery_info": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.", "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e Z-Wave JS.", "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f" - }, - "user": { - "data": { - "url": "URL-\u0430\u0434\u0440\u0435\u0441" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index 04ddcc5252c..10c9c54a98b 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Z-Wave JS eklenti ke\u015fif bilgileri al\u0131namad\u0131.", "addon_info_failed": "Z-Wave JS eklenti bilgileri al\u0131namad\u0131.", "addon_install_failed": "Z-Wave JS eklentisi y\u00fcklenemedi.", - "addon_missing_discovery_info": "Eksik Z-Wave JS eklenti bulma bilgileri.", "addon_set_config_failed": "Z-Wave JS yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", @@ -44,11 +43,6 @@ }, "description": "Z-Wave JS Supervisor eklentisini kullanmak istiyor musunuz?", "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/uk.json b/homeassistant/components/zwave_js/translations/uk.json index f5ff5224347..fe77655ac29 100644 --- a/homeassistant/components/zwave_js/translations/uk.json +++ b/homeassistant/components/zwave_js/translations/uk.json @@ -7,13 +7,6 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", "invalid_ws_url": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0441\u043e\u043a\u0435\u0442\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" - }, - "step": { - "user": { - "data": { - "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430" - } - } } }, "title": "Z-Wave JS" diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index d35c9e8a260..827c0b54e90 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", "addon_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", - "addon_missing_discovery_info": "\u7f3a\u5c11 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u3002", "addon_set_config_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", "addon_start_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5931\u6557\u3002", "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002" - }, - "user": { - "data": { - "url": "\u7db2\u5740" - } } } }, From 4e08d22a7461174149007af64469dd36b0abc467 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 19:23:09 -0500 Subject: [PATCH 349/852] Fix dhcp generated conflict (#50498) --- homeassistant/generated/dhcp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 74287c3a9e4..8bae6243f68 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -66,6 +66,10 @@ DHCP = [ "domain": "flume", "hostname": "flume-gw-*" }, + { + "domain": "gogogate2", + "hostname": "ismartgate*" + }, { "domain": "guardian", "hostname": "gvc*", @@ -76,10 +80,6 @@ DHCP = [ "hostname": "guardian*", "macaddress": "30AEA4*" }, - { - "domain": "gogogate2", - "hostname": "ismartgate*" - }, { "domain": "hunterdouglas_powerview", "hostname": "hunter*", From c037ebb27c87ce197d3216478c2380c947756ab9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 19:55:50 -0500 Subject: [PATCH 350/852] Add discovery to yeelight (#50385) --- homeassistant/components/yeelight/__init__.py | 168 +++++++++++------- .../components/yeelight/config_flow.py | 75 ++++++-- .../components/yeelight/manifest.json | 8 +- .../components/yeelight/strings.json | 4 + .../components/yeelight/translations/en.json | 4 + homeassistant/generated/dhcp.py | 4 + homeassistant/generated/zeroconf.py | 1 + tests/components/yeelight/test_config_flow.py | 107 ++++++++++- tests/components/yeelight/test_init.py | 81 ++++++++- 9 files changed, 367 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 845e8e5711a..352dddb7705 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol from yeelight import Bulb, BulbException, discover_bulbs -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -48,8 +48,8 @@ DATA_CONFIG_ENTRIES = "config_entries" DATA_CUSTOM_EFFECTS = "custom_effects" DATA_SCAN_INTERVAL = "scan_interval" DATA_DEVICE = "device" -DATA_UNSUB_UPDATE_LISTENER = "unsub_update_listener" DATA_REMOVE_INIT_DISPATCHER = "remove_init_dispatcher" +DATA_PLATFORMS_LOADED = "platforms_loaded" ATTR_COUNT = "count" ATTR_ACTION = "action" @@ -179,81 +179,115 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Yeelight from a config entry.""" +async def _async_initialize( + hass: HomeAssistant, + entry: ConfigEntry, + host: str, + device: YeelightDevice | None = None, +) -> None: + entry_data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id] = { + DATA_PLATFORMS_LOADED: False + } + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - async def _initialize(host: str, capabilities: dict | None = None) -> None: - remove_dispatcher = async_dispatcher_connect( - hass, - DEVICE_INITIALIZED.format(host), - _load_platforms, - ) - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][ - DATA_REMOVE_INIT_DISPATCHER - ] = remove_dispatcher - - device = await _async_get_device(hass, host, entry, capabilities) - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device - - await device.async_setup() - - async def _load_platforms(): + @callback + def _async_load_platforms(): + if entry_data[DATA_PLATFORMS_LOADED]: + return + entry_data[DATA_PLATFORMS_LOADED] = True hass.config_entries.async_setup_platforms(entry, PLATFORMS) - # Move options from data for imported entries - # Initialize options with default values for other entries - if not entry.options: - hass.config_entries.async_update_entry( - entry, - data={ - CONF_HOST: entry.data.get(CONF_HOST), - CONF_ID: entry.data.get(CONF_ID), - }, - options={ - CONF_NAME: entry.data.get(CONF_NAME, ""), - CONF_MODEL: entry.data.get(CONF_MODEL, ""), - CONF_TRANSITION: entry.data.get(CONF_TRANSITION, DEFAULT_TRANSITION), - CONF_MODE_MUSIC: entry.data.get(CONF_MODE_MUSIC, DEFAULT_MODE_MUSIC), - CONF_SAVE_ON_CHANGE: entry.data.get( - CONF_SAVE_ON_CHANGE, DEFAULT_SAVE_ON_CHANGE - ), - CONF_NIGHTLIGHT_SWITCH: entry.data.get( - CONF_NIGHTLIGHT_SWITCH, DEFAULT_NIGHTLIGHT_SWITCH - ), - }, - ) + if not device: + device = await _async_get_device(hass, host, entry) + entry_data[DATA_DEVICE] = device - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id] = { - DATA_UNSUB_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener) - } + entry.async_on_unload( + async_dispatcher_connect( + hass, + DEVICE_INITIALIZED.format(host), + _async_load_platforms, + ) + ) + + entry.async_on_unload(device.async_unload) + await device.async_setup() + + +@callback +def _async_populate_entry_options(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Move options from data for imported entries. + + Initialize options with default values for other entries. + """ + if entry.options: + return + + hass.config_entries.async_update_entry( + entry, + data={ + CONF_HOST: entry.data.get(CONF_HOST), + CONF_ID: entry.data.get(CONF_ID), + }, + options={ + CONF_NAME: entry.data.get(CONF_NAME, ""), + CONF_MODEL: entry.data.get(CONF_MODEL, ""), + CONF_TRANSITION: entry.data.get(CONF_TRANSITION, DEFAULT_TRANSITION), + CONF_MODE_MUSIC: entry.data.get(CONF_MODE_MUSIC, DEFAULT_MODE_MUSIC), + CONF_SAVE_ON_CHANGE: entry.data.get( + CONF_SAVE_ON_CHANGE, DEFAULT_SAVE_ON_CHANGE + ), + CONF_NIGHTLIGHT_SWITCH: entry.data.get( + CONF_NIGHTLIGHT_SWITCH, DEFAULT_NIGHTLIGHT_SWITCH + ), + }, + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Yeelight from a config entry.""" + _async_populate_entry_options(hass, entry) if entry.data.get(CONF_HOST): - # manually added device - await _initialize(entry.data[CONF_HOST]) - else: - # discovery - scanner = YeelightScanner.async_get(hass) - scanner.async_register_callback(entry.data[CONF_ID], _initialize) + try: + device = await _async_get_device(hass, entry.data[CONF_HOST], entry) + except OSError as ex: + # If CONF_ID is not valid we cannot fallback to discovery + # so we must retry by raising ConfigEntryNotReady + if not entry.data.get(CONF_ID): + raise ConfigEntryNotReady from ex + # Otherwise fall through to discovery + else: + # manually added device + await _async_initialize(hass, entry, entry.data[CONF_HOST], device=device) + return True + # discovery + scanner = YeelightScanner.async_get(hass) + + async def _async_from_discovery(host: str) -> None: + await _async_initialize(hass, entry, host) + + scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES].pop(entry.entry_id) - remove_init_dispatcher = data.get(DATA_REMOVE_INIT_DISPATCHER) - if remove_init_dispatcher is not None: - remove_init_dispatcher() - data[DATA_UNSUB_UPDATE_LISTENER]() - data[DATA_DEVICE].async_unload() - if entry.data[CONF_ID]: - # discovery - scanner = YeelightScanner.async_get(hass) - scanner.async_unregister_callback(entry.data[CONF_ID]) + data_config_entries = hass.data[DOMAIN][DATA_CONFIG_ENTRIES] + entry_data = data_config_entries[entry.entry_id] - return unload_ok + if entry_data[DATA_PLATFORMS_LOADED]: + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + return False + + if entry.data.get(CONF_ID): + # discovery + scanner = YeelightScanner.async_get(hass) + scanner.async_unregister_callback(entry.data[CONF_ID]) + + data_config_entries.pop(entry.entry_id) + + return True @callback @@ -582,16 +616,12 @@ async def _async_get_device( hass: HomeAssistant, host: str, entry: ConfigEntry, - capabilities: dict | None, ) -> YeelightDevice: # Get model from config and capabilities model = entry.options.get(CONF_MODEL) - if not model and capabilities is not None: - model = capabilities.get("model") # Set up device bulb = Bulb(host, model=model or None) - if capabilities is None: - capabilities = await hass.async_add_executor_job(bulb.get_capabilities) + capabilities = await hass.async_add_executor_job(bulb.get_capabilities) return YeelightDevice(hass, host, entry.options, bulb, capabilities) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 3c794792ac3..d6902abcf5a 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -5,6 +5,7 @@ import voluptuous as vol import yeelight from homeassistant import config_entries, exceptions +from homeassistant.components.dhcp import IP_ADDRESS from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -21,6 +22,8 @@ from . import ( _async_unique_name, ) +MODEL_UNKNOWN = "unknown" + _LOGGER = logging.getLogger(__name__) @@ -38,22 +41,69 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" self._discovered_devices = {} + self._discovered_model = None + self._discovered_ip = None + + async def async_step_homekit(self, discovery_info): + """Handle discovery from homekit.""" + self._discovered_ip = discovery_info["host"] + return await self._async_handle_discovery() + + async def async_step_dhcp(self, discovery_info): + """Handle discovery from dhcp.""" + self._discovered_ip = discovery_info[IP_ADDRESS] + return await self._async_handle_discovery() + + async def _async_handle_discovery(self): + """Handle any discovery.""" + self.context[CONF_HOST] = self._discovered_ip + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == self._discovered_ip: + return self.async_abort(reason="already_in_progress") + + self._discovered_model = await self._async_try_connect(self._discovered_ip) + if not self.unique_id: + return self.async_abort(reason="cannot_connect") + + self._abort_if_unique_id_configured( + updates={CONF_HOST: self._discovered_ip}, reload_on_update=False + ) + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm(self, user_input=None): + """Confirm discovery.""" + if user_input is not None: + return self.async_create_entry( + title=f"{self._discovered_model} {self.unique_id}", + data={CONF_ID: self.unique_id, CONF_HOST: self._discovered_ip}, + ) + + self._set_confirm_only() + placeholders = { + "model": self._discovered_model, + "host": self._discovered_ip, + } + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="discovery_confirm", description_placeholders=placeholders + ) async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: - if user_input.get(CONF_HOST): - try: - await self._async_try_connect(user_input[CONF_HOST]) - return self.async_create_entry( - title=user_input[CONF_HOST], - data=user_input, - ) - except CannotConnect: - errors["base"] = "cannot_connect" - else: + if not user_input.get(CONF_HOST): return await self.async_step_pick_device() + try: + model = await self._async_try_connect(user_input[CONF_HOST]) + except CannotConnect: + errors["base"] = "cannot_connect" + else: + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"{model} {self.unique_id}", + data=user_input, + ) user_input = user_input or {} return self.async_show_form( @@ -117,6 +167,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input.pop(CONF_NIGHTLIGHT_SWITCH_TYPE) == NIGHTLIGHT_SWITCH_TYPE_LIGHT ) + self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) async def _async_try_connect(self, host): @@ -131,8 +182,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: _LOGGER.debug("Get capabilities: %s", capabilities) await self.async_set_unique_id(capabilities["id"]) - self._abort_if_unique_id_configured() - return + return capabilities["model"] except OSError as err: _LOGGER.debug("Failed to get capabilities from %s: %s", host, err) # Ignore the error since get_capabilities uses UDP discovery packet @@ -145,6 +195,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("Failed to get properties from %s: %s", host, err) raise CannotConnect from err _LOGGER.debug("Get properties: %s", bulb.last_properties) + return MODEL_UNKNOWN class OptionsFlowHandler(config_entries.OptionsFlow): diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 8e5288efb81..9d82a4fe56e 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -5,5 +5,11 @@ "requirements": ["yeelight==0.6.2"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "dhcp": [{ + "hostname": "yeelink-*" + }], + "homekit": { + "models": ["YLDP*"] + } } diff --git a/homeassistant/components/yeelight/strings.json b/homeassistant/components/yeelight/strings.json index 52a684bc26f..807fae1ca64 100644 --- a/homeassistant/components/yeelight/strings.json +++ b/homeassistant/components/yeelight/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{model} {host}", "step": { "user": { "description": "If you leave the host empty, discovery will be used to find devices.", @@ -11,6 +12,9 @@ "data": { "device": "Device" } + }, + "discovery_confirm": { + "description": "Do you want to setup {model} ({host})?" } }, "error": { diff --git a/homeassistant/components/yeelight/translations/en.json b/homeassistant/components/yeelight/translations/en.json index 218f82f86b7..06431e7bc2b 100644 --- a/homeassistant/components/yeelight/translations/en.json +++ b/homeassistant/components/yeelight/translations/en.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Failed to connect" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Do you want to setup {model} ({host})?" + }, "pick_device": { "data": { "device": "Device" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 8bae6243f68..9da371090f5 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -343,5 +343,9 @@ DHCP = [ { "domain": "verisure", "macaddress": "0023C1*" + }, + { + "domain": "yeelight", + "hostname": "yeelink-*" } ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 8b6075d9e3b..3b801bc6ddd 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -240,6 +240,7 @@ HOMEKIT = { "Touch HD": "rainmachine", "Welcome": "netatmo", "Wemo": "wemo", + "YLDP*": "yeelight", "iSmartGate": "gogogate2", "iZone": "izone", "tado": "tado" diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 6a1508d7896..8cc49c9799a 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -1,7 +1,9 @@ """Test the Yeelight config flow.""" from unittest.mock import MagicMock, patch -from homeassistant import config_entries +import pytest + +from homeassistant import config_entries, setup from homeassistant.components.yeelight import ( CONF_MODE_MUSIC, CONF_MODEL, @@ -19,6 +21,7 @@ from homeassistant.components.yeelight import ( ) from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from . import ( ID, @@ -205,7 +208,7 @@ async def test_manual(hass: HomeAssistant): ) await hass.async_block_till_done() assert result4["type"] == "create_entry" - assert result4["title"] == IP_ADDRESS + assert result4["title"] == "color 0x000000000015243f" assert result4["data"] == {CONF_HOST: IP_ADDRESS} # Duplicate @@ -286,3 +289,103 @@ async def test_manual_no_capabilities(hass: HomeAssistant): type(mocked_bulb).get_properties.assert_called_once() assert result["type"] == "create_entry" assert result["data"] == {CONF_HOST: IP_ADDRESS} + + +async def test_discovered_by_homekit_and_dhcp(hass): + """Test we get the form with homekit and abort for dhcp source when we get both.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_bulb = _mocked_bulb() + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"host": "1.2.3.4", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.4", "macaddress": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.4", "macaddress": "00:00:00:00:00:00"}, + ) + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_in_progress" + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + {"ip": IP_ADDRESS, "macaddress": "aa:bb:cc:dd:ee:ff"}, + ), + ( + config_entries.SOURCE_HOMEKIT, + {"host": IP_ADDRESS, "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ), + ], +) +async def test_discovered_by_dhcp_or_homekit(hass, source, data): + """Test we can setup when discovered from dhcp or homekit.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_bulb = _mocked_bulb() + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data=data, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch(f"{MODULE}.async_setup", return_value=True) as mock_async_setup, patch( + f"{MODULE}.async_setup_entry", + return_value=True, + ) as mock_async_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result2["type"] == "create_entry" + assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f"} + assert mock_async_setup.called + assert mock_async_setup_entry.called + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + {"ip": IP_ADDRESS, "macaddress": "aa:bb:cc:dd:ee:ff"}, + ), + ( + config_entries.SOURCE_HOMEKIT, + {"host": IP_ADDRESS, "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ), + ], +) +async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data): + """Test we abort if we cannot get the unique id when discovered from dhcp or homekit.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_bulb = _mocked_bulb() + type(mocked_bulb).get_capabilities = MagicMock(return_value=None) + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data=data, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index b6a59809d30..3c25852810d 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -11,7 +11,14 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME, STATE_UNAVAILABLE +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_ID, + CONF_NAME, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -35,6 +42,77 @@ from . import ( from tests.common import MockConfigEntry +async def test_ip_changes_fallback_discovery(hass: HomeAssistant): + """Test Yeelight ip changes and we fallback to discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ID: ID, + CONF_HOST: "5.5.5.5", + }, + unique_id=ID, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb(True) + mocked_bulb.bulb_type = BulbType.WhiteTempMood + mocked_bulb.get_capabilities = MagicMock( + side_effect=[OSError, CAPABILITIES, CAPABILITIES] + ) + + _discovered_devices = [ + { + "capabilities": CAPABILITIES, + "ip": IP_ADDRESS, + } + ] + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + f"{MODULE}.discover_bulbs", return_value=_discovered_devices + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( + f"yeelight_color_{ID}" + ) + entity_registry = er.async_get(hass) + assert entity_registry.async_get(binary_sensor_entity_id) is None + + await hass.async_block_till_done() + + type(mocked_bulb).get_properties = MagicMock(None) + + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.async_block_till_done() + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get(binary_sensor_entity_id) is not None + + +async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): + """Test Yeelight ip changes and we fallback to discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "5.5.5.5", + }, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb(True) + mocked_bulb.bulb_type = BulbType.WhiteTempMood + mocked_bulb.get_capabilities = MagicMock( + side_effect=[OSError, CAPABILITIES, CAPABILITIES] + ) + + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_RETRY + + async def test_setup_discovery(hass: HomeAssistant): """Test setting up Yeelight by discovery.""" config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) @@ -182,6 +260,7 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() await hass.async_block_till_done() + await hass.async_block_till_done() entity_registry = er.async_get(hass) assert entity_registry.async_get(binary_sensor_entity_id) is not None From dd3965e4e2b2a2f18d03c09fda7e3336d8ce8531 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 23:24:42 -0500 Subject: [PATCH 351/852] Ensure zeroconf does not generate config flows when matching attributes are missing (#50208) If macaddress, name, or manufacturer were missing from the discovery info, the matcher would accept instead of reject. --- homeassistant/components/zeroconf/__init__.py | 29 ++++++++--------- tests/components/zeroconf/test_init.py | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index bf717141f11..484d4404a66 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -348,32 +348,29 @@ async def _async_start_zeroconf_browser( # Not all homekit types are currently used for discovery # so not all service type exist in zeroconf_types - for entry in zeroconf_types.get(service_type, []): - if len(entry) > 1: - if ( - uppercase_mac is not None - and "macaddress" in entry - and not fnmatch.fnmatch(uppercase_mac, entry["macaddress"]) + for matcher in zeroconf_types.get(service_type, []): + if len(matcher) > 1: + if "macaddress" in matcher and ( + uppercase_mac is None + or not fnmatch.fnmatch(uppercase_mac, matcher["macaddress"]) ): continue - if ( - lowercase_name is not None - and "name" in entry - and not fnmatch.fnmatch(lowercase_name, entry["name"]) + if "name" in matcher and ( + lowercase_name is None + or not fnmatch.fnmatch(lowercase_name, matcher["name"]) ): continue - if ( - lowercase_manufacturer is not None - and "manufacturer" in entry - and not fnmatch.fnmatch( - lowercase_manufacturer, entry["manufacturer"] + if "manufacturer" in matcher and ( + lowercase_manufacturer is None + or not fnmatch.fnmatch( + lowercase_manufacturer, matcher["manufacturer"] ) ): continue hass.add_job( hass.config_entries.flow.async_init( - entry["domain"], context={"source": DOMAIN}, data=info + matcher["domain"], context={"source": DOMAIN}, data=info ) # type: ignore ) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 0ad15c947cf..809177b6089 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -346,6 +346,38 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "samsungtv" +async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf): + """Test matchers reject when a property is missing.""" + + def http_only_service_update_mock(zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_airplay._tcp.local.", + "s1000._airplay._tcp.local.", + ServiceStateChange.Added, + ) + + with patch.dict( + zc_gen.ZEROCONF, + {"_airplay._tcp.local.": [{"domain": "samsungtv", "manufacturer": "samsung*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( + "aa:bb:cc:dd:ee:ff" + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 0 + + async def test_zeroconf_no_match(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" From b8d468717a6fed8b77a4ced85a7b87c62a6603be Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 12 May 2021 16:52:22 +1200 Subject: [PATCH 352/852] Bump python-juicenet package to 1.0.2 (#50505) --- homeassistant/components/juicenet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 4b0c946c53a..d56977dc9df 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -2,7 +2,7 @@ "domain": "juicenet", "name": "JuiceNet", "documentation": "https://www.home-assistant.io/integrations/juicenet", - "requirements": ["python-juicenet==1.0.1"], + "requirements": ["python-juicenet==1.0.2"], "codeowners": ["@jesserockz"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 10c32dbdf43..a129fc2e787 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1813,7 +1813,7 @@ python-izone==1.1.4 python-join-api==0.0.6 # homeassistant.components.juicenet -python-juicenet==1.0.1 +python-juicenet==1.0.2 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 110d77282fc..ac4b0601756 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -989,7 +989,7 @@ python-forecastio==1.4.0 python-izone==1.1.4 # homeassistant.components.juicenet -python-juicenet==1.0.1 +python-juicenet==1.0.2 # homeassistant.components.xiaomi_miio python-miio==0.5.6 From 6cb283d36bf36246a0637bdf860a536e9ee1a5b2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 12 May 2021 01:05:45 -0400 Subject: [PATCH 353/852] Fix vizio integration (#50436) --- homeassistant/components/vizio/__init__.py | 20 ++++++++++-- tests/components/vizio/conftest.py | 10 ++++++ tests/components/vizio/test_init.py | 36 +++++++++++++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index bec6b803023..d51b6a4fbca 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_APPS, CONF_DEVICE_CLASS, DOMAIN, VIZIO_SCHEMA @@ -106,10 +106,26 @@ class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator): update_method=self._async_update_data, ) self.data = APPS + self.fail_count = 0 + self.fail_threshold = 10 async def _async_update_data(self) -> list[dict[str, Any]]: """Update data via library.""" data = await gen_apps_list_from_url(session=async_get_clientsession(self.hass)) if not data: - raise UpdateFailed + if self.fail_count == self.fail_threshold: + _LOGGER.warning( + ( + "Unable to retrieve the apps list from the external server " + "for the last %s days" + ), + self.fail_threshold, + ) + self.fail_count = 0 + self.fail_threshold += 10 + else: + self.fail_count += 1 + return self.data + self.fail_count = 0 + self.fail_threshold = 10 return sorted(data, key=lambda app: app["name"]) diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 8124827dbf0..2c32b07cc1a 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -68,6 +68,16 @@ def vizio_data_coordinator_update_fixture(): yield +@pytest.fixture(name="vizio_data_coordinator_update_failure") +def vizio_data_coordinator_update_failure_fixture(): + """Mock get data coordinator update failure.""" + with patch( + "homeassistant.components.vizio.gen_apps_list_from_url", + return_value=None, + ): + yield + + @pytest.fixture(name="vizio_no_unique_id") def vizio_no_unique_id_fixture(): """Mock no vizio unique ID returrned.""" diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index 16e2a5bb769..c3e8afe49e6 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -1,4 +1,6 @@ """Tests for Vizio init.""" +from datetime import timedelta + import pytest from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN @@ -6,10 +8,11 @@ from homeassistant.components.vizio.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util from .const import MOCK_SPEAKER_CONFIG, MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_setup_component( @@ -71,3 +74,34 @@ async def test_speaker_load_and_unload( for entity in entities: assert hass.states.get(entity).state == STATE_UNAVAILABLE assert DOMAIN not in hass.data + + +async def test_coordinator_update_failure( + hass: HomeAssistant, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, + vizio_data_coordinator_update_failure: pytest.fixture, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test coordinator update failure after 10 days.""" + now = dt_util.now() + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert DOMAIN in hass.data + + for days in range(1, 10): + async_fire_time_changed(hass, now + timedelta(days=days)) + await hass.async_block_till_done() + assert ( + "Unable to retrieve the apps list from the external server" + not in caplog.text + ) + + async_fire_time_changed(hass, now + timedelta(days=10)) + await hass.async_block_till_done() + assert "Unable to retrieve the apps list from the external server" in caplog.text From 5ed252ebfa600f7ca409cb2ff8f38be725bc81af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 May 2021 22:15:36 -0700 Subject: [PATCH 354/852] Bump aiohue to 2.3.1 (#50506) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index de00d31f2c7..76d02d2b9fc 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.3.0"], + "requirements": ["aiohue==2.3.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index a129fc2e787..862dba2f420 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.3.0 +aiohue==2.3.1 # homeassistant.components.imap aioimaplib==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac4b0601756..ccb08e35bfc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.3.0 +aiohue==2.3.1 # homeassistant.components.apache_kafka aiokafka==0.6.0 From b35f229674b2fc4e7985276b49a92650b98e4c6b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 12 May 2021 07:27:11 +0200 Subject: [PATCH 355/852] Include _StopScript.__cause__ in trace (#50441) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/script.py | 3 +++ tests/helpers/test_script.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 5a7fdcd1767..1deb5a5073f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -193,6 +193,9 @@ async def trace_action(hass, script_run, stop, variables): try: yield trace_element + except _StopScript as ex: + trace_element.set_error(ex.__cause__ or ex) + raise ex except Exception as ex: trace_element.set_error(ex) raise ex diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a0207edcbdd..2045f8cdbbc 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -622,7 +622,7 @@ async def test_delay_template_invalid(hass, caplog): assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{"error_type": script._StopScript}], + "1": [{"error_type": vol.MultipleInvalid}], }, expected_script_execution="aborted", ) @@ -683,7 +683,7 @@ async def test_delay_template_complex_invalid(hass, caplog): assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{"error_type": script._StopScript}], + "1": [{"error_type": vol.MultipleInvalid}], }, expected_script_execution="aborted", ) @@ -1138,7 +1138,7 @@ async def test_wait_continue_on_timeout( } if continue_on_timeout is False: expected_trace["0"][0]["result"]["timeout"] = True - expected_trace["0"][0]["error_type"] = script._StopScript + expected_trace["0"][0]["error_type"] = asyncio.TimeoutError expected_script_execution = "aborted" else: expected_trace["1"] = [ From 2cdf075f952a0263d9ac2a6e995a22d0c53b6cb8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 May 2021 23:54:04 -0700 Subject: [PATCH 356/852] Only return empty string if non-fixable errors (#50508) --- script/hassfest/mypy_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 0bc97412bc0..4e75fa6e33e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -326,7 +326,7 @@ def generate_and_validate(config: Config) -> str: config.add_error("mypy_config", f"Module '{module} doesn't exist") # Don't generate mypy.ini if there're errors found because it will likely crash. - if config.errors: + if any(not err.fixable for err in config.errors): return "" mypy_config = configparser.ConfigParser() From 34cd68bdf6cc404cd157cf73ad3f89096465fe30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 09:41:30 +0200 Subject: [PATCH 357/852] Bump actions/checkout from 2 to 2.3.4 (#50510) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 2.3.4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v2.3.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 +++++------ .github/workflows/ci.yaml | 38 +++++++++++++++++------------------ .github/workflows/wheels.yml | 6 +++--- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 32ea439b830..db63bc37df4 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -23,7 +23,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 with: fetch-depth: 0 @@ -54,7 +54,7 @@ jobs: if: needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 @@ -84,7 +84,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' @@ -151,7 +151,7 @@ jobs: - tinker steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Login to DockerHub uses: docker/login-action@v1 @@ -182,7 +182,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -214,7 +214,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Login to DockerHub uses: docker/login-action@v1 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 50b346b7843..fe14e44beed 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,7 +26,7 @@ jobs: pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v2.2.2 @@ -84,7 +84,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -124,7 +124,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -164,7 +164,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -207,7 +207,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Register hadolint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/hadolint.json" @@ -226,7 +226,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -269,7 +269,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -312,7 +312,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -352,7 +352,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -395,7 +395,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -436,7 +436,7 @@ jobs: # needs: prepare-base # steps: # - name: Check out code from GitHub - # uses: actions/checkout@v2 + # uses: actions/checkout@v2.3.4 # - name: Run ShellCheck # uses: ludeeus/action-shellcheck@0.3.0 @@ -446,7 +446,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -493,7 +493,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.5 @@ -517,7 +517,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -551,7 +551,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Generate partial Python venv restore key id: generate-python-key run: >- @@ -595,7 +595,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.5 @@ -626,7 +626,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.5 @@ -660,7 +660,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.5 @@ -718,7 +718,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.5 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 7c83ecec786..949c628689d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -21,7 +21,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Get information id: info @@ -68,7 +68,7 @@ jobs: - "3.9-alpine3.13" steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Download env_file uses: actions/download-artifact@v2 @@ -109,7 +109,7 @@ jobs: - "3.9-alpine3.13" steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Download env_file uses: actions/download-artifact@v2 From 41f3c67be9d8c742e49e84019a8d9fc9a4d0cdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 12 May 2021 10:03:15 +0200 Subject: [PATCH 358/852] Pin wheels build version (#50515) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 949c628689d..6e6184e6d5d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -81,7 +81,7 @@ jobs: name: requirements_diff - name: Build wheels - uses: home-assistant/wheels@master + uses: home-assistant/wheels@2021.05.1 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} @@ -151,7 +151,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@master + uses: home-assistant/wheels@2021.05.1 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} From c79e86439430a3343109a9a377e1d32400aed129 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 12 May 2021 10:03:27 +0200 Subject: [PATCH 359/852] Ditch secret to wheels server (#50516) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6e6184e6d5d..c9a9030935d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -85,7 +85,7 @@ jobs: with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} - wheels-host: ${{ secrets.WHEELS_HOST }} + wheels-host: wheels.hass.io wheels-key: ${{ secrets.WHEELS_KEY }} wheels-user: wheels env-file: true @@ -155,7 +155,7 @@ jobs: with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} - wheels-host: ${{ secrets.WHEELS_HOST }} + wheels-host: wheels.hass.io wheels-key: ${{ secrets.WHEELS_KEY }} wheels-user: wheels env-file: true From e090581e3c673e8c60bd57afa51ac87be032454a Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Wed, 12 May 2021 11:26:12 +0300 Subject: [PATCH 360/852] Add stop for demo players (#50485) --- homeassistant/components/demo/media_player.py | 9 +++++++++ tests/components/demo/test_media_player.py | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index ea315707dea..cc02ecba175 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, @@ -66,6 +67,7 @@ YOUTUBE_PLAYER_SUPPORT = ( | SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOUND_MODE | SUPPORT_SEEK + | SUPPORT_STOP ) MUSIC_PLAYER_SUPPORT = ( @@ -83,6 +85,7 @@ MUSIC_PLAYER_SUPPORT = ( | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOUND_MODE + | SUPPORT_STOP ) NETFLIX_PLAYER_SUPPORT = ( @@ -95,6 +98,7 @@ NETFLIX_PLAYER_SUPPORT = ( | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOUND_MODE + | SUPPORT_STOP ) @@ -199,6 +203,11 @@ class AbstractDemoPlayer(MediaPlayerEntity): self._player_state = STATE_PAUSED self.schedule_update_ha_state() + def media_stop(self): + """Send stop command.""" + self._player_state = STATE_OFF + self.schedule_update_ha_state() + def set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" self._shuffle = shuffle diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 4ed41ce9c83..9cc01d18b03 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -400,6 +400,26 @@ async def test_seek(hass, mock_media_seek): assert mock_media_seek.called +async def test_stop(hass): + """Test stop.""" + assert await async_setup_component( + hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_PLAYING + + await hass.services.async_call( + mp.DOMAIN, + mp.SERVICE_MEDIA_STOP, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF + + async def test_media_image_proxy(hass, hass_client): """Test the media server image proxy server .""" assert await async_setup_component( From e6b4b803e30ff0290544eba84aaf1600a008dc18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 10:43:38 +0200 Subject: [PATCH 361/852] Bump docker/login-action from 1 to 1.9.0 (#50509) Bumps [docker/login-action](https://github.com/docker/login-action) from 1 to 1.9.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v1...v1.9.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index db63bc37df4..657f8b7f15a 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -102,13 +102,13 @@ jobs: version="$(python setup.py -V)" - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v1.9.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v1.9.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -154,13 +154,13 @@ jobs: uses: actions/checkout@v2.3.4 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v1.9.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v1.9.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -217,13 +217,13 @@ jobs: uses: actions/checkout@v2.3.4 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v1.9.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v1.9.0 with: registry: ghcr.io username: ${{ github.repository_owner }} From 9e0730c97f4b22188f4ab0b4f7f4ac74afeca0f6 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 12 May 2021 05:00:32 -0400 Subject: [PATCH 362/852] Add targets and selectors for services (F) (#50191) --- .../components/facebox/services.yaml | 14 ++++++ .../components/fastdotcom/services.yaml | 1 + homeassistant/components/ffmpeg/services.yaml | 24 +++++++-- .../components/filesize/services.yaml | 1 + homeassistant/components/filter/services.yaml | 1 + homeassistant/components/flo/services.yaml | 50 +++++++++++++------ homeassistant/components/foscam/services.yaml | 44 ++++++++++++---- .../components/foursquare/services.yaml | 42 +++++++++++++--- .../components/freebox/services.yaml | 2 +- 9 files changed, 144 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/facebox/services.yaml b/homeassistant/components/facebox/services.yaml index caa2e7df2c6..9c89c5e5c41 100644 --- a/homeassistant/components/facebox/services.yaml +++ b/homeassistant/components/facebox/services.yaml @@ -1,12 +1,26 @@ teach_face: + name: Teach face description: Teach facebox a face using a file. fields: entity_id: + name: Entity description: The facebox entity to teach. example: "image_processing.facebox" + selector: + entity: + integration: facebox + domain: image_processing name: + name: Name description: The name of the face to teach. + required: true example: "my_name" + selector: + text: file_path: + name: File path description: The path to the image file. + required: true example: "/images/my_image.jpg" + selector: + text: diff --git a/homeassistant/components/fastdotcom/services.yaml b/homeassistant/components/fastdotcom/services.yaml index 3664f9ece9f..75963557a03 100644 --- a/homeassistant/components/fastdotcom/services.yaml +++ b/homeassistant/components/fastdotcom/services.yaml @@ -1,2 +1,3 @@ speedtest: + name: Speed test description: Immediately execute a speed test with Fast.com diff --git a/homeassistant/components/ffmpeg/services.yaml b/homeassistant/components/ffmpeg/services.yaml index 15afa82ed0a..a00a820ebc8 100644 --- a/homeassistant/components/ffmpeg/services.yaml +++ b/homeassistant/components/ffmpeg/services.yaml @@ -1,18 +1,36 @@ restart: + name: Restart description: Send a restart command to a ffmpeg based sensor. fields: entity_id: - description: Name(s) of entities that will restart. Platform dependent. + name: Entity + description: Name of entity that will restart. Platform dependent. example: binary_sensor.ffmpeg_noise + selector: + entity: + integration: ffmpeg + domain: binary_sensor start: + name: Start description: Send a start command to a ffmpeg based sensor. fields: entity_id: - description: Name(s) of entities that will start. Platform dependent. + name: Entity + description: Name of entity that will start. Platform dependent. example: binary_sensor.ffmpeg_noise + selector: + entity: + integration: ffmpeg + domain: binary_sensor stop: + name: Stop description: Send a stop command to a ffmpeg based sensor. fields: entity_id: - description: Name(s) of entities that will stop. Platform dependent. + name: Entity + description: Name of entity that will stop. Platform dependent. example: binary_sensor.ffmpeg_noise + selector: + entity: + integration: ffmpeg + domain: binary_sensor diff --git a/homeassistant/components/filesize/services.yaml b/homeassistant/components/filesize/services.yaml index 9f251b50e7c..a794303f8f1 100644 --- a/homeassistant/components/filesize/services.yaml +++ b/homeassistant/components/filesize/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all filesize entities. diff --git a/homeassistant/components/filter/services.yaml b/homeassistant/components/filter/services.yaml index 7d64b34a4f7..431c73616ce 100644 --- a/homeassistant/components/filter/services.yaml +++ b/homeassistant/components/filter/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all filter entities diff --git a/homeassistant/components/flo/services.yaml b/homeassistant/components/flo/services.yaml index b5797020ac0..237fb4a7bf9 100644 --- a/homeassistant/components/flo/services.yaml +++ b/homeassistant/components/flo/services.yaml @@ -1,32 +1,52 @@ # Describes the format for available Flo services set_sleep_mode: + name: Set sleep mode description: Set the location into sleep mode. + target: + entity: + integration: flo + domain: switch fields: - entity_id: - description: Flo switch entity id - example: "switch.shutoff_valve" sleep_minutes: + name: Sleep minutes description: The time to sleep in minutes. + default: true example: 120 + selector: + select: + options: + - '120' + - '1440' + - '4320' revert_to_mode: + name: Revert to mode description: The mode to revert to after sleep_minutes has elapsed. + default: true example: "home" + selector: + select: + options: + - 'away' + - 'home' set_away_mode: + name: Set away mode description: Set the location into away mode. - fields: - entity_id: - description: Flo switch entity id - example: "switch.shutoff_valve" + target: + entity: + integration: flo + domain: switch set_home_mode: + name: Set home mode description: Set the location into home mode. - fields: - entity_id: - description: Flo switch entity id - example: "switch.shutoff_valve" + target: + entity: + integration: flo + domain: switch run_health_test: + name: Run health test description: Have the Flo device run a health test. - fields: - entity_id: - description: Flo switch entity id - example: "switch.shutoff_valve" + target: + entity: + integration: flo + domain: switch diff --git a/homeassistant/components/foscam/services.yaml b/homeassistant/components/foscam/services.yaml index 41563635f68..61326d0e8b6 100644 --- a/homeassistant/components/foscam/services.yaml +++ b/homeassistant/components/foscam/services.yaml @@ -1,22 +1,48 @@ ptz: + name: PTZ description: Pan/Tilt service for Foscam camera. + target: + entity: + integration: foscam + domain: camera fields: - entity_id: - description: Name(s) of entities to move. - example: "camera.living_room_camera" movement: - description: "Direction of the movement. Allowed values: up, down, left, right, top_left, top_right, bottom_left, bottom_right." + description: "Direction of the movement." + required: true example: "up" + selector: + select: + options: + - 'bottom_left' + - 'bottom_right' + - 'down' + - 'left' + - 'right' + - 'top_left' + - 'top_right' + - 'up' travel_time: - description: "(Optional) Travel time in seconds. Allowed values: float from 0 to 1. Default: 0.125" + description: "Travel time in seconds." example: 0.125 + default: 0.125 + selector: + number: + min: 0 + max: 1 + step: 0.005 + unit_of_measurement: seconds ptz_preset: + name: PTZ preset description: PTZ Preset service for Foscam camera. + target: + entity: + integration: foscam + domain: camera fields: - entity_id: - description: Name(s) of entities to move. - example: "camera.living_room_camera" preset_name: - description: "The name of the preset to move to. Presets can be created from within the official Foscam apps." + description: "The name of the preset to move to. Presets can be created from within the official Foscam apps." + required: true example: "TopMost" + selector: + text: diff --git a/homeassistant/components/foursquare/services.yaml b/homeassistant/components/foursquare/services.yaml index 0fcc077c7d3..5e103caeb01 100644 --- a/homeassistant/components/foursquare/services.yaml +++ b/homeassistant/components/foursquare/services.yaml @@ -1,33 +1,52 @@ checkin: + name: Check in description: Check a user into a Foursquare venue. fields: alt: - description: Altitude of the user's location, in meters. [Optional] + name: Altitude + description: Altitude of the user's location, in meters. example: 0 + selector: + text: altAcc: + name: Altitude accuracy description: Vertical accuracy of the user's location, in meters. example: 1 + selector: + text: broadcast: + name: Broadcast description: >- Who to broadcast this check-in to. Accepts a comma-delimited list of values: private (off the grid) or public (share with friends), facebook share on facebook, twitter share on twitter, followers share with followers (celebrity mode users only), If no valid value is found, the default is public. - [Optional] example: "public,twitter" + selector: + text: eventId: - description: The event the user is checking in to. [Optional] + name: Event ID + description: The event the user is checking in to. example: UHR8THISVNT + selector: + text: ll: + name: Latitude/Longitude description: >- Latitude and longitude of the user's location. Only specify this field if you have a GPS or other device reported location for the user - at the time of check-in. [Optional] + at the time of check-in. example: "33.7,44.2" + selector: + text: llAcc: - description: Accuracy of the user's latitude and longitude, in meters. [Optional] + name: Latitude/Longitude accuracy + description: Accuracy of the user's latitude and longitude, in meters. example: 1 + selector: + text: mentions: + name: Mentions description: >- Mentions in your check-in. This parameter is a semicolon-delimited list of mentions. A single mention is of the form "start,end,userid", where @@ -37,9 +56,18 @@ checkin: "fbu-", this indicates a Facebook userid that is being mention. Character indices in shouts are 0-based. [Optional] example: "5,10,HZXXY3Y;15,20,GZYYZ3Z;25,30,fbu-GZXY13Y" + selector: + text: shout: - description: A message about your check-in. The maximum length of this field is 140 characters. [Optional] + name: Shout + description: A message about your check-in. The maximum length of this field is 140 characters. example: There are crayons! Crayons! + selector: + text: venueId: - description: The Foursquare venue where the user is checking in. [Required] + name: Venue ID + description: The Foursquare venue where the user is checking in. + required: true example: IHR8THISVNU + selector: + text: diff --git a/homeassistant/components/freebox/services.yaml b/homeassistant/components/freebox/services.yaml index be7afa60562..7b2a4059434 100644 --- a/homeassistant/components/freebox/services.yaml +++ b/homeassistant/components/freebox/services.yaml @@ -1,5 +1,5 @@ # Freebox service entries description. reboot: - # Description of the service + name: Reboot description: Reboots the Freebox. From 2918929cf0a34b0e1ace74fd3d25821d3eda614f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 12 May 2021 11:55:31 +0200 Subject: [PATCH 363/852] Remove azure-pipelines-ci.yml (#50519) --- azure-pipelines-ci.yml | 232 ----------------------------------------- 1 file changed, 232 deletions(-) delete mode 100644 azure-pipelines-ci.yml diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml deleted file mode 100644 index cda5943ecd0..00000000000 --- a/azure-pipelines-ci.yml +++ /dev/null @@ -1,232 +0,0 @@ -# https://dev.azure.com/home-assistant - -trigger: - batch: true - branches: - include: - - rc - - dev - - master -pr: - - rc - - dev - - master - -resources: - containers: - - container: 38 - image: homeassistant/ci-azure:3.8 - repositories: - - repository: azure - type: github - name: "home-assistant/ci-azure" - endpoint: "home-assistant" -variables: - - name: PythonMain - value: "38" - - name: versionHadolint - value: "v1.17.6" - -stages: - - stage: "Overview" - jobs: - - job: "Lint" - pool: - vmImage: "ubuntu-latest" - container: $[ variables['PythonMain'] ] - steps: - - template: templates/azp-step-cache.yaml@azure - parameters: - keyfile: "requirements_test.txt | homeassistant/package_constraints.txt" - build: | - python -m venv venv - - . venv/bin/activate - pip install -r requirements_test.txt - pre-commit install-hooks - - script: | - . venv/bin/activate - pre-commit run --hook-stage manual check-executables-have-shebangs --all-files - displayName: "Run executables check" - - script: | - . venv/bin/activate - pre-commit run codespell --all-files - displayName: "Run codespell" - - script: | - . venv/bin/activate - pre-commit run flake8 --all-files - displayName: "Run flake8" - - script: | - . venv/bin/activate - pre-commit run bandit --all-files - displayName: "Run bandit" - - script: | - . venv/bin/activate - pre-commit run isort --all-files --show-diff-on-failure - displayName: "Run isort" - - script: | - . venv/bin/activate - pre-commit run check-json --all-files - displayName: "Run check-json" - - script: | - . venv/bin/activate - pre-commit run yamllint --all-files - displayName: "Run yamllint" - - script: | - . venv/bin/activate - pre-commit run pyupgrade --all-files --show-diff-on-failure - displayName: "Run pyupgrade" - # Prettier seems to hang on Azure, unknown why yet. - # Temporarily disable the check to no block PRs - # - script: | - # . venv/bin/activate - # pre-commit run prettier --all-files --show-diff-on-failure - # displayName: 'Run prettier' - - job: "Validate" - pool: - vmImage: "ubuntu-latest" - container: $[ variables['PythonMain'] ] - steps: - - template: templates/azp-step-cache.yaml@azure - parameters: - keyfile: "homeassistant/package_constraints.txt" - build: | - python -m venv venv - - . venv/bin/activate - pip install -e . - - script: | - . venv/bin/activate - python -m script.hassfest --action validate - displayName: "Validate manifests" - - script: | - . venv/bin/activate - ./script/gen_requirements_all.py validate - displayName: "requirements_all validate" - - job: "CheckFormat" - pool: - vmImage: "ubuntu-latest" - container: $[ variables['PythonMain'] ] - steps: - - template: templates/azp-step-cache.yaml@azure - parameters: - keyfile: "requirements_test.txt | homeassistant/package_constraints.txt" - build: | - python -m venv venv - - . venv/bin/activate - pip install -r requirements_test.txt - pre-commit install-hooks - - script: | - . venv/bin/activate - pre-commit run black --all-files --show-diff-on-failure - displayName: "Check Black formatting" - - job: "Docker" - pool: - vmImage: "ubuntu-latest" - steps: - - script: sudo docker pull hadolint/hadolint:$(versionHadolint) - displayName: "Install Hadolint" - - script: | - set -e - for dockerfile in Dockerfile Dockerfile.dev - do - echo "Linting: $dockerfile" - docker run --rm -i \ - -v "$(pwd)/.hadolint.yaml:/.hadolint.yaml:ro" \ - hadolint/hadolint:$(versionHadolint) < "$dockerfile" - done - displayName: "Run Hadolint" - - - stage: "Tests" - dependsOn: - - "Overview" - jobs: - - job: "PyTest" - pool: - vmImage: "ubuntu-latest" - strategy: - maxParallel: 3 - matrix: - Python38: - python.container: "38" - container: $[ variables['python.container'] ] - steps: - - template: templates/azp-step-cache.yaml@azure - parameters: - keyfile: "requirements_test_all.txt | requirements_test.txt | homeassistant/package_constraints.txt" - build: | - set -e - python -m venv venv - - . venv/bin/activate - pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt - pip install -r requirements_test_all.txt - - script: | - . venv/bin/activate - pip install -e . - displayName: "Install Home Assistant" - - script: | - set -e - - . venv/bin/activate - pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar tests - script/check_dirty - displayName: "Run pytest for python $(python.container)" - condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) - - script: | - set -e - - . venv/bin/activate - pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests - codecov --token $(codecovToken) - script/check_dirty - displayName: "Run pytest for python $(python.container) / coverage" - condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) - - - stage: "FullCheck" - dependsOn: - - "Overview" - jobs: - - job: "Pylint" - pool: - vmImage: "ubuntu-latest" - container: $[ variables['PythonMain'] ] - steps: - - template: templates/azp-step-cache.yaml@azure - parameters: - keyfile: "requirements_all.txt | requirements_test.txt | homeassistant/package_constraints.txt" - build: | - set -e - python -m venv venv - - . venv/bin/activate - pip install -U pip setuptools wheel - pip install -r requirements_all.txt - pip install -r requirements_test.txt - - script: | - . venv/bin/activate - pip install -e . - displayName: "Install Home Assistant" - - script: | - . venv/bin/activate - pylint homeassistant - displayName: "Run pylint" - - job: "Mypy" - pool: - vmImage: "ubuntu-latest" - container: $[ variables['PythonMain'] ] - steps: - - template: templates/azp-step-cache.yaml@azure - parameters: - keyfile: "requirements_test.txt | setup.py | homeassistant/package_constraints.txt" - build: | - python -m venv venv - - . venv/bin/activate - pip install -e . -r requirements_test.txt - pre-commit install-hooks - - script: | - . venv/bin/activate - pre-commit run mypy --all-files - displayName: "Run mypy" From c3eee9800af59563dfa0b4b2bc6b4c229b9cd387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 12 May 2021 12:03:13 +0200 Subject: [PATCH 364/852] Move translations from Azure to GitHub action (#50518) --- .github/workflows/translations.yaml | 64 ++++++++++++++++++++++++++++ azure-pipelines-translation.yml | 65 ----------------------------- 2 files changed, 64 insertions(+), 65 deletions(-) create mode 100644 .github/workflows/translations.yaml delete mode 100644 azure-pipelines-translation.yml diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml new file mode 100644 index 00000000000..86f11724d46 --- /dev/null +++ b/.github/workflows/translations.yaml @@ -0,0 +1,64 @@ +name: Translations + +# yamllint disable-line rule:truthy +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + push: + branches: + - dev + paths: + - "**strings.json" + +env: + DEFAULT_PYTHON: 3.8 + +jobs: + upload: + name: Upload + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2.2.2 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Upload Translations + run: | + export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}" + python3 -m script.translations upload + + download: + name: Download + needs: upload + if: github.event_name == 'schedule' || github.event_name == "workflow_dispatch" + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2.2.2 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Download Translations + run: | + export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}" + python3 -m script.translations download + + - name: Initialize git + uses: home-assistant/actions/helpers/git-init@master + with: + name: GitHub Action + email: github-action@users.noreply.github.com + + - name: Update translation + run: | + git add homeassistant + git commit -am "[ci skip] Translation update" + git push diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml deleted file mode 100644 index 481b98bc484..00000000000 --- a/azure-pipelines-translation.yml +++ /dev/null @@ -1,65 +0,0 @@ -# https://dev.azure.com/home-assistant - -trigger: - batch: true - branches: - include: - - dev -pr: none -schedules: - - cron: "0 0 * * *" - displayName: "translation update" - branches: - include: - - dev - always: true -variables: -- group: translation -resources: - repositories: - - repository: azure - type: github - name: 'home-assistant/ci-azure' - endpoint: 'home-assistant' - - -jobs: - -- job: 'Upload' - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.8' - inputs: - versionSpec: '3.8' - - script: | - export LOKALISE_TOKEN="$(lokaliseToken)" - export AZURE_BRANCH="$(Build.SourceBranchName)" - - python3 -m script.translations upload - displayName: 'Upload Translation' - -- job: 'Download' - dependsOn: - - 'Upload' - condition: or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual')) - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' - inputs: - versionSpec: '3.7' - - template: templates/azp-step-git-init.yaml@azure - - script: | - export LOKALISE_TOKEN="$(lokaliseToken)" - - python3 -m script.translations download - displayName: 'Download Translation' - - script: | - git checkout dev - git add homeassistant - git commit -am "[ci skip] Translation update" - git push - displayName: 'Update translation' From a4ea9b3cd39609a87f22173fa14f4fc8cb38dbbf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 May 2021 05:47:06 -0500 Subject: [PATCH 365/852] Update usage of async_entries to use _async_current_entries (#50187) --- homeassistant/components/awair/config_flow.py | 2 +- homeassistant/components/blebox/config_flow.py | 2 +- homeassistant/components/dynalite/config_flow.py | 2 +- homeassistant/components/fritzbox/config_flow.py | 2 +- homeassistant/components/glances/config_flow.py | 11 +---------- homeassistant/components/hlk_sw16/config_flow.py | 14 ++++---------- homeassistant/components/hlk_sw16/errors.py | 4 ---- homeassistant/components/iaqualink/config_flow.py | 2 +- homeassistant/components/life360/config_flow.py | 2 +- homeassistant/components/litejet/config_flow.py | 2 +- .../components/logi_circle/config_flow.py | 2 +- homeassistant/components/mikrotik/config_flow.py | 2 +- homeassistant/components/mysensors/config_flow.py | 6 +++--- homeassistant/components/nest/config_flow.py | 8 ++++---- homeassistant/components/omnilogic/config_flow.py | 2 +- .../components/opentherm_gw/config_flow.py | 2 +- homeassistant/components/point/config_flow.py | 8 ++++---- homeassistant/components/ps4/config_flow.py | 2 +- homeassistant/components/soma/config_flow.py | 2 +- homeassistant/components/somfy/config_flow.py | 2 +- .../components/tellduslive/config_flow.py | 4 ++-- .../components/transmission/config_flow.py | 2 +- homeassistant/components/upnp/config_flow.py | 2 +- homeassistant/components/vesync/__init__.py | 3 +-- homeassistant/components/vesync/config_flow.py | 8 +------- homeassistant/components/vizio/config_flow.py | 2 +- homeassistant/components/withings/config_flow.py | 2 +- tests/components/hlk_sw16/test_config_flow.py | 5 ++--- 28 files changed, 40 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 3854909bc86..2214fc30519 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -19,7 +19,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_import(self, conf: dict): """Import a configuration from config.yaml.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="already_setup") user, error = await self._check_connection(conf[CONF_ACCESS_TOKEN]) diff --git a/homeassistant/components/blebox/config_flow.py b/homeassistant/components/blebox/config_flow.py index d310232f776..17dffe154d1 100644 --- a/homeassistant/components/blebox/config_flow.py +++ b/homeassistant/components/blebox/config_flow.py @@ -91,7 +91,7 @@ class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): addr = host_port(user_input) - for entry in hass.config_entries.async_entries(DOMAIN): + for entry in self._async_current_entries(): if addr == host_port(entry.data): host, port = addr return self.async_abort( diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py index 9b2b76318f1..e1d062a6058 100644 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -23,7 +23,7 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Import a new bridge as a config entry.""" LOGGER.debug("Starting async_step_import - %s", import_info) host = import_info[CONF_HOST] - for entry in self.hass.config_entries.async_entries(DOMAIN): + for entry in self._async_current_entries(): if entry.data[CONF_HOST] == host: if entry.data != import_info: self.hass.config_entries.async_update_entry(entry, data=import_info) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index ecbcfa0bf68..50a9a5f0117 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -128,7 +128,7 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_in_progress") # update old and user-configured config entries - for entry in self.hass.config_entries.async_entries(DOMAIN): + for entry in self._async_current_entries(): if entry.data[CONF_HOST] == host: if uuid and not entry.unique_id: self.hass.config_entries.async_update_entry(entry, unique_id=uuid) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index 981c3727b8e..f00f3ca42f8 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -43,10 +43,6 @@ DATA_SCHEMA = vol.Schema( async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == data[CONF_HOST]: - raise AlreadyConfigured - if data[CONF_VERSION] not in SUPPORTED_VERSIONS: raise WrongVersion try: @@ -71,13 +67,12 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) try: await validate_input(self.hass, user_input) return self.async_create_entry( title=user_input[CONF_NAME], data=user_input ) - except AlreadyConfigured: - return self.async_abort(reason="already_configured") except CannotConnect: errors["base"] = "cannot_connect" except WrongVersion: @@ -121,9 +116,5 @@ class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" -class AlreadyConfigured(exceptions.HomeAssistantError): - """Error to indicate host is already configured.""" - - class WrongVersion(exceptions.HomeAssistantError): """Error to indicate the selected version is wrong.""" diff --git a/homeassistant/components/hlk_sw16/config_flow.py b/homeassistant/components/hlk_sw16/config_flow.py index 6f4e6ce708c..ca65647a448 100644 --- a/homeassistant/components/hlk_sw16/config_flow.py +++ b/homeassistant/components/hlk_sw16/config_flow.py @@ -15,7 +15,7 @@ from .const import ( DEFAULT_RECONNECT_INTERVAL, DOMAIN, ) -from .errors import AlreadyConfigured, CannotConnect +from .errors import CannotConnect DATA_SCHEMA = vol.Schema( { @@ -40,13 +40,6 @@ async def connect_client(hass, user_input): async def validate_input(hass: HomeAssistant, user_input): """Validate the user input allows us to connect.""" - for entry in hass.config_entries.async_entries(DOMAIN): - if ( - entry.data[CONF_HOST] == user_input[CONF_HOST] - and entry.data[CONF_PORT] == user_input[CONF_PORT] - ): - raise AlreadyConfigured - try: client = await connect_client(hass, user_input) except asyncio.TimeoutError as err: @@ -81,12 +74,13 @@ class SW16FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) try: await validate_input(self.hass, user_input) address = f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}" return self.async_create_entry(title=address, data=user_input) - except AlreadyConfigured: - errors["base"] = "already_configured" except CannotConnect: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/hlk_sw16/errors.py b/homeassistant/components/hlk_sw16/errors.py index 5b29587deba..87453737531 100644 --- a/homeassistant/components/hlk_sw16/errors.py +++ b/homeassistant/components/hlk_sw16/errors.py @@ -6,9 +6,5 @@ class SW16Exception(HomeAssistantError): """Base class for HLK-SW16 exceptions.""" -class AlreadyConfigured(SW16Exception): - """HLK-SW16 is already configured.""" - - class CannotConnect(SW16Exception): """Unable to connect to the HLK-SW16.""" diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index 235b45ca5af..96c82cd2c76 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -19,7 +19,7 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input: ConfigType | None = None): """Handle a flow start.""" # Supporting a single account. - entries = self.hass.config_entries.async_entries(DOMAIN) + entries = self._async_current_entries() if entries: return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index 78ec938586d..0a200e72097 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -30,7 +30,7 @@ class Life360ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @property def configured_usernames(self): """Return tuple of configured usernames.""" - entries = self.hass.config_entries.async_entries(DOMAIN) + entries = self._async_current_entries() if entries: return (entry.data[CONF_USERNAME] for entry in entries) return () diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index a4de9d883e4..2e63c150e41 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -24,7 +24,7 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Create a LiteJet config entry based upon user input.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") errors = {} diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index ff0fd83ff5b..d61de1ea017 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -95,7 +95,7 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_auth(self, user_input=None): """Create an entry for auth.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="external_setup") external_error = self.hass.data[DATA_FLOW_IMPL][DOMAIN][EXTERNAL_ERRORS] diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index 7208c96d18f..922df221d5a 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -40,7 +40,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: - for entry in self.hass.config_entries.async_entries(DOMAIN): + for entry in self._async_current_entries(): if entry.data[CONF_HOST] == user_input[CONF_HOST]: return self.async_abort(reason="already_configured") if entry.data[CONF_NAME] == user_input[CONF_NAME]: diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 847408abcc5..6676e11febf 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -218,7 +218,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="gw_tcp", data_schema=schema, errors=errors) def _check_topic_exists(self, topic: str) -> bool: - for other_config in self.hass.config_entries.async_entries(DOMAIN): + for other_config in self._async_current_entries(): if topic == other_config.data.get( CONF_TOPIC_IN_PREFIX ) or topic == other_config.data.get(CONF_TOPIC_OUT_PREFIX): @@ -329,7 +329,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ] = self._normalize_persistence_file( user_input[CONF_PERSISTENCE_FILE] ) - for other_entry in self.hass.config_entries.async_entries(DOMAIN): + for other_entry in self._async_current_entries(): if CONF_PERSISTENCE_FILE not in other_entry.data: continue if real_persistence_path == self._normalize_persistence_file( @@ -338,7 +338,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_PERSISTENCE_FILE] = "duplicate_persistence_file" break - for other_entry in self.hass.config_entries.async_entries(DOMAIN): + for other_entry in self._async_current_entries(): if _is_same_device(gw_type, user_input, other_entry): errors["base"] = "already_configured" break diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index cd705d816c2..c6ebe543c99 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -112,7 +112,7 @@ class NestFlowHandler( # Update existing config entry when in the reauth flow. This # integration only supports one config entry so remove any prior entries # added before the "single_instance_allowed" check was added - existing_entries = self.hass.config_entries.async_entries(DOMAIN) + existing_entries = self._async_current_entries() if existing_entries: updated = False for entry in existing_entries: @@ -148,7 +148,7 @@ class NestFlowHandler( """Handle a flow initialized by the user.""" if self.is_sdm_api(): # Reauth will update an existing entry - if self.hass.config_entries.async_entries(DOMAIN) and not self._reauth: + if self._async_current_entries() and not self._reauth: return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) return await self.async_step_init(user_input) @@ -159,7 +159,7 @@ class NestFlowHandler( flows = self.hass.data.get(DATA_FLOW_IMPL, {}) - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") if not flows: @@ -229,7 +229,7 @@ class NestFlowHandler( """Import existing auth from Nest.""" assert not self.is_sdm_api(), "Step only supported for legacy API" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") config_path = info["nest_conf_path"] diff --git a/homeassistant/components/omnilogic/config_flow.py b/homeassistant/components/omnilogic/config_flow.py index e8fc947340f..d5239760fcc 100644 --- a/homeassistant/components/omnilogic/config_flow.py +++ b/homeassistant/components/omnilogic/config_flow.py @@ -29,7 +29,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} - config_entry = self.hass.config_entries.async_entries(DOMAIN) + config_entry = self._async_current_entries() if config_entry: return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 3b3cc8fefdc..7c3bc8f8f6b 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -45,7 +45,7 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): device = info[CONF_DEVICE] gw_id = cv.slugify(info.get(CONF_ID, name)) - entries = [e.data for e in self.hass.config_entries.async_entries(DOMAIN)] + entries = [e.data for e in self._async_current_entries()] if gw_id in [e[CONF_ID] for e in entries]: return self._show_form({"base": "id_exists"}) diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index d12274f4f9a..3b9ba84fab5 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -51,7 +51,7 @@ class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input=None): """Handle external yaml configuration.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="already_setup") self.flow_impl = DOMAIN @@ -62,7 +62,7 @@ class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow start.""" flows = self.hass.data.get(DATA_FLOW_IMPL, {}) - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="already_setup") if not flows: @@ -84,7 +84,7 @@ class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_auth(self, user_input=None): """Create an entry for auth.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="external_setup") errors = {} @@ -123,7 +123,7 @@ class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_code(self, code=None): """Received code for authentication.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="already_setup") if code is None: diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index 1084aca1e8b..1be879df58e 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -118,7 +118,7 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.device_list = [device["host-ip"] for device in devices] # Check that devices found aren't configured per account. - entries = self.hass.config_entries.async_entries(DOMAIN) + entries = self._async_current_entries() if entries: # Retrieve device data from all entries if creds match. conf_devices = [ diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py index fcc5b238d70..b696d583c04 100644 --- a/homeassistant/components/soma/config_flow.py +++ b/homeassistant/components/soma/config_flow.py @@ -59,6 +59,6 @@ class SomaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input=None): """Handle flow start from existing config section.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="already_setup") return await self.async_step_creation(user_input) diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index dd803f86af7..05d1720cf6d 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -20,7 +20,7 @@ class SomfyFlowHandler( async def async_step_user(self, user_input=None): """Handle a flow start.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index c2c8a413ded..712f25560cd 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -53,7 +53,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Let user select host or cloud.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="already_setup") if user_input is not None or len(self._hosts) == 1: @@ -125,7 +125,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input): """Import a config entry.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="already_setup") self._scan_interval = user_input[KEY_SCAN_INTERVAL] diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index 90a4bac8cba..d5c63aa736f 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -54,7 +54,7 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: - for entry in self.hass.config_entries.async_entries(DOMAIN): + for entry in self._async_current_entries(): if ( entry.data[CONF_HOST] == user_input[CONF_HOST] and entry.data[CONF_PORT] == user_input[CONF_PORT] diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index cc1a2d8051a..8e2dfc1f43f 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -184,7 +184,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) # Handle devices changing their UDN, only allow a single - existing_entries = self.hass.config_entries.async_entries(DOMAIN) + existing_entries = self._async_current_entries() for config_entry in existing_entries: entry_hostname = config_entry.data.get(CONFIG_ENTRY_HOSTNAME) if entry_hostname == discovery[DISCOVERY_HOSTNAME]: diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 6ae978eb4b8..9a79ca1fbb4 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -10,7 +10,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from .common import async_process_devices -from .config_flow import configured_instances from .const import ( DOMAIN, SERVICE_UPDATE_DEVS, @@ -46,7 +45,7 @@ async def async_setup(hass, config): if conf is None: return True - if not configured_instances(hass): + if not hass.config_entries.async_entries(DOMAIN): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py index 31455468ceb..f91848c5238 100644 --- a/homeassistant/components/vesync/config_flow.py +++ b/homeassistant/components/vesync/config_flow.py @@ -11,12 +11,6 @@ from homeassistant.core import callback from .const import DOMAIN -@callback -def configured_instances(hass): - """Return already configured instances.""" - return hass.config_entries.async_entries(DOMAIN) - - class VeSyncFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -45,7 +39,7 @@ class VeSyncFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a flow start.""" - if configured_instances(self.hass): + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") if not user_input: diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 545ab87f47a..c1aba99d84b 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -278,7 +278,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a config entry from configuration.yaml.""" # Check if new config entry matches any existing config entries - for entry in self.hass.config_entries.async_entries(DOMAIN): + for entry in self._async_current_entries(): # If source is ignore bypass host check and continue through loop if entry.source == SOURCE_IGNORE: continue diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index 59c2d741269..29c1e162ed4 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -59,7 +59,7 @@ class WithingsFlowHandler( if profile: existing_entries = [ config_entry - for config_entry in self.hass.config_entries.async_entries(const.DOMAIN) + for config_entry in self._async_current_entries() if slugify(config_entry.data.get(const.PROFILE)) == slugify(profile) ] diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index 6a13fae70dc..7a57bc20417 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -104,9 +104,8 @@ async def test_form(hass): conf, ) - assert result4["type"] == "form" - assert result4["errors"] == {"base": "already_configured"} - await hass.async_block_till_done() + assert result4["type"] == "abort" + assert result4["reason"] == "already_configured" async def test_import(hass): From 6ef3de464cd18a5913abf91bfc95bd599b53071e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 12 May 2021 15:13:24 +0200 Subject: [PATCH 366/852] Update wheels builder to 2021.05.2 (#50520) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c9a9030935d..93d2827fc51 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -81,7 +81,7 @@ jobs: name: requirements_diff - name: Build wheels - uses: home-assistant/wheels@2021.05.1 + uses: home-assistant/wheels@2021.05.2 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} @@ -151,7 +151,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2021.05.1 + uses: home-assistant/wheels@2021.05.2 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} From 72f342aa5bcc415069e28f4a44e2f7ac0c7855ab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 May 2021 15:21:54 +0200 Subject: [PATCH 367/852] Upgrade aioesphomeapi to 2.7.0 (#50522) --- homeassistant/components/esphome/__init__.py | 8 +++++--- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index e62cb995b98..7d87c6bc736 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -146,16 +146,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if new_state is None: return entity_id = event.data.get("entity_id") - await cli.send_home_assistant_state(entity_id, new_state.state) + await cli.send_home_assistant_state(entity_id, None, new_state.state) async def _send_home_assistant_state( entity_id: str, new_state: State | None ) -> None: """Forward Home Assistant states to ESPHome.""" - await cli.send_home_assistant_state(entity_id, new_state.state) + await cli.send_home_assistant_state(entity_id, None, new_state.state) @callback - def async_on_state_subscription(entity_id: str) -> None: + def async_on_state_subscription( + entity_id: str, attribute: str | None = None + ) -> None: """Subscribe and forward states for requested entities.""" unsub = async_track_state_change_event( hass, [entity_id], send_home_assistant_state_event diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 2f60c84a828..e103fa65992 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==2.6.6"], + "requirements": ["aioesphomeapi==2.7.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 862dba2f420..f2ff61a58d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.6.6 +aioesphomeapi==2.7.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccb08e35bfc..15721c5599f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.6.6 +aioesphomeapi==2.7.0 # homeassistant.components.flo aioflo==0.4.1 From 70961c79a047e47b0bbedec8356c56617f07d2a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 May 2021 09:10:28 -0500 Subject: [PATCH 368/852] Migrate emulate_hue to use storage to fix I/O in event loop (#50473) --- .../components/emulated_hue/__init__.py | 34 ++-- tests/components/emulated_hue/test_init.py | 147 +++++++++--------- 2 files changed, 88 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 3864a2651f8..1ee5e19caa7 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -1,5 +1,4 @@ """Support for local control of entities by emulating a Philips Hue bridge.""" -from contextlib import suppress import logging from aiohttp import web @@ -12,9 +11,8 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv -from homeassistant.util.json import load_json, save_json from .hue_api import ( HueAllGroupsStateView, @@ -34,6 +32,9 @@ DOMAIN = "emulated_hue" _LOGGER = logging.getLogger(__name__) NUMBERS_FILE = "emulated_hue_ids.json" +DATA_KEY = "emulated_hue.ids" +DATA_VERSION = "1" +SAVE_DELAY = 60 CONF_ADVERTISE_IP = "advertise_ip" CONF_ADVERTISE_PORT = "advertise_port" @@ -155,6 +156,7 @@ async def async_setup(hass, yaml_config): nonlocal protocol nonlocal site nonlocal runner + await config.async_setup() _, protocol = await listen @@ -189,6 +191,7 @@ class Config: self.hass = hass self.type = conf.get(CONF_TYPE) self.numbers = None + self.store = None self.cached_states = {} self._exposed_cache = {} @@ -257,14 +260,21 @@ class Config: # for compatibility with older installations. self.lights_all_dimmable = conf.get(CONF_LIGHTS_ALL_DIMMABLE) + async def async_setup(self): + """Set up and migrate to storage.""" + self.store = storage.Store(self.hass, DATA_VERSION, DATA_KEY) + self.numbers = ( + await storage.async_migrator( + self.hass, self.hass.config.path(NUMBERS_FILE), self.store + ) + or {} + ) + def entity_id_to_number(self, entity_id): """Get a unique number for the entity id.""" if self.type == TYPE_ALEXA: return entity_id - if self.numbers is None: - self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE)) - # Google Home for number, ent_id in self.numbers.items(): if entity_id == ent_id: @@ -274,7 +284,7 @@ class Config: if self.numbers: number = str(max(int(k) for k in self.numbers) + 1) self.numbers[number] = entity_id - save_json(self.hass.config.path(NUMBERS_FILE), self.numbers) + self.store.async_delay_save(lambda: self.numbers, SAVE_DELAY) return number def number_to_entity_id(self, number): @@ -282,9 +292,6 @@ class Config: if self.type == TYPE_ALEXA: return number - if self.numbers is None: - self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE)) - # Google Home assert isinstance(number, str) return self.numbers.get(number) @@ -338,10 +345,3 @@ class Config: return True return False - - -def _load_json(filename): - """Load JSON, handling invalid syntax.""" - with suppress(HomeAssistantError): - return load_json(filename) - return {} diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 8e8c0f41249..da15fbfba30 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,106 +1,101 @@ """Test the Emulated Hue component.""" -from unittest.mock import MagicMock, Mock, patch +from datetime import timedelta -from homeassistant.components.emulated_hue import Config +from homeassistant.components.emulated_hue import ( + DATA_KEY, + DATA_VERSION, + SAVE_DELAY, + Config, +) +from homeassistant.util import utcnow + +from tests.common import async_fire_time_changed -def test_config_google_home_entity_id_to_number(): +async def test_config_google_home_entity_id_to_number(hass, hass_storage): """Test config adheres to the type.""" - mock_hass = Mock() - mock_hass.config.path = MagicMock("path", return_value="test_path") - conf = Config(mock_hass, {"type": "google_home"}) + conf = Config(hass, {"type": "google_home"}) + hass_storage[DATA_KEY] = { + "version": DATA_VERSION, + "key": DATA_KEY, + "data": {"1": "light.test2"}, + } - with patch( - "homeassistant.components.emulated_hue.load_json", - return_value={"1": "light.test2"}, - ) as json_loader, patch( - "homeassistant.components.emulated_hue.save_json" - ) as json_saver: - number = conf.entity_id_to_number("light.test") - assert number == "2" + await conf.async_setup() - assert json_saver.mock_calls[0][1][1] == { - "1": "light.test2", - "2": "light.test", - } + number = conf.entity_id_to_number("light.test") + assert number == "2" - assert json_saver.call_count == 1 - assert json_loader.call_count == 1 + async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY)) + await hass.async_block_till_done() + assert hass_storage[DATA_KEY]["data"] == { + "1": "light.test2", + "2": "light.test", + } - number = conf.entity_id_to_number("light.test") - assert number == "2" - assert json_saver.call_count == 1 + number = conf.entity_id_to_number("light.test") + assert number == "2" - number = conf.entity_id_to_number("light.test2") - assert number == "1" - assert json_saver.call_count == 1 + number = conf.entity_id_to_number("light.test2") + assert number == "1" - entity_id = conf.number_to_entity_id("1") - assert entity_id == "light.test2" + entity_id = conf.number_to_entity_id("1") + assert entity_id == "light.test2" -def test_config_google_home_entity_id_to_number_altered(): +async def test_config_google_home_entity_id_to_number_altered(hass, hass_storage): """Test config adheres to the type.""" - mock_hass = Mock() - mock_hass.config.path = MagicMock("path", return_value="test_path") - conf = Config(mock_hass, {"type": "google_home"}) + conf = Config(hass, {"type": "google_home"}) + hass_storage[DATA_KEY] = { + "version": DATA_VERSION, + "key": DATA_KEY, + "data": {"21": "light.test2"}, + } - with patch( - "homeassistant.components.emulated_hue.load_json", - return_value={"21": "light.test2"}, - ) as json_loader, patch( - "homeassistant.components.emulated_hue.save_json" - ) as json_saver: - number = conf.entity_id_to_number("light.test") - assert number == "22" - assert json_saver.call_count == 1 - assert json_loader.call_count == 1 + await conf.async_setup() - assert json_saver.mock_calls[0][1][1] == { - "21": "light.test2", - "22": "light.test", - } + number = conf.entity_id_to_number("light.test") + assert number == "22" - number = conf.entity_id_to_number("light.test") - assert number == "22" - assert json_saver.call_count == 1 + async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY)) + await hass.async_block_till_done() + assert hass_storage[DATA_KEY]["data"] == { + "21": "light.test2", + "22": "light.test", + } - number = conf.entity_id_to_number("light.test2") - assert number == "21" - assert json_saver.call_count == 1 + number = conf.entity_id_to_number("light.test") + assert number == "22" - entity_id = conf.number_to_entity_id("21") - assert entity_id == "light.test2" + number = conf.entity_id_to_number("light.test2") + assert number == "21" + + entity_id = conf.number_to_entity_id("21") + assert entity_id == "light.test2" -def test_config_google_home_entity_id_to_number_empty(): +async def test_config_google_home_entity_id_to_number_empty(hass, hass_storage): """Test config adheres to the type.""" - mock_hass = Mock() - mock_hass.config.path = MagicMock("path", return_value="test_path") - conf = Config(mock_hass, {"type": "google_home"}) + conf = Config(hass, {"type": "google_home"}) + hass_storage[DATA_KEY] = {"version": DATA_VERSION, "key": DATA_KEY, "data": {}} - with patch( - "homeassistant.components.emulated_hue.load_json", return_value={} - ) as json_loader, patch( - "homeassistant.components.emulated_hue.save_json" - ) as json_saver: - number = conf.entity_id_to_number("light.test") - assert number == "1" - assert json_saver.call_count == 1 - assert json_loader.call_count == 1 + await conf.async_setup() - assert json_saver.mock_calls[0][1][1] == {"1": "light.test"} + number = conf.entity_id_to_number("light.test") + assert number == "1" - number = conf.entity_id_to_number("light.test") - assert number == "1" - assert json_saver.call_count == 1 + async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY)) + await hass.async_block_till_done() + assert hass_storage[DATA_KEY]["data"] == {"1": "light.test"} - number = conf.entity_id_to_number("light.test2") - assert number == "2" - assert json_saver.call_count == 2 + number = conf.entity_id_to_number("light.test") + assert number == "1" - entity_id = conf.number_to_entity_id("2") - assert entity_id == "light.test2" + number = conf.entity_id_to_number("light.test2") + assert number == "2" + + entity_id = conf.number_to_entity_id("2") + assert entity_id == "light.test2" def test_config_alexa_entity_id_to_number(): From 0cb08f951665503d4114b4efba871c596a8d29e5 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Wed, 12 May 2021 15:28:44 +0100 Subject: [PATCH 369/852] Add missing type hints in entity_platform (#50453) --- homeassistant/helpers/entity_component.py | 4 +- homeassistant/helpers/entity_platform.py | 53 +++++++++++++++-------- homeassistant/helpers/entity_registry.py | 8 ++-- homeassistant/helpers/reload.py | 2 +- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 7ac221ea06e..f1623889946 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -246,9 +246,7 @@ class EntityComponent: platform_type, platform, scan_interval, entity_namespace ) - await self._platforms[key].async_setup( # type: ignore - platform_config, discovery_info - ) + await self._platforms[key].async_setup(platform_config, discovery_info) async def _async_reset(self) -> None: """Remove entities and reset the entity component to initial values. diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6350467c05b..3331c71dd32 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -33,17 +33,19 @@ from homeassistant.exceptions import ( PlatformNotReady, RequiredParameterMissing, ) -from homeassistant.helpers import ( +from homeassistant.setup import async_start_setup +from homeassistant.util.async_ import run_callback_threadsafe + +from . import ( config_validation as cv, device_registry as dev_reg, entity_registry as ent_reg, service, ) -from homeassistant.setup import async_start_setup -from homeassistant.util.async_ import run_callback_threadsafe - -from .entity_registry import DISABLED_INTEGRATION +from .device_registry import DeviceRegistry +from .entity_registry import DISABLED_INTEGRATION, EntityRegistry from .event import async_call_later, async_track_time_interval +from .typing import ConfigType, DiscoveryInfoType if TYPE_CHECKING: from .entity import Entity @@ -148,7 +150,11 @@ class EntityPlatform: return self.parallel_updates - async def async_setup(self, platform_config, discovery_info=None): # type: ignore[no-untyped-def] + async def async_setup( + self, + platform_config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: """Set up the platform from a config file.""" platform = self.platform hass = self.hass @@ -206,9 +212,9 @@ class EntityPlatform: platform = self.platform @callback - def async_create_setup_task(): # type: ignore[no-untyped-def] + def async_create_setup_task() -> Coroutine: """Get task to set up platform.""" - return platform.async_setup_entry( # type: ignore + return platform.async_setup_entry( # type: ignore[no-any-return,union-attr] self.hass, config_entry, self._async_schedule_add_entities ) @@ -273,7 +279,7 @@ class EntityPlatform: wait_time, ) - async def setup_again(*_): # type: ignore[no-untyped-def] + async def setup_again(*_args: Any) -> None: """Run setup again.""" self._async_cancel_retry_setup = None await self._async_setup_platform(async_create_setup_task, tries) @@ -360,7 +366,7 @@ class EntityPlatform: device_registry = dev_reg.async_get(hass) entity_registry = ent_reg.async_get(hass) tasks = [ - self._async_add_entity( # type: ignore + self._async_add_entity( entity, update_before_add, entity_registry, device_registry ) for entity in new_entities @@ -400,9 +406,13 @@ class EntityPlatform: self.scan_interval, ) - async def _async_add_entity( # type: ignore[no-untyped-def] # noqa: C901 - self, entity, update_before_add, entity_registry, device_registry - ): + async def _async_add_entity( # noqa: C901 + self, + entity: Entity, + update_before_add: bool, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + ) -> None: """Add an entity to the platform.""" if entity is None: raise ValueError("Entity cannot be None") @@ -424,6 +434,7 @@ class EntityPlatform: requested_entity_id = None suggested_object_id: str | None = None + generate_new_entity_id = False # Get entity_id from unique ID registration if entity.unique_id is not None: @@ -431,7 +442,7 @@ class EntityPlatform: requested_entity_id = entity.entity_id suggested_object_id = split_entity_id(entity.entity_id)[1] else: - suggested_object_id = entity.name + suggested_object_id = entity.name # type: ignore[unreachable] if self.entity_namespace is not None: suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" @@ -461,10 +472,10 @@ class EntityPlatform: "suggested_area", ): if key in device_info: - processed_dev_info[key] = device_info[key] + processed_dev_info[key] = device_info[key] # type: ignore[misc] try: - device = device_registry.async_get_or_create(**processed_dev_info) + device = device_registry.async_get_or_create(**processed_dev_info) # type: ignore[arg-type] device_id = device.id except RequiredParameterMissing: pass @@ -510,10 +521,10 @@ class EntityPlatform: ): # If entity already registered, convert entity id to suggestion suggested_object_id = split_entity_id(entity.entity_id)[1] - entity.entity_id = None + generate_new_entity_id = True # Generate entity ID - if entity.entity_id is None: + if entity.entity_id is None or generate_new_entity_id: suggested_object_id = ( suggested_object_id or entity.name or DEVICE_DEFAULT_NAME ) @@ -565,7 +576,11 @@ class EntityPlatform: # has a chance to finish. self.hass.states.async_reserve(entity.entity_id) - entity.async_on_remove(lambda: self.entities.pop(entity_id)) + def remove_entity_cb() -> None: + """Remove entity from entities list.""" + self.entities.pop(entity_id) + + entity.async_on_remove(remove_entity_cb) await entity.add_to_platform_finish() diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 936464dc423..5d3ede31ee6 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -10,7 +10,7 @@ timer. from __future__ import annotations from collections import OrderedDict -from collections.abc import Iterable +from collections.abc import Iterable, Mapping import logging from typing import TYPE_CHECKING, Any, Callable, cast @@ -96,7 +96,7 @@ class RegistryEntry: ) ), ) - capabilities: dict[str, Any] | None = attr.ib(default=None) + capabilities: Mapping[str, Any] | None = attr.ib(default=None) supported_features: int = attr.ib(default=0) device_class: str | None = attr.ib(default=None) unit_of_measurement: str | None = attr.ib(default=None) @@ -232,7 +232,7 @@ class EntityRegistry: config_entry: ConfigEntry | None = None, device_id: str | None = None, area_id: str | None = None, - capabilities: dict[str, Any] | None = None, + capabilities: Mapping[str, Any] | None = None, supported_features: int | None = None, device_class: str | None = None, unit_of_measurement: str | None = None, @@ -392,7 +392,7 @@ class EntityRegistry: area_id: str | None | UndefinedType = UNDEFINED, new_unique_id: str | UndefinedType = UNDEFINED, disabled_by: str | None | UndefinedType = UNDEFINED, - capabilities: dict[str, Any] | None | UndefinedType = UNDEFINED, + capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, supported_features: int | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, unit_of_measurement: str | None | UndefinedType = UNDEFINED, diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 01350b579c4..da6c6935b35 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -124,7 +124,7 @@ async def _async_reconfig_platform( ) -> None: """Reconfigure an already loaded platform.""" await platform.async_reset() - tasks = [platform.async_setup(p_config) for p_config in platform_configs] # type: ignore + tasks = [platform.async_setup(p_config) for p_config in platform_configs] await asyncio.gather(*tasks) From 38a0cf665070da812e9dd2faa01b499249eef27c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 May 2021 17:43:27 +0200 Subject: [PATCH 370/852] Refactor SolarEdge config flow tests (#50467) Co-authored-by: Martin Hjelmare --- .../components/solaredge/test_config_flow.py | 88 +++++++++++-------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index c01a5b6827c..280b1c02ca0 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -6,7 +6,8 @@ from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import data_entry_flow from homeassistant.components.solaredge import config_flow -from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME +from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant @@ -35,20 +36,25 @@ def init_config_flow(hass: HomeAssistant) -> config_flow.SolarEdgeConfigFlow: async def test_user(hass: HomeAssistant, test_api: Mock) -> None: """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" - - # tets with all provided - result = await flow.async_step_user( - {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "solaredge_site_1_2_3" - assert result["data"][CONF_SITE_ID] == SITE_ID - assert result["data"][CONF_API_KEY] == API_KEY + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "user" + + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "solaredge_site_1_2_3" + + data = result.get("data") + assert data + assert data[CONF_SITE_ID] == SITE_ID + assert data[CONF_API_KEY] == API_KEY async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> None: @@ -58,48 +64,56 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> Non data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}, ).add_to_hass(hass) - flow = init_config_flow(hass) - # user: Should fail, same SITE_ID - result = await flow.async_step_user( - {CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_SITE_ID: "already_configured"} + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("errors") == {CONF_SITE_ID: "already_configured"} async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: """Test the _site_in_configuration_exists method.""" - flow = init_config_flow(hass) # test with inactive site test_api.get_details.return_value = {"details": {"status": "NOK"}} - result = await flow.async_step_user( - {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_SITE_ID: "site_not_active"} + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("errors") == {CONF_SITE_ID: "site_not_active"} # test with api_failure test_api.get_details.return_value = {} - result = await flow.async_step_user( - {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_SITE_ID: "invalid_api_key"} + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("errors") == {CONF_SITE_ID: "invalid_api_key"} # test with ConnectionTimeout test_api.get_details.side_effect = ConnectTimeout() - result = await flow.async_step_user( - {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_SITE_ID: "could_not_connect"} + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} # test with HTTPError test_api.get_details.side_effect = HTTPError() - result = await flow.async_step_user( - {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_SITE_ID: "could_not_connect"} + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} From 216b0df908b7db91ca55bc10181d1abf4d10894a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 12 May 2021 18:38:26 +0200 Subject: [PATCH 371/852] Set state_class for demo sensor (#50523) --- homeassistant/components/demo/sensor.py | 27 +++++++++++++++++++----- tests/components/prometheus/test_init.py | 15 +++++++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 7607bad4e1c..00111d34dd4 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -1,5 +1,5 @@ """Demo platform that has a couple of fake sensors.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONCENTRATION_PARTS_PER_MILLION, @@ -23,6 +23,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Outside Temperature", 15.6, DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, TEMP_CELSIUS, 12, ), @@ -31,6 +32,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Outside Humidity", 54, DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, PERCENTAGE, None, ), @@ -39,6 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Carbon monoxide", 54, DEVICE_CLASS_CO, + STATE_CLASS_MEASUREMENT, CONCENTRATION_PARTS_PER_MILLION, None, ), @@ -47,6 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Carbon dioxide", 54, DEVICE_CLASS_CO2, + STATE_CLASS_MEASUREMENT, CONCENTRATION_PARTS_PER_MILLION, 14, ), @@ -63,15 +67,23 @@ class DemoSensor(SensorEntity): """Representation of a Demo sensor.""" def __init__( - self, unique_id, name, state, device_class, unit_of_measurement, battery + self, + unique_id, + name, + state, + device_class, + state_class, + unit_of_measurement, + battery, ): """Initialize the sensor.""" - self._unique_id = unique_id + self._battery = battery + self._device_class = device_class self._name = name self._state = state - self._device_class = device_class + self._state_class = state_class + self._unique_id = unique_id self._unit_of_measurement = unit_of_measurement - self._battery = battery @property def device_info(self): @@ -99,6 +111,11 @@ class DemoSensor(SensorEntity): """Return the device class of the sensor.""" return self._device_class + @property + def state_class(self): + """Return the state class of the sensor.""" + return self._state_class + @property def name(self): """Return the name of the sensor.""" diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index fe91c7c0002..b1abe1de3e5 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -47,14 +47,14 @@ async def prometheus_client(hass, hass_client): ) sensor1 = DemoSensor( - None, "Television Energy", 74, None, ENERGY_KILO_WATT_HOUR, None + None, "Television Energy", 74, None, None, ENERGY_KILO_WATT_HOUR, None ) sensor1.hass = hass sensor1.entity_id = "sensor.television_energy" await sensor1.async_update_ha_state() sensor2 = DemoSensor( - None, "Radio Energy", 14, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, None + None, "Radio Energy", 14, DEVICE_CLASS_POWER, None, ENERGY_KILO_WATT_HOUR, None ) sensor2.hass = hass sensor2.entity_id = "sensor.radio_energy" @@ -65,13 +65,19 @@ async def prometheus_client(hass, hass_client): await sensor2.async_update_ha_state() sensor3 = DemoSensor( - None, "Electricity price", 0.123, None, f"SEK/{ENERGY_KILO_WATT_HOUR}", None + None, + "Electricity price", + 0.123, + None, + None, + f"SEK/{ENERGY_KILO_WATT_HOUR}", + None, ) sensor3.hass = hass sensor3.entity_id = "sensor.electricity_price" await sensor3.async_update_ha_state() - sensor4 = DemoSensor(None, "Wind Direction", 25, None, DEGREE, None) + sensor4 = DemoSensor(None, "Wind Direction", 25, None, None, DEGREE, None) sensor4.hass = hass sensor4.entity_id = "sensor.wind_direction" await sensor4.async_update_ha_state() @@ -81,6 +87,7 @@ async def prometheus_client(hass, hass_client): "SPS30 PM <1µm Weight concentration", 3.7069, None, + None, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, None, ) From 2945c79c5aacbb00950a9f658f0b2400e52a3b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 12 May 2021 20:07:44 +0200 Subject: [PATCH 372/852] Tibber sensors (#50418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tibber, split attribute to sensors Signed-off-by: Daniel Hjelseth Høyer * Tibber, split attribute to sensors Signed-off-by: Daniel Hjelseth Høyer * Tibber, split attribute to sensors Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * fix review comments Signed-off-by: Daniel Hjelseth Høyer * migrate to new device ids Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * Migrate entity id Signed-off-by: Daniel Hjelseth Høyer * Migrate entity id Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * move registers out of looå Signed-off-by: Daniel Hjelseth Høyer Co-authored-by: Martin Hjelmare --- homeassistant/components/tibber/manifest.json | 2 +- homeassistant/components/tibber/sensor.py | 235 ++++++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 194 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 01a20011bef..57b329765a9 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.16.2"], + "requirements": ["pyTibber==0.17.0"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 5ab85013a25..4a706a6a517 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -6,9 +6,29 @@ from random import randrange import aiohttp -from homeassistant.components.sensor import DEVICE_CLASS_POWER, SensorEntity -from homeassistant.const import POWER_WATT +from homeassistant.components.sensor import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_VOLTAGE, + SensorEntity, +) +from homeassistant.const import ( + ELECTRICAL_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + SIGNAL_STRENGTH_DECIBELS, + VOLT, +) +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity_registry import async_get as async_get_entity_reg from homeassistant.util import Throttle, dt as dt_util from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER @@ -19,6 +39,56 @@ ICON = "mdi:currency-usd" SCAN_INTERVAL = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) PARALLEL_UPDATES = 0 +SIGNAL_UPDATE_ENTITY = "tibber_rt_update_{}" + +RT_SENSOR_MAP = { + "averagePower": ["average power", DEVICE_CLASS_POWER, POWER_WATT], + "power": ["power", DEVICE_CLASS_POWER, POWER_WATT], + "minPower": ["min power", DEVICE_CLASS_POWER, POWER_WATT], + "maxPower": ["max power", DEVICE_CLASS_POWER, POWER_WATT], + "accumulatedConsumption": [ + "accumulated consumption", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "accumulatedConsumptionLastHour": [ + "accumulated consumption last hour", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "accumulatedProduction": [ + "accumulated production", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "accumulatedProductionLastHour": [ + "accumulated production last hour", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "lastMeterConsumption": [ + "last meter consumption", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "lastMeterProduction": [ + "last meter production", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "voltagePhase1": ["voltage phase1", DEVICE_CLASS_VOLTAGE, VOLT], + "voltagePhase2": ["voltage phase2", DEVICE_CLASS_VOLTAGE, VOLT], + "voltagePhase3": ["voltage phase3", DEVICE_CLASS_VOLTAGE, VOLT], + "currentL1": ["current L1", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE], + "currentL2": ["current L2", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE], + "currentL3": ["current L3", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE], + "signalStrength": [ + "signal strength", + DEVICE_CLASS_SIGNAL_STRENGTH, + SIGNAL_STRENGTH_DECIBELS, + ], + "accumulatedCost": ["accumulated cost", None, None], +} async def async_setup_entry(hass, entry, async_add_entities): @@ -26,7 +96,10 @@ async def async_setup_entry(hass, entry, async_add_entities): tibber_connection = hass.data.get(TIBBER_DOMAIN) - dev = [] + entity_registry = async_get_entity_reg(hass) + device_registry = async_get_dev_reg(hass) + + entities = [] for home in tibber_connection.get_homes(only_active=False): try: await home.update_info() @@ -36,12 +109,36 @@ async def async_setup_entry(hass, entry, async_add_entities): except aiohttp.ClientError as err: _LOGGER.error("Error connecting to Tibber home: %s ", err) raise PlatformNotReady() from err - if home.has_active_subscription: - dev.append(TibberSensorElPrice(home)) - if home.has_real_time_consumption: - dev.append(TibberSensorRT(home)) - async_add_entities(dev, True) + if home.has_active_subscription: + entities.append(TibberSensorElPrice(home)) + if home.has_real_time_consumption: + await home.rt_subscribe( + TibberRtDataHandler(async_add_entities, home, hass).async_callback + ) + + # migrate + old_id = home.info["viewer"]["home"]["meteringPointData"]["consumptionEan"] + if old_id is None: + continue + + # migrate to new device ids + old_entity_id = entity_registry.async_get_entity_id( + "sensor", TIBBER_DOMAIN, old_id + ) + if old_entity_id is not None: + entity_registry.async_update_entity( + old_entity_id, new_unique_id=home.home_id + ) + + # migrate to new device ids + device_entry = device_registry.async_get_device({(TIBBER_DOMAIN, old_id)}) + if device_entry and entry.entry_id in device_entry.config_entries: + device_registry.async_update_device( + device_entry.id, new_identifiers={(TIBBER_DOMAIN, home.home_id)} + ) + + async_add_entities(entities, True) class TibberSensor(SensorEntity): @@ -50,21 +147,13 @@ class TibberSensor(SensorEntity): def __init__(self, tibber_home): """Initialize the sensor.""" self._tibber_home = tibber_home - self._last_updated = None self._state = None - self._is_available = False - self._extra_state_attributes = {} + self._name = tibber_home.info["viewer"]["home"]["appNickname"] if self._name is None: self._name = tibber_home.info["viewer"]["home"]["address"].get( "address1", "" ) - self._spread_load_constant = randrange(3600) - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._extra_state_attributes @property def model(self): @@ -79,8 +168,7 @@ class TibberSensor(SensorEntity): @property def device_id(self): """Return the ID of the physical device this sensor is part of.""" - home = self._tibber_home.info["viewer"]["home"] - return home["meteringPointData"]["consumptionEan"] + return self._tibber_home.home_id @property def device_info(self): @@ -98,6 +186,19 @@ class TibberSensor(SensorEntity): class TibberSensorElPrice(TibberSensor): """Representation of a Tibber sensor for el price.""" + def __init__(self, tibber_home): + """Initialize the sensor.""" + super().__init__(tibber_home) + self._last_updated = None + self._is_available = False + self._extra_state_attributes = {} + self._spread_load_constant = randrange(5000) + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return self._extra_state_attributes + async def async_update(self): """Get the latest data and updates the states.""" now = dt_util.now() @@ -176,29 +277,23 @@ class TibberSensorElPrice(TibberSensor): class TibberSensorRT(TibberSensor): """Representation of a Tibber sensor for real time consumption.""" + def __init__(self, tibber_home, sensor_name, device_class, unit, initial_state): + """Initialize the sensor.""" + super().__init__(tibber_home) + self._sensor_name = sensor_name + self._device_class = device_class + self._unit = unit + self._state = initial_state + async def async_added_to_hass(self): """Start listen for real time data.""" - await self._tibber_home.rt_subscribe(self.hass.loop, self._async_callback) - - async def _async_callback(self, payload): - """Handle received data.""" - errors = payload.get("errors") - if errors: - _LOGGER.error(errors[0]) - return - data = payload.get("data") - if data is None: - return - live_measurement = data.get("liveMeasurement") - if live_measurement is None: - return - self._state = live_measurement.pop("power", None) - for key, value in live_measurement.items(): - if value is None: - continue - self._extra_state_attributes[key] = value - - self.async_write_ha_state() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_ENTITY.format(self._sensor_name), + self._set_state, + ) + ) @property def available(self): @@ -213,7 +308,13 @@ class TibberSensorRT(TibberSensor): @property def name(self): """Return the name of the sensor.""" - return f"Real time consumption {self._name}" + return f"{self._sensor_name} {self._name}" + + @callback + def _set_state(self, state): + """Set sensor state.""" + self._state = state + self.async_write_ha_state() @property def should_poll(self): @@ -223,14 +324,60 @@ class TibberSensorRT(TibberSensor): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return POWER_WATT + return self._unit @property def unique_id(self): """Return a unique ID.""" - return f"{self.device_id}_rt_consumption" + return f"{self.device_id}_rt_{self._sensor_name}" @property def device_class(self): """Return the device class of the sensor.""" - return DEVICE_CLASS_POWER + return self._device_class + + +class TibberRtDataHandler: + """Handle Tibber realtime data.""" + + def __init__(self, async_add_entities, tibber_home, hass): + """Initialize the data handler.""" + self._async_add_entities = async_add_entities + self._tibber_home = tibber_home + self.hass = hass + self._entities = set() + + async def async_callback(self, payload): + """Handle received data.""" + errors = payload.get("errors") + if errors: + _LOGGER.error(errors[0]) + return + data = payload.get("data") + if data is None: + return + live_measurement = data.get("liveMeasurement") + if live_measurement is None: + return + + new_entities = [] + for sensor_type, state in live_measurement.items(): + if state is None or sensor_type not in RT_SENSOR_MAP: + continue + if sensor_type in self._entities: + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_ENTITY.format(RT_SENSOR_MAP[sensor_type][0]), + state, + ) + else: + sensor_name, device_class, unit = RT_SENSOR_MAP[sensor_type] + if sensor_type == "accumulatedCost": + unit = self._tibber_home.currency + entity = TibberSensorRT( + self._tibber_home, sensor_name, device_class, unit, state + ) + new_entities.append(entity) + self._entities.add(sensor_type) + if new_entities: + self._async_add_entities(new_entities) diff --git a/requirements_all.txt b/requirements_all.txt index f2ff61a58d2..4490c2e2a46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1250,7 +1250,7 @@ pyRFXtrx==0.26.1 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.16.2 +pyTibber==0.17.0 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15721c5599f..c53334f9688 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -687,7 +687,7 @@ pyMetno==0.8.3 pyRFXtrx==0.26.1 # homeassistant.components.tibber -pyTibber==0.16.2 +pyTibber==0.17.0 # homeassistant.components.nextbus py_nextbusnext==0.1.4 From db82808466af8cba461b52755932ff9cc3801459 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 12 May 2021 13:40:10 -0500 Subject: [PATCH 373/852] Skip adding battery on S1 Sonos devices (#50536) --- homeassistant/components/sonos/speaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 0b40b23fe40..786b0113334 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -180,7 +180,7 @@ class SonosSpeaker: self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen ) - if (battery_info := fetch_battery_info_or_none(self.soco)) is not None: + if battery_info := fetch_battery_info_or_none(self.soco): # Battery events can be infrequent, polling is still necessary self.battery_info = battery_info self._battery_poll_timer = self.hass.helpers.event.track_time_interval( From 4ce3038b296f85f6b09ecab986bc9e813f4dceb1 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 12 May 2021 14:49:04 -0400 Subject: [PATCH 374/852] Add targets and selectors for services (G-H) (#50524) Co-authored-by: Franck Nijhof --- .../components/generic/services.yaml | 1 + .../generic_thermostat/services.yaml | 1 + .../components/geniushub/services.yaml | 48 +++++++- .../components/guardian/services.yaml | 83 ++++++++----- .../components/habitica/services.yaml | 12 ++ .../components/hangouts/services.yaml | 18 ++- .../components/harmony/services.yaml | 23 ++-- .../components/hdmi_cec/services.yaml | 44 ++++++- homeassistant/components/heos/services.yaml | 14 ++- .../components/history_stats/services.yaml | 1 + homeassistant/components/hive/services.yaml | 63 +++++----- .../components/homekit/services.yaml | 3 + .../components/homematic/services.yaml | 116 +++++++++++++++++- .../homematicip_cloud/services.yaml | 83 +++++++++++-- homeassistant/components/html5/services.yaml | 11 +- .../components/huawei_lte/services.yaml | 16 +++ .../components/humidifier/services.yaml | 47 ++++--- 17 files changed, 476 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/generic/services.yaml b/homeassistant/components/generic/services.yaml index afde1990cef..a05a9e3415d 100644 --- a/homeassistant/components/generic/services.yaml +++ b/homeassistant/components/generic/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all generic entities. diff --git a/homeassistant/components/generic_thermostat/services.yaml b/homeassistant/components/generic_thermostat/services.yaml index fedcd268253..ef6745bd36f 100644 --- a/homeassistant/components/generic_thermostat/services.yaml +++ b/homeassistant/components/generic_thermostat/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all generic_thermostat entities. diff --git a/homeassistant/components/geniushub/services.yaml b/homeassistant/components/geniushub/services.yaml index fa46c1d4c09..27d7189f953 100644 --- a/homeassistant/components/geniushub/services.yaml +++ b/homeassistant/components/geniushub/services.yaml @@ -2,39 +2,77 @@ # Describes the format for available services set_zone_mode: + name: Set zone mode description: >- Set the zone to an operating mode. fields: entity_id: + name: Entity description: The zone's entity_id. + required: true example: climate.kitchen + selector: + entity: + integration: geniushub + domain: climate mode: + name: Mode description: "One of: off, timer or footprint." + required: true example: timer + selector: + select: + options: + - 'off' + - 'timer' + - 'footprint' set_zone_override: + name: Set zone override description: >- - Override the zone's setpoint for a given duration. + Override the zone's set point for a given duration. fields: entity_id: + name: Entity description: The zone's entity_id. + required: true example: climate.bathroom + selector: + entity: + integration: geniushub + domain: climate temperature: - description: The target temperature, to 0.1 C. + name: Temperature + description: The target temperature. + required: true example: 19.2 + selector: + number: + min: 4 + max: 28 + step: 0.1 + unit_of_measurement: '°' duration: + name: Duration description: >- The duration of the override. Optional, default 1 hour, maximum 24 hours. example: '{"minutes": 135}' + selector: + object: set_switch_override: + name: Set switch override description: >- Override switch for a given duration. + target: + entity: + integration: geniushub + domain: switch fields: - entity_id: - description: The zone's entity_id. - example: switch.study duration: + name: Duration description: >- The duration of the override. Optional, default 1 hour, maximum 24 hours. example: '{"minutes": 135}' + selector: + object: diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index dc78503eb12..cb2e7827657 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -1,58 +1,85 @@ # Describes the format for available Elexa Guardians services disable_ap: + name: Disable AP description: Disable the device's onboard access point. - fields: - entity_id: - description: The Guardian valve controller to affect. - example: switch.guardian_abcde_valve + target: + entity: + integration: guardian + domain: switch enable_ap: + name: Enable AP description: Enable the device's onboard access point. - fields: - entity_id: - description: The Guardian valve controller to affect. - example: switch.guardian_abcde_valve + target: + entity: + integration: guardian + domain: switch pair_sensor: + name: Pair sensor description: Add a new paired sensor to the valve controller. + target: + entity: + integration: guardian + domain: switch fields: - entity_id: - description: The Guardian valve controller to affect. - example: switch.guardian_abcde_valve uid: + name: UID description: The UID of the paired sensor + required: true example: 5410EC688BCF + selector: + text: reboot: + name: Reboot description: Reboot the device. - fields: - entity_id: - description: The Guardian valve controller to affect. - example: switch.guardian_abcde_valve + target: + entity: + integration: guardian + domain: switch reset_valve_diagnostics: + name: Reset valve diagnostics description: Fully (and irrecoverably) reset all valve diagnostics. - fields: - entity_id: - description: The Guardian valve controller to affect. - example: switch.guardian_abcde_valve + target: + entity: + integration: guardian + domain: switch unpair_sensor: + name: Unpair sensor description: Remove a paired sensor from the valve controller. + target: + entity: + integration: guardian + domain: switch fields: - entity_id: - description: The Guardian valve controller to affect. - example: switch.guardian_abcde_valve uid: + name: UID description: The UID of the paired sensor + required: true example: 5410EC688BCF + selector: + text: upgrade_firmware: + name: Upgrade firmware description: Upgrade the device firmware. + target: + entity: + integration: guardian + domain: switch fields: - entity_id: - description: The Guardian valve controller to affect. - example: switch.guardian_abcde_valve url: - description: (optional) The URL of the server hosting the firmware file. + name: URL + description: The URL of the server hosting the firmware file. example: https://repo.guardiancloud.services/gvc/fw + selector: + text: port: - description: (optional) The port on which the firmware file is served. + name: Port + description: The port on which the firmware file is served. example: 443 + selector: + text: filename: - description: (optional) The firmware filename. + name: Filename + description: The firmware filename. example: latest.bin + selector: + text: diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml index 6fa8589ba4c..e60e2238088 100644 --- a/homeassistant/components/habitica/services.yaml +++ b/homeassistant/components/habitica/services.yaml @@ -1,13 +1,25 @@ # Describes the format for Habitica service api_call: + name: API name description: Call Habitica API fields: name: + name: Name description: Habitica's username to call for + required: true example: "xxxNotAValidNickxxx" + selector: + text: path: + name: Path description: "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks" + required: true example: '["tasks", "user", "post"]' + selector: + object: args: + name: Args description: Any additional JSON or URL parameter arguments. See apidoc mentioned for path. Example uses same API endpoint example: '{"text": "Use API from Home Assistant", "type": "todo"}' + selector: + object: diff --git a/homeassistant/components/hangouts/services.yaml b/homeassistant/components/hangouts/services.yaml index 717e2888493..041c21b5c25 100644 --- a/homeassistant/components/hangouts/services.yaml +++ b/homeassistant/components/hangouts/services.yaml @@ -1,18 +1,32 @@ update: + name: Update description: Updates the list of conversations. send_message: + name: Send message description: Send a notification to a specific target. fields: target: - description: List of targets with id or name. [Required] + name: Target + description: List of targets with id or name. + required: true example: '[{"id": "UgxrXzVrARmjx_C6AZx4AaABAagBo-6UCw"}, {"name": "Test Conversation"}]' + selector: + object: message: - description: List of message segments, only the "text" field is required in every segment. [Required] + name: Message + description: List of message segments, only the "text" field is required in every segment. + required: true example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}]' + selector: + object: data: + name: Data description: Other options ['image_file' / 'image_url'] example: '{ "image_file": "file" }' + selector: + object: reconnect: + name: Reconnect description: Reconnect the bot. diff --git a/homeassistant/components/harmony/services.yaml b/homeassistant/components/harmony/services.yaml index f20f0494a5f..e548285ae83 100644 --- a/homeassistant/components/harmony/services.yaml +++ b/homeassistant/components/harmony/services.yaml @@ -1,16 +1,25 @@ sync: + name: Sync description: Syncs the remote's configuration. - fields: - entity_id: - description: Name(s) of entities to sync. - example: "remote.family_room" + target: + entity: + integration: harmony + domain: remote change_channel: + name: Change channel description: Sends change channel command to the Harmony HUB + target: + entity: + integration: harmony + domain: remote fields: - entity_id: - description: Name(s) of Harmony remote entities to send change channel command to - example: "remote.family_room" channel: + name: Channel description: Channel number to change to + required: true example: "200" + selector: + number: + min: 1 + max: 100000 diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml index aa85ffb0214..e5ee0d0a95a 100644 --- a/homeassistant/components/hdmi_cec/services.yaml +++ b/homeassistant/components/hdmi_cec/services.yaml @@ -1,45 +1,87 @@ power_on: + name: Power on description: Power on all devices which supports it. select_device: + name: Select device description: Select HDMI device. fields: device: + name: Device description: Address of device to select. Can be entity_id, physical address or alias from configuration. + required: true example: '"switch.hdmi_1" or "1.1.0.0" or "01:10"' + selector: + text: send_command: + name: Send command description: Sends CEC command into HDMI CEC capable adapter. fields: att: + name: Att description: Optional parameters. example: [0, 2] + selector: + object: cmd: + name: Command description: 'Command itself. Could be decimal number or string with hexadeximal notation: "0x10".' example: 144 or "0x90" + selector: + text: dst: + name: Destination description: 'Destination for command. Could be decimal number or string with hexadeximal notation: "0x10".' example: 5 or "0x5" + selector: + text: raw: + name: Raw description: >- Raw CEC command in format "00:00:00:00" where first two digits are source and destination, second byte is command and optional other bytes are command parameters. If raw command specified, other params are ignored. example: '"10:36"' + selector: + text: src: + name: Source description: 'Source of command. Could be decimal number or string with hexadeximal notation: "0x10".' example: 12 or "0xc" + selector: + text: standby: + name: Standby description: Standby all devices which supports it. update: + name: Update description: Update devices state from network. volume: + name: Volume description: Increase or decrease volume of system. fields: down: + name: Down description: Decreases volume x levels. example: 3 + selector: + number: + min: 1 + max: 100 mute: - description: Mutes audio system. Value should be on, off or toggle. + name: Mute + description: Mutes audio system. example: toggle + selector: + select: + options: + - 'off' + - 'on' + - 'toggle' up: + name: Up description: Increases volume x levels. example: 3 + selector: + number: + min: 1 + max: 100 diff --git a/homeassistant/components/heos/services.yaml b/homeassistant/components/heos/services.yaml index 0fe0518323f..320ed297873 100644 --- a/homeassistant/components/heos/services.yaml +++ b/homeassistant/components/heos/services.yaml @@ -1,12 +1,22 @@ sign_in: + name: Sign in description: Sign the controller in to a HEOS account. fields: username: - description: The username or email of the HEOS account. [Required] + name: Username + description: The username or email of the HEOS account. + required: true example: "example@example.com" + selector: + text: password: - description: The password of the HEOS account. [Required] + name: Password + description: The password of the HEOS account. + required: true example: "password" + selector: + text: sign_out: + name: Sign out description: Sign the controller out of the HEOS account. diff --git a/homeassistant/components/history_stats/services.yaml b/homeassistant/components/history_stats/services.yaml index 38758a35df0..f254295ea20 100644 --- a/homeassistant/components/history_stats/services.yaml +++ b/homeassistant/components/history_stats/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all history_stats entities. diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml index de1439eead4..f69e9efa19f 100644 --- a/homeassistant/components/hive/services.yaml +++ b/homeassistant/components/hive/services.yaml @@ -1,35 +1,11 @@ boost_heating: name: Boost Heating (To be deprecated) description: To be deprecated please use boost_heating_on. + target: + entity: + integration: hive + domain: climate fields: - entity_id: - name: Entity ID - description: Select entity_id to boost. - required: true - example: climate.heating - time_period: - name: Time Period - description: Set the time period for the boost. - required: true - example: 01:30:00 - temperature: - name: Temperature - description: Set the target temperature for the boost period. - required: true - example: 20.5 -boost_heating_on: - name: Boost Heating On - description: Set the boost mode ON defining the period of time and the desired target temperature for the boost. - fields: - entity_id: - name: Entity ID - description: Select entity_id to boost. - required: true - example: climate.heating - selector: - entity: - integration: hive - domain: climate time_period: name: Time Period description: Set the time period for the boost. @@ -40,14 +16,41 @@ boost_heating_on: temperature: name: Temperature description: Set the target temperature for the boost period. - required: true example: 20.5 + default: 25.0 selector: number: min: 7 max: 35 step: 0.5 - unit_of_measurement: degrees + unit_of_measurement: ° + mode: slider +boost_heating_on: + name: Boost Heating On + description: Set the boost mode ON defining the period of time and the desired target temperature for the boost. + target: + entity: + integration: hive + domain: climate + fields: + time_period: + name: Time Period + description: Set the time period for the boost. + required: true + example: 01:30:00 + selector: + time: + temperature: + name: Temperature + description: Set the target temperature for the boost period. + example: 20.5 + default: 25.0 + selector: + number: + min: 7 + max: 35 + step: 0.5 + unit_of_measurement: ° mode: slider boost_heating_off: name: Boost Heating Off diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml index a6b09a80e7f..315a612241f 100644 --- a/homeassistant/components/homekit/services.yaml +++ b/homeassistant/components/homekit/services.yaml @@ -1,12 +1,15 @@ # Describes the format for available HomeKit services start: + name: Start description: Starts the HomeKit driver reload: + name: Reload description: Reload homekit and re-process YAML configuration reset_accessory: + name: Reset accessory description: Reset a HomeKit accessory target: entity: {} diff --git a/homeassistant/components/homematic/services.yaml b/homeassistant/components/homematic/services.yaml index 2dc850330f0..c1afa3cf1a8 100644 --- a/homeassistant/components/homematic/services.yaml +++ b/homeassistant/components/homematic/services.yaml @@ -1,87 +1,193 @@ # Describes the format for available component services virtualkey: + name: Virtual key description: Press a virtual key from CCU/Homegear or simulate keypress. fields: address: + name: Address description: Address of homematic device or BidCoS-RF for virtual remote. + required: true example: BidCoS-RF + selector: + text: channel: + name: Channel description: Channel for calling a keypress. + required: true example: 1 + selector: + number: + min: 1 + max: 6 param: + name: Param description: Event to send i.e. PRESS_LONG, PRESS_SHORT. + required: true example: PRESS_LONG + selector: + text: interface: - description: (Optional) for set an interface value. + name: Interface + description: Set an interface value. example: Interfaces name from config + selector: + text: set_variable_value: + name: Set variable value description: Set the name of a node. fields: entity_id: + name: Entity description: Name(s) of homematic central to set value. example: "homematic.ccu2" + selector: + entity: + domain: homematic name: + name: Name description: Name of the variable to set. + required: true example: "testvariable" + selector: + text: value: + name: Value description: New value + required: true example: 1 + selector: + text: set_device_value: + name: Set device value description: Set a device property on RPC XML interface. fields: address: + name: Address description: Address of homematic device or BidCoS-RF for virtual remote + required: true example: BidCoS-RF + selector: + text: channel: + name: Channel description: Channel for calling a keypress + required: true example: 1 + selector: + number: + min: 1 + max: 6 param: + name: Param description: Event to send i.e. PRESS_LONG, PRESS_SHORT + required: true example: PRESS_LONG + selector: + text: interface: - description: (Optional) for set an interface value + name: Interface + description: Set an interface value example: Interfaces name from config + selector: + text: value: + name: Value description: New value + required: true example: 1 + selector: + text: + value_type: + name: Value type + description: Type for new value + selector: + select: + options: + - 'boolean' + - 'dateTime.iso8601' + - 'double' + - 'int' + - 'string' reconnect: + name: Reconnect description: Reconnect to all Homematic Hubs. set_install_mode: + name: Set install mode description: Set a RPC XML interface into installation mode. fields: interface: + name: Interface description: Select the given interface into install mode + required: true example: Interfaces name from config + selector: + text: mode: - description: (Default 1) 1= Normal mode / 2= Remove exists old links + name: Mode + description: 1= Normal mode / 2= Remove exists old links example: 1 + default: 1 + selector: + number: + min: 1 + max: 2 time: - description: (Default 60) Time in seconds to run in install mode + name: Time + description: Time to run in install mode example: 1 + default: 60 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds address: - description: (Optional) Address of homematic device or BidCoS-RF to learn + name: Address + description: Address of homematic device or BidCoS-RF to learn example: LEQ3948571 + selector: + text: put_paramset: + name: Put paramset description: Call to putParamset in the RPC XML interface fields: interface: + name: Interface description: The interfaces name from the config + required: true example: wireless + selector: + text: address: + name: Address description: Address of Homematic device + required: true example: LEQ3948571:0 + selector: + text: paramset_key: + name: Paramset key description: The paramset_key argument to putParamset + required: true example: MASTER + selector: + text: paramset: + name: Paramset description: A paramset dictionary + required: true example: '{"WEEK_PROGRAM_POINTER": 1}' + selector: + object: rx_mode: + name: RX mode description: The receive mode used. example: BURST + selector: + text: diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index 20447e496f7..3f493ef11a9 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -1,78 +1,145 @@ # Describes the format for available component services activate_eco_mode_with_duration: + name: Activate eco mode with duration description: Activate eco mode with period. fields: duration: + name: Duration description: The duration of eco mode in minutes. + required: true example: 60 accesspoint_id: - description: The ID of the Homematic IP Access Point (optional) + name: Accesspoint ID + description: The ID of the Homematic IP Access Point example: 3014xxxxxxxxxxxxxxxxxxxx + selector: + text: activate_eco_mode_with_period: + name: Activate eco more with period description: Activate eco mode with period. fields: endtime: + name: Endtime description: The time when the eco mode should automatically be disabled. + required: true example: 2019-02-17 14:00 + selector: + text: accesspoint_id: - description: The ID of the Homematic IP Access Point (optional) + name: Accesspoint ID + description: The ID of the Homematic IP Access Point example: 3014xxxxxxxxxxxxxxxxxxxx + selector: + text: activate_vacation: + name: Activate vacation description: Activates the vacation mode until the given time. fields: endtime: + name: Endtime description: The time when the vacation mode should automatically be disabled. + required: true example: 2019-09-17 14:00 + selector: + text: temperature: + name: Temperature description: the set temperature during the vacation mode. + required: true example: 18.5 + default: 18 + selector: + number: + min: 0 + max: 55 + step: 0.5 + unit_of_measurement: '°' accesspoint_id: - description: The ID of the Homematic IP Access Point (optional) + name: Accesspoint ID + description: The ID of the Homematic IP Access Point example: 3014xxxxxxxxxxxxxxxxxxxx + selector: + text: deactivate_eco_mode: + name: Deactivate eco mode description: Deactivates the eco mode immediately. fields: accesspoint_id: - description: The ID of the Homematic IP Access Point (optional) + name: Accesspoint ID + description: The ID of the Homematic IP Access Point example: 3014xxxxxxxxxxxxxxxxxxxx + selector: + text: deactivate_vacation: + name: Deactivate vacation description: Deactivates the vacation mode immediately. fields: accesspoint_id: - description: The ID of the Homematic IP Access Point (optional) + name: Accesspoint ID + description: The ID of the Homematic IP Access Point example: 3014xxxxxxxxxxxxxxxxxxxx + selector: + text: set_active_climate_profile: + name: Set active climate profile description: Set the active climate profile index. fields: entity_id: - description: The ID of the climte entity. Use 'all' keyword to switch the profile for all entities. + name: Entity + description: The ID of the climate entity. Use 'all' keyword to switch the profile for all entities. + required: true example: climate.livingroom + selector: + text: climate_profile_index: + name: Climate profile index description: The index of the climate profile (1 based) + required: true example: 1 + selector: + number: + min: 1 + max: 100 dump_hap_config: + name: Dump hap config description: Dump the configuration of the Homematic IP Access Point(s). fields: config_output_path: + name: Config output path description: (Default is 'Your home-assistant config directory') Path where to store the config. example: "/config" + selector: + text: config_output_file_prefix: - description: (Default is 'hmip-config') Name of the config file. The SGTIN of the AP will always be appended. + name: Config output file prefix + description: Name of the config file. The SGTIN of the AP will always be appended. example: "hmip-config" + default: "hmip-config" + selector: + text: anonymize: - description: (Default is True) Should the Configuration be anonymized? + name: Anonymize + description: Should the Configuration be anonymized? example: true + default: true + selector: + boolean: reset_energy_counter: + name: Reset energy counter description: Reset the energy counter of a measuring entity. fields: entity_id: + name: Entity description: The ID of the measuring entity. Use 'all' keyword to reset all energy counters. + required: true example: switch.livingroom + selector: + text: diff --git a/homeassistant/components/html5/services.yaml b/homeassistant/components/html5/services.yaml index f3df4341594..f6b76e67cd7 100644 --- a/homeassistant/components/html5/services.yaml +++ b/homeassistant/components/html5/services.yaml @@ -1,9 +1,16 @@ dismiss: + name: Dismiss description: Dismiss a html5 notification. fields: target: - description: An array of targets. Optional. + name: Target + description: An array of targets. example: ["my_phone", "my_tablet"] + selector: + object: data: - description: Extended information of notification. Supports tag. Optional. + name: Data + description: Extended information of notification. Supports tag. example: '{ "tag": "tagname" }' + selector: + object: diff --git a/homeassistant/components/huawei_lte/services.yaml b/homeassistant/components/huawei_lte/services.yaml index bcb9be33299..711064b435e 100644 --- a/homeassistant/components/huawei_lte/services.yaml +++ b/homeassistant/components/huawei_lte/services.yaml @@ -1,30 +1,46 @@ clear_traffic_statistics: + name: Clear traffic statistics description: Clear traffic statistics. fields: url: + name: URL description: URL of router to clear; optional when only one is configured. example: http://192.168.100.1/ + selector: + text: reboot: + name: Reboot description: Reboot router. fields: url: + name: URL description: URL of router to reboot; optional when only one is configured. example: http://192.168.100.1/ + selector: + text: resume_integration: + name: Resume integration description: Resume suspended integration. fields: url: + name: URL description: URL of router to resume integration for; optional when only one is configured. example: http://192.168.100.1/ + selector: + text: suspend_integration: + name: Suspend integration description: > Suspend integration. Suspending logs the integration out from the router, and stops accessing it. Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. Invoke the resume_integration service to resume. fields: url: + name: URL description: URL of router to resume integration for; optional when only one is configured. example: http://192.168.100.1/ + selector: + text: diff --git a/homeassistant/components/humidifier/services.yaml b/homeassistant/components/humidifier/services.yaml index d10f2fb604b..a11c5fb1198 100644 --- a/homeassistant/components/humidifier/services.yaml +++ b/homeassistant/components/humidifier/services.yaml @@ -1,42 +1,53 @@ # Describes the format for available humidifier services set_mode: + name: Set mode description: Set mode for humidifier device. + target: + entity: + domain: humidifier fields: - entity_id: - description: Name(s) of entities to change. - example: 'humidifier.bedroom' mode: description: New mode + required: true example: 'away' + selector: + text: set_humidity: + name: Set humidity description: Set target humidity of humidifier device. + target: + entity: + domain: humidifier fields: - entity_id: - description: Name(s) of entities to change. - example: 'humidifier.bedroom' humidity: description: New target humidity for humidifier device. + required: true example: 50 + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" turn_on: + name: Turn on description: Turn humidifier device on. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'humidifier.bedroom' + target: + entity: + domain: humidifier turn_off: + name: Turn off description: Turn humidifier device off. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'humidifier.bedroom' + target: + entity: + domain: humidifier toggle: + name: Toggle description: Toggles a humidifier device. - fields: - entity_id: - description: Name(s) of entities to toggle. - example: 'humidifier.bedroom' + target: + entity: + domain: humidifier From ce87fc902b74aa2d400e3f677d0ff85842c8f08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 12 May 2021 21:12:58 +0200 Subject: [PATCH 375/852] Bump pyhaversion from 21.3.0 to 21.5.0 (#50540) --- homeassistant/components/version/manifest.json | 11 ++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 880b000bc43..6f36c337a76 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,8 +2,13 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": ["pyhaversion==21.3.0"], - "codeowners": ["@fabaff", "@ludeeus"], + "requirements": [ + "pyhaversion==21.5.0" + ], + "codeowners": [ + "@fabaff", + "@ludeeus" + ], "quality_scale": "internal", "iot_class": "local_push" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 4490c2e2a46..08a61a62cc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1437,7 +1437,7 @@ pygtfs==0.1.5 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.3.0 +pyhaversion==21.5.0 # homeassistant.components.heos pyheos==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c53334f9688..56c4baa35a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -787,7 +787,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.3.0 +pyhaversion==21.5.0 # homeassistant.components.heos pyheos==0.7.2 From ca090279146b50b019bb593153bae5e4c5e6b480 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 12 May 2021 16:16:29 -0500 Subject: [PATCH 376/852] Bump pysonos to 0.0.46 (#50544) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index b4c53d96fd6..e410fff2a07 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.45"], + "requirements": ["pysonos==0.0.46"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 08a61a62cc0..13c07db65c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1741,7 +1741,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.45 +pysonos==0.0.46 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56c4baa35a6..74f17affea3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -968,7 +968,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.45 +pysonos==0.0.46 # homeassistant.components.spc pyspcwebgw==0.4.0 From 3b3d6e0da53db2a0c9a65a649776ac3bd4e8906f Mon Sep 17 00:00:00 2001 From: Kevin Anthony Date: Thu, 13 May 2021 01:02:24 -0300 Subject: [PATCH 377/852] Add vesync Core200S air purifier (#50216) --- homeassistant/components/vesync/fan.py | 39 ++++++++++++++----- homeassistant/components/vesync/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index d01d3d4dc5d..6641f43d17b 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -18,12 +18,16 @@ _LOGGER = logging.getLogger(__name__) DEV_TYPE_TO_HA = { "LV-PUR131S": "fan", + "Core200S": "fan", } FAN_MODE_AUTO = "auto" FAN_MODE_SLEEP = "sleep" -PRESET_MODES = [FAN_MODE_AUTO, FAN_MODE_SLEEP] +PRESET_MODES = { + "LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "Core200S": [FAN_MODE_SLEEP], +} SPEED_RANGE = (1, 3) # off is not included @@ -86,7 +90,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): @property def preset_modes(self): """Get the list of available preset modes.""" - return PRESET_MODES + return PRESET_MODES[self.device.device_type] @property def preset_mode(self): @@ -103,13 +107,30 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): @property def extra_state_attributes(self): """Return the state attributes of the fan.""" - return { - "mode": self.smartfan.mode, - "active_time": self.smartfan.active_time, - "filter_life": self.smartfan.filter_life, - "air_quality": self.smartfan.air_quality, - "screen_status": self.smartfan.screen_status, - } + attr = {} + + if hasattr(self.smartfan, "active_time"): + attr["active_time"] = self.smartfan.active_time + + if hasattr(self.smartfan, "screen_status"): + attr["screen_status"] = self.smartfan.screen_status + + if hasattr(self.smartfan, "child_lock"): + attr["child_lock"] = self.smartfan.child_lock + + if hasattr(self.smartfan, "night_light"): + attr["night_light"] = self.smartfan.night_light + + if hasattr(self.smartfan, "air_quality"): + attr["air_quality"] = self.smartfan.air_quality + + if hasattr(self.smartfan, "mode"): + attr["mode"] = self.smartfan.mode + + if hasattr(self.smartfan, "filter_life"): + attr["filter_life"] = self.smartfan.filter_life + + return attr def set_percentage(self, percentage): """Set the speed of the device.""" diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index f09a58e4696..70c46d0f02e 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -3,7 +3,7 @@ "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], - "requirements": ["pyvesync==1.3.1"], + "requirements": ["pyvesync==1.4.0"], "config_flow": true, "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 13c07db65c9..43e2cd5e846 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1919,7 +1919,7 @@ pyvera==0.3.13 pyversasense==0.0.6 # homeassistant.components.vesync -pyvesync==1.3.1 +pyvesync==1.4.0 # homeassistant.components.vizio pyvizio==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74f17affea3..3db0b015696 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1034,7 +1034,7 @@ pytradfri[async]==7.0.6 pyvera==0.3.13 # homeassistant.components.vesync -pyvesync==1.3.1 +pyvesync==1.4.0 # homeassistant.components.vizio pyvizio==0.1.57 From 1c2692c3c33ceed1594b6a73b601629daab983c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 May 2021 01:18:37 -0500 Subject: [PATCH 378/852] Drop nuheat code owner (#50319) - I no longer have this device --- CODEOWNERS | 1 - homeassistant/components/nuheat/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c2824fb33b6..d1f14761180 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -330,7 +330,6 @@ homeassistant/components/notify_events/* @matrozov @papajojo homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte -homeassistant/components/nuheat/* @bdraco homeassistant/components/nuki/* @pschmitt @pvizeli @pree homeassistant/components/numato/* @clssn homeassistant/components/number/* @home-assistant/core @Shulyaka diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index 64f7c0e43e4..d2dbb12ebc5 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -3,7 +3,7 @@ "name": "NuHeat", "documentation": "https://www.home-assistant.io/integrations/nuheat", "requirements": ["nuheat==0.3.0"], - "codeowners": ["@bdraco"], + "codeowners": [], "config_flow": true, "dhcp": [ { From 7224012016b8f0965081edd546fa200902432f71 Mon Sep 17 00:00:00 2001 From: LJU Date: Thu, 13 May 2021 11:15:02 +0200 Subject: [PATCH 379/852] Fix spelling in Cast and Growatt (#50555) --- homeassistant/components/cast/strings.json | 2 +- homeassistant/components/growatt_server/strings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json index 02bfeccf794..719465e98ca 100644 --- a/homeassistant/components/cast/strings.json +++ b/homeassistant/components/cast/strings.json @@ -30,7 +30,7 @@ }, "advanced_options": { "title": "Advanced Google Cast configuration", - "description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don’t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be will be passed to pychromecast.IGNORE_CEC.", + "description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don’t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be passed to pychromecast.IGNORE_CEC.", "data": { "ignore_cec": "Ignore CEC", "uuid": "Allowed UUIDs" diff --git a/homeassistant/components/growatt_server/strings.json b/homeassistant/components/growatt_server/strings.json index 903ba400a6f..e8d4f395c7b 100644 --- a/homeassistant/components/growatt_server/strings.json +++ b/homeassistant/components/growatt_server/strings.json @@ -16,7 +16,7 @@ "user": { "data": { "name": "[%key:common::config_flow::data::name%]", - "password": "[%key:common::config_flow::data::name%]", + "password": "[%key:common::config_flow::data::password%]", "username": "[%key:common::config_flow::data::username%]" }, "title": "Enter your Growatt information" From e991b61c12d4de252d0fbabc1a90e8436dce8ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 13 May 2021 16:20:01 +0200 Subject: [PATCH 380/852] Fix issue with quotes (#50571) --- .github/workflows/translations.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 86f11724d46..d2ccdc6c9ca 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -35,7 +35,7 @@ jobs: download: name: Download needs: upload - if: github.event_name == 'schedule' || github.event_name == "workflow_dispatch" + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - name: Checkout the repository From 136b34af20588f101d6ef11c53a4df6377536151 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 13 May 2021 17:07:13 +0200 Subject: [PATCH 381/852] Use requirements for constraints (#50558) --- .github/workflows/wheels.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 93d2827fc51..796b4b2c219 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -162,6 +162,6 @@ jobs: apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev" pip: "Cython;numpy;scikit-build" skip-binary: aiohttp - constraints: "homeassistant/package_constraints.txt" + constraints: "requirements_all.txt" requirements-diff: 'requirements_diff.txt' requirements: "requirements_all.txt" diff --git a/Dockerfile b/Dockerfile index 6bcb080a06e..dcf3c9979d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ WORKDIR /usr/src COPY . homeassistant/ RUN \ pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -r homeassistant/requirements_all.txt \ + -r homeassistant/requirements_all.txt -c homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -e ./homeassistant \ && python3 -m compileall homeassistant/homeassistant From c079803fcbb402825a0bafec385663eda908c3b5 Mon Sep 17 00:00:00 2001 From: Barry Quiel Date: Thu, 13 May 2021 09:12:48 -0700 Subject: [PATCH 382/852] Powerwall add Current attribute (#50550) --- homeassistant/components/powerwall/__init__.py | 2 +- homeassistant/components/powerwall/config_flow.py | 2 +- homeassistant/components/powerwall/const.py | 1 + homeassistant/components/powerwall/manifest.json | 2 +- homeassistant/components/powerwall/sensor.py | 4 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 1792ca19fc8..0f63bf97986 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -176,7 +176,7 @@ async def _async_update_powerwall_data( def _login_and_fetch_base_info(power_wall: Powerwall, password: str): """Login to the powerwall and fetch the base info.""" if password is not None: - power_wall.login("", password) + power_wall.login(password) power_wall.detect_and_pin_version() return call_base_info(power_wall) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index bd4e49f45d3..420212a86ba 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) def _login_and_fetch_site_info(power_wall: Powerwall, password: str): """Login to the powerwall and fetch the base info.""" if password is not None: - power_wall.login("", password) + power_wall.login(password) power_wall.detect_and_pin_version() return power_wall.get_site_info() diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index 6dd4558a98c..f338d5f981d 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -12,6 +12,7 @@ ATTR_FREQUENCY = "frequency" ATTR_ENERGY_EXPORTED = "energy_exported_(in_kW)" ATTR_ENERGY_IMPORTED = "energy_imported_(in_kW)" ATTR_INSTANT_AVERAGE_VOLTAGE = "instant_average_voltage" +ATTR_INSTANT_TOTAL_CURRENT = "instant_total_current" ATTR_IS_ACTIVE = "is_active" STATUS_VERSION = "version" diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index d9f821df905..5cee6c1fd19 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.3.5"], + "requirements": ["tesla-powerwall==0.3.10"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ { diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 36f803e66d7..982952a4830 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -11,6 +11,7 @@ from .const import ( ATTR_ENERGY_IMPORTED, ATTR_FREQUENCY, ATTR_INSTANT_AVERAGE_VOLTAGE, + ATTR_INSTANT_TOTAL_CURRENT, ATTR_IS_ACTIVE, DOMAIN, ENERGY_KILO_WATT, @@ -144,6 +145,7 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): ATTR_FREQUENCY: round(meter.frequency, 1), ATTR_ENERGY_EXPORTED: meter.get_energy_exported(), ATTR_ENERGY_IMPORTED: meter.get_energy_imported(), - ATTR_INSTANT_AVERAGE_VOLTAGE: round(meter.avarage_voltage, 1), + ATTR_INSTANT_AVERAGE_VOLTAGE: round(meter.average_voltage, 1), + ATTR_INSTANT_TOTAL_CURRENT: meter.get_instant_total_current(), ATTR_IS_ACTIVE: meter.is_active(), } diff --git a/requirements_all.txt b/requirements_all.txt index 43e2cd5e846..4b83ee4e645 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2220,7 +2220,7 @@ temperusb==1.5.3 # tensorflow==2.3.0 # homeassistant.components.powerwall -tesla-powerwall==0.3.5 +tesla-powerwall==0.3.10 # homeassistant.components.tesla teslajsonpy==0.18.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3db0b015696..9228f6cbf7b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1189,7 +1189,7 @@ systembridge==1.1.5 tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.3.5 +tesla-powerwall==0.3.10 # homeassistant.components.tesla teslajsonpy==0.18.3 From 52edf9ac356029438dee0741a5ba9d419decd590 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 May 2021 12:35:24 -0500 Subject: [PATCH 383/852] Ensure isy994 is only discovered once (#50577) The formatting of the mac was different between dhcp and ssdp --- homeassistant/components/isy994/config_flow.py | 5 ++++- tests/components/isy994/test_config_flow.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 502008ff0ab..248d2d9c520 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -155,7 +155,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): friendly_name = discovery_info[HOSTNAME] url = f"http://{discovery_info[IP_ADDRESS]}" mac = discovery_info[MAC_ADDRESS] - await self.async_set_unique_id(mac) + isy_mac = ( + f"{mac[0:2]}:{mac[2:4]}:{mac[4:6]}:{mac[6:8]}:{mac[8:10]}:{mac[10:12]}" + ) + await self.async_set_unique_id(isy_mac) self._abort_if_unique_id_configured() self.discovered_conf = { diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index bf08e6526ba..51750d42718 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -61,7 +61,8 @@ MOCK_IMPORT_FULL_CONFIG = { } MOCK_DEVICE_NAME = "Name of the device" -MOCK_UUID = "CE:FB:72:31:B7:B9" +MOCK_UUID = "ce:fb:72:31:b7:b9" +MOCK_MAC = "cefb7231b7b9" MOCK_VALIDATED_RESPONSE = {"name": MOCK_DEVICE_NAME, "uuid": MOCK_UUID} PATCH_CONFIGURATION = "homeassistant.components.isy994.config_flow.Configuration" @@ -331,7 +332,7 @@ async def test_form_dhcp(hass: HomeAssistant): data={ dhcp.IP_ADDRESS: "1.2.3.4", dhcp.HOSTNAME: "isy994-ems", - dhcp.MAC_ADDRESS: MOCK_UUID, + dhcp.MAC_ADDRESS: MOCK_MAC, }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM From 47d4928d62ac56825fae6a1b76f438c80a2bac2a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 13 May 2021 19:35:58 +0200 Subject: [PATCH 384/852] Revert "Use requirements for constraints" (#50576) This reverts commit 136b34af20588f101d6ef11c53a4df6377536151. --- .github/workflows/wheels.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 796b4b2c219..93d2827fc51 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -162,6 +162,6 @@ jobs: apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev" pip: "Cython;numpy;scikit-build" skip-binary: aiohttp - constraints: "requirements_all.txt" + constraints: "homeassistant/package_constraints.txt" requirements-diff: 'requirements_diff.txt' requirements: "requirements_all.txt" diff --git a/Dockerfile b/Dockerfile index dcf3c9979d5..6bcb080a06e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ WORKDIR /usr/src COPY . homeassistant/ RUN \ pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -r homeassistant/requirements_all.txt -c homeassistant/requirements_all.txt \ + -r homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -e ./homeassistant \ && python3 -m compileall homeassistant/homeassistant From 6adbc702ebbcdb610bfcd864166513e569870d31 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 13 May 2021 20:14:00 +0200 Subject: [PATCH 385/852] Bump `brother` library (#50572) --- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index e2c1d4e9aff..2555490721e 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==1.0.0"], + "requirements": ["brother==1.0.1"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 4b83ee4e645..4c404819e61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -393,7 +393,7 @@ bravia-tv==1.0.8 broadlink==0.17.0 # homeassistant.components.brother -brother==1.0.0 +brother==1.0.1 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9228f6cbf7b..efa49b9afbe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ bravia-tv==1.0.8 broadlink==0.17.0 # homeassistant.components.brother -brother==1.0.0 +brother==1.0.1 # homeassistant.components.bsblan bsblan==0.4.0 From e956a726a0dd31643262b75d1380a2aa0db0b85f Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 13 May 2021 15:43:52 -0400 Subject: [PATCH 386/852] Fix SonarrEntity docstring (#50568) --- homeassistant/components/sonarr/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index bf5b2456b66..730ba857c49 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -114,7 +114,7 @@ class SonarrEntity(Entity): icon: str, enabled_default: bool = True, ) -> None: - """Initialize the Sonar entity.""" + """Initialize the Sonarr entity.""" self._entry_id = entry_id self._device_id = device_id self._enabled_default = enabled_default From 35f304450c534dfe7dada24ba3ba2fb5411b2c2e Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Thu, 13 May 2021 22:26:11 +0100 Subject: [PATCH 387/852] Enable type checks for stream component (#50527) * Enable type checks for stream component * Fix pylint --- homeassistant/components/stream/__init__.py | 8 ++++--- homeassistant/components/stream/core.py | 24 +++++++++++--------- homeassistant/components/stream/fmp4utils.py | 7 +++++- homeassistant/components/stream/hls.py | 4 ++-- homeassistant/components/stream/recorder.py | 8 ++++--- homeassistant/components/stream/worker.py | 7 +++--- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 8 files changed, 35 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 63c8439d43a..67bfe404d7d 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -14,6 +14,8 @@ are no active output formats, the background worker is shut down and access tokens are expired. Alternatively, a Stream can be configured with keepalive to always keep workers active. """ +from __future__ import annotations + import logging import re import secrets @@ -34,7 +36,7 @@ from .const import ( STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, ) -from .core import PROVIDERS, IdleTimer +from .core import PROVIDERS, IdleTimer, StreamOutput from .hls import async_setup_hls _LOGGER = logging.getLogger(__name__) @@ -118,7 +120,7 @@ class Stream: self.access_token = None self._thread = None self._thread_quit = threading.Event() - self._outputs = {} + self._outputs: dict[str, StreamOutput] = {} self._fast_restart_once = False if self.options is None: @@ -274,4 +276,4 @@ class Stream: num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) # Wait for latest segment, then add the lookback await hls.recv() - recorder.prepend(list(hls.get_segment())[-num_segments:]) + recorder.prepend(list(hls.get_segments())[-num_segments:]) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 0e513d3ae81..ae96d80af0b 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import deque import io -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Callable from aiohttp import web import attr @@ -95,12 +95,12 @@ class StreamOutput: """Initialize a stream output.""" self._hass = hass self._idle_timer = idle_timer - self._cursor = None + self._cursor: int | None = None self._event = asyncio.Event() - self._segments = deque(maxlen=deque_maxlen) + self._segments: deque[Segment] = deque(maxlen=deque_maxlen) @property - def name(self) -> str: + def name(self) -> str | None: """Return provider name.""" return None @@ -123,19 +123,21 @@ class StreamOutput: durations = [s.duration for s in self._segments] return round(max(durations)) or 1 - def get_segment(self, sequence: int = None) -> Any: - """Retrieve a specific segment, or the whole list.""" + def get_segment(self, sequence: int) -> Segment | None: + """Retrieve a specific segment.""" self._idle_timer.awake() - if not sequence: - return self._segments - for segment in self._segments: if segment.sequence == sequence: return segment return None - async def recv(self) -> Segment: + def get_segments(self) -> deque[Segment]: + """Retrieve all segments.""" + self._idle_timer.awake() + return self._segments + + async def recv(self) -> Segment | None: """Wait for and retrieve the latest segment.""" last_segment = max(self.segments, default=0) if self._cursor is None or self._cursor <= last_segment: @@ -144,7 +146,7 @@ class StreamOutput: if not self._segments: return None - segment = self.get_segment()[-1] + segment = self.get_segments()[-1] self._cursor = segment.sequence return segment diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 1838b3fc88b..ad5b100ce77 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -1,8 +1,13 @@ """Utilities to help convert mp4s to fmp4s.""" +from __future__ import annotations + +from collections.abc import Generator import io -def find_box(segment: io.BytesIO, target_type: bytes, box_start: int = 0) -> int: +def find_box( + segment: io.BytesIO, target_type: bytes, box_start: int = 0 +) -> Generator[int, None, None]: """Find location of first box (or sub_box if box_start provided) of given type.""" if box_start == 0: box_end = segment.seek(0, io.SEEK_END) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index ffeae4dbffd..42f7f2dbfa3 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -75,7 +75,7 @@ class HlsPlaylistView(StreamView): @staticmethod def render_playlist(track): """Render playlist.""" - segments = list(track.get_segment())[-NUM_PLAYLIST_SEGMENTS:] + segments = list(track.get_segments())[-NUM_PLAYLIST_SEGMENTS:] if not segments: return [] @@ -125,7 +125,7 @@ class HlsInitView(StreamView): async def handle(self, request, stream, sequence): """Return init.mp4.""" track = stream.add_provider("hls") - segments = track.get_segment() + segments = track.get_segments() if not segments: return web.HTTPNotFound() headers = {"Content-Type": "video/mp4"} diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index f5393078ab9..085a6448597 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -7,6 +7,7 @@ import os import threading import av +from av.container import OutputContainer from homeassistant.core import HomeAssistant, callback @@ -31,8 +32,8 @@ def recorder_save_worker(file_out: str, segments: deque[Segment]): if not os.path.exists(os.path.dirname(file_out)): os.makedirs(os.path.dirname(file_out), exist_ok=True) - pts_adjuster = {"video": None, "audio": None} - output = None + pts_adjuster: dict[str, int | None] = {"video": None, "audio": None} + output: OutputContainer | None = None output_v = None output_a = None @@ -100,7 +101,8 @@ def recorder_save_worker(file_out: str, segments: deque[Segment]): source.close() - output.close() + if output is not None: + output.close() @PROVIDERS.register("recorder") diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index fb3562c1b53..05dc0b076a4 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -1,4 +1,6 @@ """Provides the worker thread needed for processing streams.""" +from __future__ import annotations + from collections import deque import io import logging @@ -15,7 +17,7 @@ from .const import ( SEGMENT_CONTAINER_FORMAT, STREAM_TIMEOUT, ) -from .core import Segment, StreamBuffer +from .core import Segment, StreamBuffer, StreamOutput _LOGGER = logging.getLogger(__name__) @@ -56,8 +58,7 @@ class SegmentBuffer: self._video_stream = None self._audio_stream = None self._outputs_callback = outputs_callback - # Each element is a StreamOutput - self._outputs = [] + self._outputs: list[StreamOutput] = [] self._sequence = 0 self._segment_start_pts = None self._stream_buffer = None diff --git a/mypy.ini b/mypy.ini index d501c482dd6..94cd59e4956 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1154,9 +1154,6 @@ ignore_errors = true [mypy-homeassistant.components.spotify.*] ignore_errors = true -[mypy-homeassistant.components.stream.*] -ignore_errors = true - [mypy-homeassistant.components.stt.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 4e75fa6e33e..6c8fba912ac 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -197,7 +197,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.songpal.*", "homeassistant.components.sonos.*", "homeassistant.components.spotify.*", - "homeassistant.components.stream.*", "homeassistant.components.stt.*", "homeassistant.components.surepetcare.*", "homeassistant.components.switchbot.*", From a16629601afc158dc606e37fb4ed50ee2eabe674 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 14 May 2021 00:39:53 +0200 Subject: [PATCH 388/852] Add support for tracking entity attributes in ESPHome (#50528) --- homeassistant/components/esphome/__init__.py | 59 +++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 7d87c6bc736..94e0089042d 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -140,34 +140,65 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) - async def send_home_assistant_state_event(event: Event) -> None: - """Forward Home Assistant states updates to ESPHome.""" - new_state = event.data.get("new_state") - if new_state is None: - return - entity_id = event.data.get("entity_id") - await cli.send_home_assistant_state(entity_id, None, new_state.state) - async def _send_home_assistant_state( - entity_id: str, new_state: State | None + entity_id: str, attribute: str | None, state: State | None ) -> None: """Forward Home Assistant states to ESPHome.""" - await cli.send_home_assistant_state(entity_id, None, new_state.state) + if state is None or (attribute and attribute not in state.attributes): + return + + send_state = state.state + if attribute: + send_state = state.attributes[attribute] + # ESPHome only handles "on"/"off" for boolean values + if isinstance(send_state, bool): + send_state = "on" if send_state else "off" + + await cli.send_home_assistant_state(entity_id, attribute, str(send_state)) @callback def async_on_state_subscription( entity_id: str, attribute: str | None = None ) -> None: """Subscribe and forward states for requested entities.""" + + async def send_home_assistant_state_event(event: Event) -> None: + """Forward Home Assistant states updates to ESPHome.""" + + # Only communicate changes to the state or attribute tracked + if ( + "old_state" in event.data + and "new_state" in event.data + and ( + ( + not attribute + and event.data["old_state"].state + == event.data["new_state"].state + ) + or ( + attribute + and attribute in event.data["old_state"].attributes + and attribute in event.data["new_state"].attributes + and event.data["old_state"].attributes[attribute] + == event.data["new_state"].attributes[attribute] + ) + ) + ): + return + + await _send_home_assistant_state( + event.data["entity_id"], attribute, event.data.get("new_state") + ) + unsub = async_track_state_change_event( hass, [entity_id], send_home_assistant_state_event ) entry_data.disconnect_callbacks.append(unsub) - new_state = hass.states.get(entity_id) - if new_state is None: - return + # Send initial state - hass.async_create_task(_send_home_assistant_state(entity_id, new_state)) + hass.async_create_task( + _send_home_assistant_state(entity_id, attribute, hass.states.get(entity_id)) + ) async def on_login() -> None: """Subscribe to states and list entities on successful API login.""" From 42d1ec753dc47c4c27caf3347bcdfa2045bf61e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 May 2021 20:16:20 -0500 Subject: [PATCH 389/852] Small tweaks to improve homekit_controller startup time (#50590) --- .../homekit_controller/connection.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index cc9ba7b620e..eaaab390136 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -13,6 +13,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.event import async_track_time_interval from .const import ( @@ -179,7 +180,8 @@ class HKDevice: return True - async def async_create_devices(self): + @callback + def async_create_devices(self): """ Build device registry entries for all accessories paired with the bridge. @@ -187,7 +189,7 @@ class HKDevice: might not have any entities attached to it. Secondly there are stateless entities like doorbells and remote controls. """ - device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(self.hass) devices = {} @@ -248,7 +250,7 @@ class HKDevice: await self.async_load_platforms() - await self.async_create_devices() + self.async_create_devices() # Load any triggers for this config entry await async_setup_triggers_for_entry(self.hass, self.config_entry) @@ -260,8 +262,6 @@ class HKDevice: await self.async_update() - return True - async def async_unload(self): """Stop interacting with device and prepare for removal from hass.""" if self._polling_interval_remover: @@ -365,17 +365,23 @@ class HKDevice: async def async_load_platforms(self): """Load any platforms needed by this HomeKit device.""" + tasks = [] for accessory in self.accessories: for service in accessory["services"]: stype = ServicesTypes.get_short(service["type"].upper()) if stype in HOMEKIT_ACCESSORY_DISPATCH: platform = HOMEKIT_ACCESSORY_DISPATCH[stype] - await self.async_load_platform(platform) + if platform not in self.platforms: + tasks.append(self.async_load_platform(platform)) for char in service["characteristics"]: if char["type"].upper() in CHARACTERISTIC_PLATFORMS: platform = CHARACTERISTIC_PLATFORMS[char["type"].upper()] - await self.async_load_platform(platform) + if platform not in self.platforms: + tasks.append(self.async_load_platform(platform)) + + if tasks: + await asyncio.gather(*tasks) async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" From d6e9f094c4e6fa83deb6244fd6431213439ee7f3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 14 May 2021 05:30:15 +0200 Subject: [PATCH 390/852] Cleanup unused CONFIG_SCHEMA from kmtronic (#50567) --- homeassistant/components/kmtronic/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index 3b8da77faab..7dd5b087a87 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -6,7 +6,6 @@ import aiohttp import async_timeout from pykmtronic.auth import Auth from pykmtronic.hub import KMTronicHubAPI -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -16,8 +15,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER, UPDATE_LISTENER -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - PLATFORMS = ["switch"] _LOGGER = logging.getLogger(__name__) From dbf7430003895867de659ffaab91ee4d763810e8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 14 May 2021 05:31:48 +0200 Subject: [PATCH 391/852] Bump pymodbus to v2.5.2 (#50582) Solves a serial - rs-485 adapter issue. --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 0833292a7e3..f5c7bf2df4e 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,7 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==2.5.1"], + "requirements": ["pymodbus==2.5.2"], "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 4c404819e61..bdd9b8a1d21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1557,7 +1557,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.1 +pymodbus==2.5.2 # homeassistant.components.monoprice pymonoprice==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index efa49b9afbe..bddd762ff90 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -865,7 +865,7 @@ pymfy==0.9.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.1 +pymodbus==2.5.2 # homeassistant.components.monoprice pymonoprice==0.3 From e8d7d962315e3cf3e9e160ba168a921cd4444cff Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 14 May 2021 11:32:06 +0800 Subject: [PATCH 392/852] Roll back #47852 (shield httpx in generic) (#50562) --- homeassistant/components/generic/camera.py | 31 +++++++--------------- tests/components/generic/test_camera.py | 2 +- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 1ec7f0874e0..56b490e165a 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -125,45 +125,32 @@ class GenericCamera(Camera): ).result() async def async_camera_image(self): - """Wrap _async_camera_image with an asyncio.shield.""" - # Shield the request because of https://github.com/encode/httpx/issues/1461 - try: - self._last_url, self._last_image = await asyncio.shield( - self._async_camera_image() - ) - except asyncio.CancelledError as err: - _LOGGER.warning("Timeout getting camera image from %s", self._name) - raise err - return self._last_image - - async def _async_camera_image(self): """Return a still image response from the camera.""" try: url = self._still_image_url.async_render(parse_result=False) except TemplateError as err: _LOGGER.error("Error parsing template %s: %s", self._still_image_url, err) - return self._last_url, self._last_image + return self._last_image if url == self._last_url and self._limit_refetch: - return self._last_url, self._last_image - response = None + return self._last_image + try: async_client = get_async_client(self.hass, verify_ssl=self.verify_ssl) response = await async_client.get( url, auth=self._auth, timeout=GET_IMAGE_TIMEOUT ) response.raise_for_status() - image = response.content + self._last_image = response.content except httpx.TimeoutException: _LOGGER.error("Timeout getting camera image from %s", self._name) - return self._last_url, self._last_image + return self._last_image except (httpx.RequestError, httpx.HTTPStatusError) as err: _LOGGER.error("Error getting new camera image from %s: %s", self._name, err) - return self._last_url, self._last_image - finally: - if response: - await response.aclose() - return url, image + return self._last_image + + self._last_url = url + return self._last_image @property def name(self): diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index deb8049da33..1a1edc4eece 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -440,7 +440,7 @@ async def test_timeout_cancelled(hass, hass_client): respx.get("http://example.com").respond(text="not hello world") with patch( - "homeassistant.components.generic.camera.GenericCamera._async_camera_image", + "homeassistant.components.generic.camera.GenericCamera.async_camera_image", side_effect=asyncio.CancelledError(), ): resp = await client.get("/api/camera_proxy/camera.config_test") From 122741b91425d2c90231aea284a61cd186ccc500 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Thu, 13 May 2021 20:52:52 -0700 Subject: [PATCH 393/852] Add lock platform to the Mazda integration (#50548) --- homeassistant/components/mazda/__init__.py | 21 ++++--- .../components/mazda/device_tracker.py | 9 +-- homeassistant/components/mazda/lock.py | 51 ++++++++++++++++ homeassistant/components/mazda/manifest.json | 2 +- homeassistant/components/mazda/sensor.py | 41 +++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/mazda/__init__.py | 4 +- tests/components/mazda/test_init.py | 56 ++++++++++++++++-- tests/components/mazda/test_lock.py | 58 +++++++++++++++++++ 10 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/mazda/lock.py create mode 100644 tests/components/mazda/test_lock.py diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index f6e31fa4357..c34dfa10682 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -28,7 +28,7 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["device_tracker", "sensor"] +PLATFORMS = ["device_tracker", "lock", "sensor"] async def with_timeout(task, timeout_seconds=10): @@ -117,26 +117,31 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class MazdaEntity(CoordinatorEntity): """Defines a base Mazda entity.""" - def __init__(self, coordinator, index): + def __init__(self, client, coordinator, index): """Initialize the Mazda entity.""" super().__init__(coordinator) + self.client = client self.index = index self.vin = self.coordinator.data[self.index]["vin"] + self.vehicle_id = self.coordinator.data[self.index]["id"] + + @property + def data(self): + """Shortcut to access coordinator data for the entity.""" + return self.coordinator.data[self.index] @property def device_info(self): """Return device info for the Mazda entity.""" - data = self.coordinator.data[self.index] return { "identifiers": {(DOMAIN, self.vin)}, "name": self.get_vehicle_name(), "manufacturer": "Mazda", - "model": f"{data['modelYear']} {data['carlineName']}", + "model": f"{self.data['modelYear']} {self.data['carlineName']}", } def get_vehicle_name(self): """Return the vehicle name, to be used as a prefix for names of other entities.""" - data = self.coordinator.data[self.index] - if "nickname" in data and len(data["nickname"]) > 0: - return data["nickname"] - return f"{data['modelYear']} {data['carlineName']}" + if "nickname" in self.data and len(self.data["nickname"]) > 0: + return self.data["nickname"] + return f"{self.data['modelYear']} {self.data['carlineName']}" diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py index ea05d2c8c8b..ffe36a1215e 100644 --- a/homeassistant/components/mazda/device_tracker.py +++ b/homeassistant/components/mazda/device_tracker.py @@ -3,17 +3,18 @@ from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity from . import MazdaEntity -from .const import DATA_COORDINATOR, DOMAIN +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the device tracker platform.""" + client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] entities = [] for index, _ in enumerate(coordinator.data): - entities.append(MazdaDeviceTracker(coordinator, index)) + entities.append(MazdaDeviceTracker(client, coordinator, index)) async_add_entities(entities) @@ -50,9 +51,9 @@ class MazdaDeviceTracker(MazdaEntity, TrackerEntity): @property def latitude(self): """Return latitude value of the device.""" - return self.coordinator.data[self.index]["status"]["latitude"] + return self.data["status"]["latitude"] @property def longitude(self): """Return longitude value of the device.""" - return self.coordinator.data[self.index]["status"]["longitude"] + return self.data["status"]["longitude"] diff --git a/homeassistant/components/mazda/lock.py b/homeassistant/components/mazda/lock.py new file mode 100644 index 00000000000..fb485bd7b13 --- /dev/null +++ b/homeassistant/components/mazda/lock.py @@ -0,0 +1,51 @@ +"""Platform for Mazda lock integration.""" + +from homeassistant.components.lock import LockEntity + +from . import MazdaEntity +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the lock platform.""" + client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + + entities = [] + + for index, _ in enumerate(coordinator.data): + entities.append(MazdaLock(client, coordinator, index)) + + async_add_entities(entities) + + +class MazdaLock(MazdaEntity, LockEntity): + """Class for the lock.""" + + @property + def name(self): + """Return the name of the entity.""" + vehicle_name = self.get_vehicle_name() + return f"{vehicle_name} Lock" + + @property + def unique_id(self): + """Return a unique identifier for this entity.""" + return self.vin + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self.client.get_assumed_lock_state(self.vehicle_id) + + async def async_lock(self, **kwargs): + """Lock the vehicle doors.""" + await self.client.lock_doors(self.vehicle_id) + + self.async_write_ha_state() + + async def async_unlock(self, **kwargs): + """Unlock the vehicle doors.""" + await self.client.unlock_doors(self.vehicle_id) + + self.async_write_ha_state() diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 9c5fb2c6b46..4ca4384e952 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.0.9"], + "requirements": ["pymazda==0.1.5"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 7382347e6de..673c965544b 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -9,23 +9,24 @@ from homeassistant.const import ( ) from . import MazdaEntity -from .const import DATA_COORDINATOR, DOMAIN +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the sensor platform.""" + client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] entities = [] for index, _ in enumerate(coordinator.data): - entities.append(MazdaFuelRemainingSensor(coordinator, index)) - entities.append(MazdaFuelDistanceSensor(coordinator, index)) - entities.append(MazdaOdometerSensor(coordinator, index)) - entities.append(MazdaFrontLeftTirePressureSensor(coordinator, index)) - entities.append(MazdaFrontRightTirePressureSensor(coordinator, index)) - entities.append(MazdaRearLeftTirePressureSensor(coordinator, index)) - entities.append(MazdaRearRightTirePressureSensor(coordinator, index)) + entities.append(MazdaFuelRemainingSensor(client, coordinator, index)) + entities.append(MazdaFuelDistanceSensor(client, coordinator, index)) + entities.append(MazdaOdometerSensor(client, coordinator, index)) + entities.append(MazdaFrontLeftTirePressureSensor(client, coordinator, index)) + entities.append(MazdaFrontRightTirePressureSensor(client, coordinator, index)) + entities.append(MazdaRearLeftTirePressureSensor(client, coordinator, index)) + entities.append(MazdaRearRightTirePressureSensor(client, coordinator, index)) async_add_entities(entities) @@ -57,7 +58,7 @@ class MazdaFuelRemainingSensor(MazdaEntity, SensorEntity): @property def state(self): """Return the state of the sensor.""" - return self.coordinator.data[self.index]["status"]["fuelRemainingPercent"] + return self.data["status"]["fuelRemainingPercent"] class MazdaFuelDistanceSensor(MazdaEntity, SensorEntity): @@ -89,9 +90,7 @@ class MazdaFuelDistanceSensor(MazdaEntity, SensorEntity): @property def state(self): """Return the state of the sensor.""" - fuel_distance_km = self.coordinator.data[self.index]["status"][ - "fuelDistanceRemainingKm" - ] + fuel_distance_km = self.data["status"]["fuelDistanceRemainingKm"] return ( None if fuel_distance_km is None @@ -130,7 +129,7 @@ class MazdaOdometerSensor(MazdaEntity, SensorEntity): @property def state(self): """Return the state of the sensor.""" - odometer_km = self.coordinator.data[self.index]["status"]["odometerKm"] + odometer_km = self.data["status"]["odometerKm"] return ( None if odometer_km is None @@ -165,9 +164,7 @@ class MazdaFrontLeftTirePressureSensor(MazdaEntity, SensorEntity): @property def state(self): """Return the state of the sensor.""" - tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][ - "frontLeftTirePressurePsi" - ] + tire_pressure = self.data["status"]["tirePressure"]["frontLeftTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) @@ -198,9 +195,7 @@ class MazdaFrontRightTirePressureSensor(MazdaEntity, SensorEntity): @property def state(self): """Return the state of the sensor.""" - tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][ - "frontRightTirePressurePsi" - ] + tire_pressure = self.data["status"]["tirePressure"]["frontRightTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) @@ -231,9 +226,7 @@ class MazdaRearLeftTirePressureSensor(MazdaEntity, SensorEntity): @property def state(self): """Return the state of the sensor.""" - tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][ - "rearLeftTirePressurePsi" - ] + tire_pressure = self.data["status"]["tirePressure"]["rearLeftTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) @@ -264,7 +257,5 @@ class MazdaRearRightTirePressureSensor(MazdaEntity, SensorEntity): @property def state(self): """Return the state of the sensor.""" - tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][ - "rearRightTirePressurePsi" - ] + tire_pressure = self.data["status"]["tirePressure"]["rearRightTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) diff --git a/requirements_all.txt b/requirements_all.txt index bdd9b8a1d21..1eee20651cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1539,7 +1539,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.0.9 +pymazda==0.1.5 # homeassistant.components.mediaroom pymediaroom==0.6.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bddd762ff90..0bef6802cc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -853,7 +853,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.0.9 +pymazda==0.1.5 # homeassistant.components.melcloud pymelcloud==2.5.2 diff --git a/tests/components/mazda/__init__.py b/tests/components/mazda/__init__.py index f7a267a5110..9676b2b5765 100644 --- a/tests/components/mazda/__init__.py +++ b/tests/components/mazda/__init__.py @@ -42,6 +42,8 @@ async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfig ) client_mock.get_vehicles = AsyncMock(return_value=get_vehicles_fixture) client_mock.get_vehicle_status = AsyncMock(return_value=get_vehicle_status_fixture) + client_mock.lock_doors = AsyncMock() + client_mock.unlock_doors = AsyncMock() with patch( "homeassistant.components.mazda.config_flow.MazdaAPI", @@ -50,4 +52,4 @@ async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfig assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - return config_entry + return client_mock diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index 1b062dd84f1..c8c631b48af 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -12,7 +12,12 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION +from homeassistant.const import ( + CONF_EMAIL, + CONF_PASSWORD, + CONF_REGION, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.util import dt as dt_util @@ -102,14 +107,57 @@ async def test_update_auth_failure(hass: HomeAssistant): assert flows[0]["step_id"] == "user" +async def test_update_general_failure(hass: HomeAssistant): + """Test general failure during data update.""" + get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json")) + get_vehicle_status_fixture = json.loads( + load_fixture("mazda/get_vehicle_status.json") + ) + + with patch( + "homeassistant.components.mazda.MazdaAPI.validate_credentials", + return_value=True, + ), patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicles", + return_value=get_vehicles_fixture, + ), patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicle_status", + return_value=get_vehicle_status_fixture, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_LOADED + + with patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicles", + side_effect=Exception("Unknown exception"), + ): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=61)) + await hass.async_block_till_done() + + entity = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage") + assert entity is not None + assert entity.state == STATE_UNAVAILABLE + + async def test_unload_config_entry(hass: HomeAssistant) -> None: """Test the Mazda configuration entry unloading.""" - entry = await init_integration(hass) + await init_integration(hass) assert hass.data[DOMAIN] - await hass.config_entries.async_unload(entry.entry_id) + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entries[0].state == ENTRY_STATE_NOT_LOADED async def test_device_nickname(hass): diff --git a/tests/components/mazda/test_lock.py b/tests/components/mazda/test_lock.py new file mode 100644 index 00000000000..1230e624cdd --- /dev/null +++ b/tests/components/mazda/test_lock.py @@ -0,0 +1,58 @@ +"""The lock tests for the Mazda Connected Services integration.""" + +from homeassistant.components.lock import ( + DOMAIN as LOCK_DOMAIN, + SERVICE_LOCK, + SERVICE_UNLOCK, + STATE_LOCKED, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME +from homeassistant.helpers import entity_registry as er + +from tests.components.mazda import init_integration + + +async def test_lock_setup(hass): + """Test locking and unlocking the vehicle.""" + await init_integration(hass) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("lock.my_mazda3_lock") + assert entry + assert entry.unique_id == "JM000000000000000" + + state = hass.states.get("lock.my_mazda3_lock") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Lock" + + assert state.state == STATE_LOCKED + + +async def test_locking(hass): + """Test locking the vehicle.""" + client_mock = await init_integration(hass) + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.my_mazda3_lock"}, + blocking=True, + ) + await hass.async_block_till_done() + + client_mock.lock_doors.assert_called_once() + + +async def test_unlocking(hass): + """Test unlocking the vehicle.""" + client_mock = await init_integration(hass) + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.my_mazda3_lock"}, + blocking=True, + ) + await hass.async_block_till_done() + + client_mock.unlock_doors.assert_called_once() From aef24a807e72a3548dfbd594a8108c0ee52b5c56 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 May 2021 22:33:18 -0700 Subject: [PATCH 394/852] Yeelight: Do not log errors when cannot connect (#50592) --- homeassistant/components/yeelight/config_flow.py | 6 +++++- tests/components/yeelight/test_config_flow.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index d6902abcf5a..0b0fe0d96c1 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -61,7 +61,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if progress.get("context", {}).get(CONF_HOST) == self._discovered_ip: return self.async_abort(reason="already_in_progress") - self._discovered_model = await self._async_try_connect(self._discovered_ip) + try: + self._discovered_model = await self._async_try_connect(self._discovered_ip) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + if not self.unique_id: return self.async_abort(reason="cannot_connect") diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 8cc49c9799a..dd5ae85e89e 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -19,6 +19,7 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) +from homeassistant.components.yeelight.config_flow import CannotConnect from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM @@ -323,6 +324,15 @@ async def test_discovered_by_homekit_and_dhcp(hass): assert result3["type"] == RESULT_TYPE_ABORT assert result3["reason"] == "already_in_progress" + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", side_effect=CannotConnect): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.5", "macaddress": "00:00:00:00:00:01"}, + ) + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "cannot_connect" + @pytest.mark.parametrize( "source, data", From de5472403b8dd641b602084bd28bf5fe510e8b87 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Fri, 14 May 2021 06:36:49 +0100 Subject: [PATCH 395/852] Use mypy-friendly conditional import for zoneinfo (#50444) --- homeassistant/util/dt.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 28aebc5db47..656f77b3289 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -5,17 +5,18 @@ import bisect from contextlib import suppress import datetime as dt import re -from typing import Any - -try: - import zoneinfo -except ImportError: - from backports import zoneinfo +import sys +from typing import Any, cast import ciso8601 from homeassistant.const import MATCH_ALL +if sys.version_info[:2] >= (3, 9): + import zoneinfo # pylint: disable=import-error +else: + from backports import zoneinfo # pylint: disable=import-error + DATE_STR_FORMAT = "%Y-%m-%d" UTC = dt.timezone.utc DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc @@ -49,7 +50,8 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: Async friendly. """ try: - return zoneinfo.ZoneInfo(time_zone_str) # type: ignore + # Cast can be removed when mypy is switched to Python 3.9. + return cast(dt.tzinfo, zoneinfo.ZoneInfo(time_zone_str)) except zoneinfo.ZoneInfoNotFoundError: return None From 4c68518b18be5bcf40c38a7f1a573e5f1bebc75d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 14 May 2021 09:38:44 +0200 Subject: [PATCH 396/852] Bump accuweather library (#50573) --- homeassistant/components/accuweather/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 068b0fc83a9..04b1b4b39c6 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -2,7 +2,7 @@ "domain": "accuweather", "name": "AccuWeather", "documentation": "https://www.home-assistant.io/integrations/accuweather/", - "requirements": ["accuweather==0.1.1"], + "requirements": ["accuweather==0.2.0"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 1eee20651cf..da8efdf77d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -93,7 +93,7 @@ WazeRouteCalculator==0.12 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.1.1 +accuweather==0.2.0 # homeassistant.components.bmp280 adafruit-circuitpython-bmp280==3.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0bef6802cc2..33dbadc4856 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ WazeRouteCalculator==0.12 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.1.1 +accuweather==0.2.0 # homeassistant.components.androidtv adb-shell[async]==0.3.1 From 207ee39d0057b35aa3ccb50028f8c300f9ff10c8 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Fri, 14 May 2021 08:50:41 +0100 Subject: [PATCH 397/852] Bump growattServer library (#50588) --- homeassistant/components/growatt_server/manifest.json | 2 +- homeassistant/components/growatt_server/sensor.py | 4 +--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index 94fc293b8d7..8b4a82d7b99 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==1.0.0"], + "requirements": ["growattServer==1.0.1"], "codeowners": ["@indykoning", "@muppet3000"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 0ccdc9425f6..881f0f46480 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -738,9 +738,7 @@ class GrowattData: self.device_id, self.plant_id ) - mix_detail = self.api.mix_detail( - self.device_id, self.plant_id, date=datetime.datetime.now() - ) + mix_detail = self.api.mix_detail(self.device_id, self.plant_id) # Get the chart data and work out the time of the last entry, use this as the last time data was published to the Growatt Server mix_chart_entries = mix_detail["chartData"] sorted_keys = sorted(mix_chart_entries) diff --git a/requirements_all.txt b/requirements_all.txt index da8efdf77d3..30c01cf80c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -708,7 +708,7 @@ greeneye_monitor==2.1 greenwavereality==0.5.1 # homeassistant.components.growatt_server -growattServer==1.0.0 +growattServer==1.0.1 # homeassistant.components.gstreamer gstreamer-player==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33dbadc4856..3de8763efff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -387,7 +387,7 @@ googlemaps==2.5.1 greeclimate==0.11.4 # homeassistant.components.growatt_server -growattServer==1.0.0 +growattServer==1.0.1 # homeassistant.components.profiler guppy3==3.1.0 From 7ea23533cf2bf35b1637215f3a5b91338cabaf16 Mon Sep 17 00:00:00 2001 From: djtimca <60706061+djtimca@users.noreply.github.com> Date: Fri, 14 May 2021 04:02:54 -0400 Subject: [PATCH 398/852] Address late review for Omnilogic Switch (#50404) * Address previous PR comments. * Update all instances of async_schedule_update_ha_state to async_write_ha_state. --- homeassistant/components/omnilogic/common.py | 2 ++ homeassistant/components/omnilogic/sensor.py | 2 +- homeassistant/components/omnilogic/switch.py | 12 ++++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 645c24ac676..a4e8d4f491e 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -176,3 +176,5 @@ def check_guard(state_key, item, entity_setting): for guard_key, guard_value in guard_condition.items() ): return True + + return False diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index 24cbe82e17b..60ca685a2f8 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -136,7 +136,7 @@ class OmniLogicPumpSpeedSensor(OmnilogicSensor): self._unit = PERCENTAGE state = pump_speed elif pump_type == "DUAL": - self._unit = "" + self._unit = None if pump_speed == 0: state = "off" elif pump_speed == self.coordinator.data[self._item_id].get( diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py index ef4d2b32cc5..b6fec2e9f25 100644 --- a/homeassistant/components/omnilogic/switch.py +++ b/homeassistant/components/omnilogic/switch.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class OmniLogicSwitch(OmniLogicEntity, SwitchEntity): - """Define an Omnilogic Base Switch entity which will be instantiated through specific switch type entities.""" + """Define an Omnilogic Base Switch entity to be extended.""" def __init__( self, @@ -108,7 +108,7 @@ class OmniLogicRelayControl(OmniLogicSwitch): """Turn on the relay.""" self._state = True self._last_action = time.time() - self.async_schedule_update_ha_state() + self.async_write_ha_state() await self.coordinator.api.set_relay_valve( int(self._item_id[1]), @@ -121,7 +121,7 @@ class OmniLogicRelayControl(OmniLogicSwitch): """Turn off the relay.""" self._state = False self._last_action = time.time() - self.async_schedule_update_ha_state() + self.async_write_ha_state() await self.coordinator.api.set_relay_valve( int(self._item_id[1]), @@ -167,7 +167,7 @@ class OmniLogicPumpControl(OmniLogicSwitch): """Turn on the pump.""" self._state = True self._last_action = time.time() - self.async_schedule_update_ha_state() + self.async_write_ha_state() on_value = 100 @@ -185,7 +185,7 @@ class OmniLogicPumpControl(OmniLogicSwitch): """Turn off the pump.""" self._state = False self._last_action = time.time() - self.async_schedule_update_ha_state() + self.async_write_ha_state() if self._pump_type != "SINGLE": if "filterSpeed" in self.coordinator.data[self._item_id]: @@ -213,7 +213,7 @@ class OmniLogicPumpControl(OmniLogicSwitch): ) if success: - self.async_schedule_update_ha_state() + self.async_write_ha_state() else: raise OmniLogicException( From 19cdff10c33af7d92cbdcb043f53d197c8cc3866 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 14 May 2021 10:54:23 +0200 Subject: [PATCH 399/852] Add "close_comm_on_error" to modbus configuration (#50583) --- homeassistant/components/modbus/__init__.py | 2 ++ homeassistant/components/modbus/const.py | 1 + homeassistant/components/modbus/modbus.py | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8e0d9220718..8b9e37aa1e4 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -57,6 +57,7 @@ from .const import ( CONF_BAUDRATE, CONF_BYTESIZE, CONF_CLIMATES, + CONF_CLOSE_COMM_ON_ERROR, CONF_CURRENT_TEMP, CONF_CURRENT_TEMP_REGISTER_TYPE, CONF_DATA_COUNT, @@ -281,6 +282,7 @@ MODBUS_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, + vol.Optional(CONF_CLOSE_COMM_ON_ERROR, default=True): cv.boolean, vol.Optional(CONF_DELAY, default=0): cv.positive_int, vol.Optional(CONF_BINARY_SENSORS): vol.All( cv.ensure_list, [BINARY_SENSOR_SCHEMA] diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index d817108b4d3..15de884d5b9 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_BAUDRATE = "baudrate" CONF_BYTESIZE = "bytesize" CONF_CLIMATES = "climates" +CONF_CLOSE_COMM_ON_ERROR = "close_comm_on_error" CONF_COILS = "coils" CONF_CURRENT_TEMP = "current_temp_register" CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index d9db6cf980c..8610c6a855c 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -28,6 +28,7 @@ from .const import ( ATTR_VALUE, CONF_BAUDRATE, CONF_BYTESIZE, + CONF_CLOSE_COMM_ON_ERROR, CONF_PARITY, CONF_STOPBITS, DEFAULT_HUB, @@ -125,6 +126,7 @@ class ModbusHub: self._config_port = client_config[CONF_PORT] self._config_timeout = client_config[CONF_TIMEOUT] self._config_delay = client_config[CONF_DELAY] + self._config_reset_socket = client_config[CONF_CLOSE_COMM_ON_ERROR] Defaults.Timeout = client_config[CONF_TIMEOUT] if self._config_type == "serial": # serial configuration @@ -163,6 +165,7 @@ class ModbusHub: parity=self._config_parity, timeout=self._config_timeout, retry_on_empty=True, + reset_socket=self._config_reset_socket, ) elif self._config_type == "rtuovertcp": self._client = ModbusTcpClient( @@ -170,18 +173,21 @@ class ModbusHub: port=self._config_port, framer=ModbusRtuFramer, timeout=self._config_timeout, + reset_socket=self._config_reset_socket, ) elif self._config_type == "tcp": self._client = ModbusTcpClient( host=self._config_host, port=self._config_port, timeout=self._config_timeout, + reset_socket=self._config_reset_socket, ) elif self._config_type == "udp": self._client = ModbusUdpClient( host=self._config_host, port=self._config_port, timeout=self._config_timeout, + reset_socket=self._config_reset_socket, ) except ModbusException as exception_error: self._log_error(exception_error, error_state=False) From ca2b3fcc9e812eb74b3188d8dc2399fec8911041 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 14 May 2021 12:15:15 +0200 Subject: [PATCH 400/852] Upgrade evdev to 1.4.0 (#50601) --- homeassistant/components/keyboard_remote/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index 7e7525f6664..b63873bd165 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -2,7 +2,7 @@ "domain": "keyboard_remote", "name": "Keyboard Remote", "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", - "requirements": ["evdev==1.1.2", "aionotify==0.2.0"], + "requirements": ["evdev==1.4.0", "aionotify==0.2.0"], "codeowners": ["@bendavid"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 30c01cf80c1..e9ef1b26e76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -575,7 +575,7 @@ epsonprinter==0.0.9 eternalegypt==0.0.12 # homeassistant.components.keyboard_remote -# evdev==1.1.2 +# evdev==1.4.0 # homeassistant.components.evohome evohome-async==0.3.8 From 42df6750e218c4d0acd982d004aa23260af06ff8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 14 May 2021 13:19:24 +0200 Subject: [PATCH 401/852] Refactor AdGuard config flow tests (#50566) --- tests/components/adguard/test_config_flow.py | 113 +++++++++++-------- 1 file changed, 65 insertions(+), 48 deletions(-) diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 872f9e5807e..75d138f400c 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -2,8 +2,8 @@ import aiohttp from homeassistant import config_entries, data_entry_flow -from homeassistant.components.adguard import config_flow from homeassistant.components.adguard.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -30,9 +30,9 @@ FIXTURE_USER_INPUT = { async def test_show_authenticate_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" - flow = config_flow.AdGuardHomeFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -49,13 +49,14 @@ async def test_connection_error( exc=aiohttp.ClientError, ) - flow = config_flow.AdGuardHomeFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + assert result + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "user" + assert result.get("errors") == {"base": "cannot_connect"} async def test_full_flow_implementation( @@ -70,21 +71,30 @@ async def test_full_flow_implementation( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.AdGuardHomeFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == FIXTURE_USER_INPUT[CONF_HOST] - assert result["data"][CONF_HOST] == FIXTURE_USER_INPUT[CONF_HOST] - assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] - assert result["data"][CONF_PORT] == FIXTURE_USER_INPUT[CONF_PORT] - assert result["data"][CONF_SSL] == FIXTURE_USER_INPUT[CONF_SSL] - assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] - assert result["data"][CONF_VERIFY_SSL] == FIXTURE_USER_INPUT[CONF_VERIFY_SSL] + assert result + assert result.get("flow_id") + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=FIXTURE_USER_INPUT + ) + assert result2 + assert result2.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == FIXTURE_USER_INPUT[CONF_HOST] + + data = result2.get("data") + assert data + assert data[CONF_HOST] == FIXTURE_USER_INPUT[CONF_HOST] + assert data[CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + assert data[CONF_PORT] == FIXTURE_USER_INPUT[CONF_PORT] + assert data[CONF_SSL] == FIXTURE_USER_INPUT[CONF_SSL] + assert data[CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] + assert data[CONF_VERIFY_SSL] == FIXTURE_USER_INPUT[CONF_VERIFY_SSL] async def test_integration_already_exists(hass: HomeAssistant) -> None: @@ -98,8 +108,9 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: data={"host": "mock-adguard", "port": "3000"}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + assert result + assert result.get("type") == "abort" + assert result.get("reason") == "already_configured" async def test_hassio_already_configured(hass: HomeAssistant) -> None: @@ -113,8 +124,9 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000"}, context={"source": config_entries.SOURCE_HASSIO}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" async def test_hassio_ignored(hass: HomeAssistant) -> None: @@ -128,11 +140,9 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000"}, context={"source": config_entries.SOURCE_HASSIO}, ) - - assert "type" in result - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert "reason" in result - assert result["reason"] == "already_configured" + assert result + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" async def test_hassio_confirm( @@ -150,19 +160,25 @@ async def test_hassio_confirm( data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, context={"source": config_entries.SOURCE_HASSIO}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "hassio_confirm" - assert result["description_placeholders"] == {"addon": "AdGuard Home Addon"} + assert result + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "hassio_confirm" + assert result.get("description_placeholders") == {"addon": "AdGuard Home Addon"} - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "AdGuard Home Addon" - assert result["data"][CONF_HOST] == "mock-adguard" - assert result["data"][CONF_PASSWORD] is None - assert result["data"][CONF_PORT] == 3000 - assert result["data"][CONF_SSL] is False - assert result["data"][CONF_USERNAME] is None - assert result["data"][CONF_VERIFY_SSL] + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2 + assert result2.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "AdGuard Home Addon" + + data = result2.get("data") + assert data + assert data[CONF_HOST] == "mock-adguard" + assert data[CONF_PASSWORD] is None + assert data[CONF_PORT] == 3000 + assert data[CONF_SSL] is False + assert data[CONF_USERNAME] is None + assert data[CONF_VERIFY_SSL] async def test_hassio_connection_error( @@ -181,6 +197,7 @@ async def test_hassio_connection_error( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "hassio_confirm" - assert result["errors"] == {"base": "cannot_connect"} + assert result + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "hassio_confirm" + assert result.get("errors") == {"base": "cannot_connect"} From 9247a157d8254b0adc0a285051894198c84595a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 14 May 2021 13:28:48 +0200 Subject: [PATCH 402/852] Add AEMET conditional station updates (#50227) --- .coveragerc | 1 - homeassistant/components/aemet/__init__.py | 20 ++++++- .../components/aemet/abstract_aemet_sensor.py | 58 ------------------ homeassistant/components/aemet/config_flow.py | 32 +++++++++- homeassistant/components/aemet/const.py | 2 +- homeassistant/components/aemet/sensor.py | 55 ++++++++++++++++- homeassistant/components/aemet/strings.json | 9 +++ .../components/aemet/translations/en.json | 9 +++ .../aemet/weather_update_coordinator.py | 5 +- tests/components/aemet/test_config_flow.py | 60 ++++++++++++++++++- 10 files changed, 183 insertions(+), 68 deletions(-) delete mode 100644 homeassistant/components/aemet/abstract_aemet_sensor.py diff --git a/.coveragerc b/.coveragerc index 6be3ac8cef6..75a9622d284 100644 --- a/.coveragerc +++ b/.coveragerc @@ -23,7 +23,6 @@ omit = homeassistant/components/adguard/sensor.py homeassistant/components/adguard/switch.py homeassistant/components/ads/* - homeassistant/components/aemet/abstract_aemet_sensor.py homeassistant/components/aemet/weather_update_coordinator.py homeassistant/components/aftership/sensor.py homeassistant/components/agent_dvr/__init__.py diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index a4a0526062d..879f59fa2fc 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -7,7 +7,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from .const import DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, PLATFORMS +from .const import ( + CONF_STATION_UPDATES, + DOMAIN, + ENTRY_NAME, + ENTRY_WEATHER_COORDINATOR, + PLATFORMS, +) from .weather_update_coordinator import WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -19,9 +25,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): api_key = config_entry.data[CONF_API_KEY] latitude = config_entry.data[CONF_LATITUDE] longitude = config_entry.data[CONF_LONGITUDE] + station_updates = config_entry.options.get(CONF_STATION_UPDATES, True) aemet = AEMET(api_key) - weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude) + weather_coordinator = WeatherUpdateCoordinator( + hass, aemet, latitude, longitude, station_updates + ) await weather_coordinator.async_config_entry_first_refresh() @@ -33,9 +42,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + config_entry.async_on_unload(config_entry.add_update_listener(async_update_options)) + return True +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( diff --git a/homeassistant/components/aemet/abstract_aemet_sensor.py b/homeassistant/components/aemet/abstract_aemet_sensor.py deleted file mode 100644 index 8847a5d094d..00000000000 --- a/homeassistant/components/aemet/abstract_aemet_sensor.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Abstraction form AEMET OpenData sensors.""" -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT -from .weather_update_coordinator import WeatherUpdateCoordinator - - -class AbstractAemetSensor(CoordinatorEntity, SensorEntity): - """Abstract class for an AEMET OpenData sensor.""" - - def __init__( - self, - name, - unique_id, - sensor_type, - sensor_configuration, - coordinator: WeatherUpdateCoordinator, - ): - """Initialize the sensor.""" - super().__init__(coordinator) - self._name = name - self._unique_id = unique_id - self._sensor_type = sensor_type - self._sensor_name = sensor_configuration[SENSOR_NAME] - self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) - self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {self._sensor_name}" - - @property - def unique_id(self): - """Return a unique_id for this entity.""" - return self._unique_id - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - - @property - def device_class(self): - """Return the device_class.""" - return self._device_class - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py index f40725c6182..6c97ca98cb8 100644 --- a/homeassistant/components/aemet/config_flow.py +++ b/homeassistant/components/aemet/config_flow.py @@ -4,9 +4,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import DEFAULT_NAME, DOMAIN +from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -47,6 +48,35 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for AEMET.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Required( + CONF_STATION_UPDATES, + default=self.config_entry.options.get(CONF_STATION_UPDATES), + ): bool, + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + async def _is_aemet_api_online(hass, api_key): aemet = AEMET(api_key) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 390ccb86003..0927f64dd2a 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -34,12 +34,12 @@ from homeassistant.const import ( ) ATTRIBUTION = "Powered by AEMET OpenData" +CONF_STATION_UPDATES = "station_updates" PLATFORMS = ["sensor", "weather"] DEFAULT_NAME = "AEMET" DOMAIN = "aemet" ENTRY_NAME = "name" ENTRY_WEATHER_COORDINATOR = "weather_coordinator" -UPDATE_LISTENER = "update_listener" SENSOR_NAME = "sensor_name" SENSOR_UNIT = "sensor_unit" SENSOR_DEVICE_CLASS = "sensor_device_class" diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index 6f43d66e011..de7b06347c3 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -1,6 +1,10 @@ """Support for the AEMET OpenData service.""" -from .abstract_aemet_sensor import AbstractAemetSensor +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.update_coordinator import CoordinatorEntity + from .const import ( + ATTRIBUTION, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, @@ -10,6 +14,9 @@ from .const import ( FORECAST_MONITORED_CONDITIONS, FORECAST_SENSOR_TYPES, MONITORED_CONDITIONS, + SENSOR_DEVICE_CLASS, + SENSOR_NAME, + SENSOR_UNIT, WEATHER_SENSOR_TYPES, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -56,6 +63,52 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) +class AbstractAemetSensor(CoordinatorEntity, SensorEntity): + """Abstract class for an AEMET OpenData sensor.""" + + def __init__( + self, + name, + unique_id, + sensor_type, + sensor_configuration, + coordinator: WeatherUpdateCoordinator, + ): + """Initialize the sensor.""" + super().__init__(coordinator) + self._name = name + self._unique_id = unique_id + self._sensor_type = sensor_type + self._sensor_name = sensor_configuration[SENSOR_NAME] + self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) + self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name} {self._sensor_name}" + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + + @property + def device_class(self): + """Return the device_class.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + class AemetSensor(AbstractAemetSensor): """Implementation of an AEMET OpenData sensor.""" diff --git a/homeassistant/components/aemet/strings.json b/homeassistant/components/aemet/strings.json index a25a503bade..360f7c680ea 100644 --- a/homeassistant/components/aemet/strings.json +++ b/homeassistant/components/aemet/strings.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Gather data from AEMET weather stations" + } + } + } } } diff --git a/homeassistant/components/aemet/translations/en.json b/homeassistant/components/aemet/translations/en.json index 60e7f5f2ec2..3888ccdafc0 100644 --- a/homeassistant/components/aemet/translations/en.json +++ b/homeassistant/components/aemet/translations/en.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Gather data from AEMET weather stations" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 7aab23488b5..8259baf9984 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -118,7 +118,7 @@ class TownNotFound(UpdateFailed): class WeatherUpdateCoordinator(DataUpdateCoordinator): """Weather data update coordinator.""" - def __init__(self, hass, aemet, latitude, longitude): + def __init__(self, hass, aemet, latitude, longitude, station_updates): """Initialize coordinator.""" super().__init__( hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL @@ -129,6 +129,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): self._town = None self._latitude = latitude self._longitude = longitude + self._station_updates = station_updates self._data = { "daily": None, "hourly": None, @@ -210,7 +211,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ) station = None - if self._get_weather_station(): + if self._station_updates and self._get_weather_station(): station = self._aemet.get_conventional_observation_station_data( self._station[AEMET_ATTR_IDEMA] ) diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index be01ac9de07..36713a02903 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import requests_mock from homeassistant import data_entry_flow -from homeassistant.components.aemet.const import DOMAIN +from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.util.dt as dt_util @@ -58,8 +58,64 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_options(hass): + """Test the form options.""" + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ), requests_mock.mock() as _m: + aemet_requests_mock(_m) + + entry = MockConfigEntry( + domain=DOMAIN, unique_id="40.30403754--3.72935236", data=CONFIG + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == "loaded" + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_STATION_UPDATES: False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == { + CONF_STATION_UPDATES: False, + } + + await hass.async_block_till_done() + + assert entry.state == "loaded" + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_STATION_UPDATES: True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == { + CONF_STATION_UPDATES: True, + } + + await hass.async_block_till_done() + + assert entry.state == "loaded" + + async def test_form_duplicated_id(hass): - """Test that the options form.""" + """Test setting up duplicated entry.""" now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( From 6f5629cf1406f382e994fe0336fbbfa54ac0a81b Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 14 May 2021 07:38:41 -0400 Subject: [PATCH 403/852] Add targets and selectors for services (B-C) (#50189) --- .../components/bayesian/services.yaml | 1 + .../components/blackbird/services.yaml | 11 ++++++ homeassistant/components/blink/services.yaml | 23 ++++++++++--- .../components/bluesound/services.yaml | 34 +++++++++++++++++-- .../bluetooth_tracker/services.yaml | 1 + .../bmw_connected_drive/services.yaml | 21 ++++++++++++ .../components/channels/services.yaml | 33 ++++++++++++------ homeassistant/components/cloud/services.yaml | 2 ++ .../components/cloudflare/services.yaml | 1 + .../components/command_line/services.yaml | 1 + 10 files changed, 111 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/bayesian/services.yaml b/homeassistant/components/bayesian/services.yaml index 2fe3a4f7c9b..c1dc891805a 100644 --- a/homeassistant/components/bayesian/services.yaml +++ b/homeassistant/components/bayesian/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all bayesian entities diff --git a/homeassistant/components/blackbird/services.yaml b/homeassistant/components/blackbird/services.yaml index a783dff241b..7b3096c25e4 100644 --- a/homeassistant/components/blackbird/services.yaml +++ b/homeassistant/components/blackbird/services.yaml @@ -1,9 +1,20 @@ set_all_zones: + name: Set all zones description: Set all Blackbird zones to a single source. fields: entity_id: + name: Entity description: Name of any blackbird zone. + required: true example: "media_player.zone_1" + selector: + entity: + integration: blackbird + domain: media_player source: + name: Source description: Name of source to switch to. + required: true example: "Source 1" + selector: + text: diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index 6ea4e2aa9ac..89af4799c85 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -1,28 +1,43 @@ # Describes the format for available Blink services blink_update: + name: Update description: Force a refresh. trigger_camera: + name: Trigger camera description: Request camera to take new image. - fields: - entity_id: - description: Name(s) of camera entities to take new image. - example: "camera.living_room_camera" + target: + entity: + integration: blink + domain: camera save_video: + name: Save video description: Save last recorded video clip to local file. fields: name: + name: Name description: Name of camera to grab video from. + required: true example: "Living Room" + selector: + text: filename: + name: File name description: Filename to writable path (directory may need to be included in whitelist_dirs in config) + required: true example: "/tmp/video.mp4" + selector: + text: send_pin: + name: Send pin description: Send a new PIN to blink for 2FA. fields: pin: + name: Pin description: PIN received from blink. Leave empty if you only received a verification email. example: "abc123" + selector: + text: diff --git a/homeassistant/components/bluesound/services.yaml b/homeassistant/components/bluesound/services.yaml index 0ca12c9e2ae..992fd34a0bc 100644 --- a/homeassistant/components/bluesound/services.yaml +++ b/homeassistant/components/bluesound/services.yaml @@ -1,30 +1,60 @@ join: + name: Join description: Group player together. fields: master: + name: Master description: Entity ID of the player that should become the master of the group. + required: true example: "media_player.bluesound_livingroom" + selector: + entity: + integration: bluesound + domain: media_player entity_id: - description: Name(s) of entities that will coordinate the grouping. Platform dependent. + name: Entity + description: Name of entity that will coordinate the grouping. Platform dependent. example: "media_player.bluesound_livingroom" + selector: + entity: + integration: bluesound + domain: media_player unjoin: + name: Unjoin description: Unjoin the player from a group. fields: entity_id: - description: Name(s) of entities that will be unjoined from their group. Platform dependent. + name: Entity + description: Name of entity that will be unjoined from their group. Platform dependent. example: "media_player.bluesound_livingroom" + selector: + entity: + integration: bluesound + domain: media_player set_sleep_timer: + name: Set sleep timer description: "Set a Bluesound timer. It will increase timer in steps: 15, 30, 45, 60, 90, 0" fields: entity_id: + name: Entity description: Name(s) of entities that will have a timer set. example: "media_player.bluesound_livingroom" + selector: + entity: + integration: bluesound + domain: media_player clear_sleep_timer: + name: Clear sleep timer description: Clear a Bluesound timer. fields: entity_id: + name: Entity description: Name(s) of entities that will have the timer cleared. example: "media_player.bluesound_livingroom" + selector: + entity: + integration: bluesound + domain: media_player diff --git a/homeassistant/components/bluetooth_tracker/services.yaml b/homeassistant/components/bluetooth_tracker/services.yaml index 01b31eee63e..3150403dbf1 100644 --- a/homeassistant/components/bluetooth_tracker/services.yaml +++ b/homeassistant/components/bluetooth_tracker/services.yaml @@ -1,2 +1,3 @@ update: + name: Update description: Trigger manual tracker update diff --git a/homeassistant/components/bmw_connected_drive/services.yaml b/homeassistant/components/bmw_connected_drive/services.yaml index 170289edaea..563e14e5577 100644 --- a/homeassistant/components/bmw_connected_drive/services.yaml +++ b/homeassistant/components/bmw_connected_drive/services.yaml @@ -4,26 +4,37 @@ # component to avoid redundancy. light_flash: + name: Flash lights description: > Flash the lights of the vehicle. The vehicle is identified via the vin (see below). fields: vin: + name: VIN description: > The vehicle identification number (VIN) of the vehicle, 17 characters + required: true example: WBANXXXXXX1234567 + selector: + text: sound_horn: + name: Sound horn description: > Sound the horn of the vehicle. The vehicle is identified via the vin (see below). fields: vin: + name: VIN description: > The vehicle identification number (VIN) of the vehicle, 17 characters + required: true example: WBANXXXXXX1234567 + selector: + text: activate_air_conditioning: + name: Activate air conditioning description: > Start the air conditioning of the vehicle. What exactly is started here depends on the type of vehicle. It might range from just ventilation over @@ -31,21 +42,31 @@ activate_air_conditioning: the vin (see below). fields: vin: + name: VIN description: > The vehicle identification number (VIN) of the vehicle, 17 characters + required: true example: WBANXXXXXX1234567 + selector: + text: find_vehicle: + name: Find vehicle description: > Request vehicle to update the gps location. The vehicle is identified via the vin (see below). fields: vin: + name: VIN description: > The vehicle identification number (VIN) of the vehicle, 17 characters + required: true example: WBANXXXXXX1234567 + selector: + text: update_state: + name: Update state description: > Fetch the last state of the vehicles of all your accounts from the BMW server. This does *not* trigger an update from the vehicle, it just gets diff --git a/homeassistant/components/channels/services.yaml b/homeassistant/components/channels/services.yaml index f06b2bfd905..f5b4639817e 100644 --- a/homeassistant/components/channels/services.yaml +++ b/homeassistant/components/channels/services.yaml @@ -1,23 +1,34 @@ seek_forward: + name: Seek forward description: Seek forward by a set number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: "media_player.family_room_channels" + target: + entity: + integration: channels + domain: media_player seek_backward: + name: Seek backward description: Seek backward by a set number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: "media_player.family_room_channels" + target: + entity: + integration: channels + domain: media_player seek_by: + name: Seek by description: Seek by an inputted number of seconds. + target: + entity: + integration: channels + domain: media_player fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: "media_player.family_room_channels" seconds: + name: Seconds description: Number of seconds to seek by. Negative numbers seek backwards. + required: true example: 120 + selector: + number: + min: -3600 + max: 3600 + unit_of_measurement: seconds diff --git a/homeassistant/components/cloud/services.yaml b/homeassistant/components/cloud/services.yaml index a7fb6b2f21b..1b676ea6be9 100644 --- a/homeassistant/components/cloud/services.yaml +++ b/homeassistant/components/cloud/services.yaml @@ -1,7 +1,9 @@ # Describes the format for available cloud services remote_connect: + name: Remote connect description: Make instance UI available outside over NabuCasa cloud remote_disconnect: + name: Remote disconnect description: Disconnect UI from NabuCasa cloud diff --git a/homeassistant/components/cloudflare/services.yaml b/homeassistant/components/cloudflare/services.yaml index 80165700dbb..f9465e788d8 100644 --- a/homeassistant/components/cloudflare/services.yaml +++ b/homeassistant/components/cloudflare/services.yaml @@ -1,2 +1,3 @@ update_records: + name: Update records description: Manually trigger update to Cloudflare records diff --git a/homeassistant/components/command_line/services.yaml b/homeassistant/components/command_line/services.yaml index de010ba8b85..f4cec426860 100644 --- a/homeassistant/components/command_line/services.yaml +++ b/homeassistant/components/command_line/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all command_line entities From 404188d0051ada5e23cd2ba1bb62223240c6ab17 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 14 May 2021 14:04:53 +0200 Subject: [PATCH 404/852] Update wheel action to 2021.05.3 (#50607) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 93d2827fc51..583a2ea4211 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -81,7 +81,7 @@ jobs: name: requirements_diff - name: Build wheels - uses: home-assistant/wheels@2021.05.2 + uses: home-assistant/wheels@2021.05.3 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} @@ -151,7 +151,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2021.05.2 + uses: home-assistant/wheels@2021.05.3 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} From 9d174e8a0504a83831530ef9c9178bbbe5a58872 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 14 May 2021 14:30:48 +0200 Subject: [PATCH 405/852] GRPC is fixed, don't need a workaround (#50605) * GRPC is fixed, don't need a workaround * Update gen_requirements_all.py --- homeassistant/package_constraints.txt | 4 ---- script/gen_requirements_all.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 713c2f1a62f..d485da19142 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -48,10 +48,6 @@ h11>=0.12.0 # https://github.com/advisories/GHSA-93xj-8mrv-444m httplib2>=0.19.0 -# gRPC 1.32+ currently causes issues on ARMv7, see: -# https://github.com/home-assistant/core/issues/40148 -grpcio==1.31.0 - # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 4fd96cb1b04..79d4c05b0b6 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,10 +68,6 @@ h11>=0.12.0 # https://github.com/advisories/GHSA-93xj-8mrv-444m httplib2>=0.19.0 -# gRPC 1.32+ currently causes issues on ARMv7, see: -# https://github.com/home-assistant/core/issues/40148 -grpcio==1.31.0 - # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From 8fcf06a2a900d602906d6f881f2c79d822892283 Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Fri, 14 May 2021 15:03:26 +0200 Subject: [PATCH 406/852] Add bosch_shc supporting Bosch Smart Home Controller (#34063) Co-authored-by: Martin Hjelmare --- .coveragerc | 4 + CODEOWNERS | 1 + .../components/bosch_shc/__init__.py | 94 +++ .../components/bosch_shc/binary_sensor.py | 49 ++ .../components/bosch_shc/config_flow.py | 227 +++++++ homeassistant/components/bosch_shc/const.py | 12 + homeassistant/components/bosch_shc/entity.py | 92 +++ .../components/bosch_shc/manifest.json | 13 + .../components/bosch_shc/strings.json | 38 ++ .../components/bosch_shc/translations/en.json | 41 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 4 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/bosch_shc/__init__.py | 1 + .../components/bosch_shc/test_config_flow.py | 625 ++++++++++++++++++ 16 files changed, 1208 insertions(+) create mode 100644 homeassistant/components/bosch_shc/__init__.py create mode 100644 homeassistant/components/bosch_shc/binary_sensor.py create mode 100644 homeassistant/components/bosch_shc/config_flow.py create mode 100644 homeassistant/components/bosch_shc/const.py create mode 100644 homeassistant/components/bosch_shc/entity.py create mode 100644 homeassistant/components/bosch_shc/manifest.json create mode 100644 homeassistant/components/bosch_shc/strings.json create mode 100644 homeassistant/components/bosch_shc/translations/en.json create mode 100644 tests/components/bosch_shc/__init__.py create mode 100644 tests/components/bosch_shc/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 75a9622d284..1b44cb3013b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -112,6 +112,10 @@ omit = homeassistant/components/bmw_connected_drive/lock.py homeassistant/components/bmw_connected_drive/notify.py homeassistant/components/bmw_connected_drive/sensor.py + homeassistant/components/bosch_shc/__init__.py + homeassistant/components/bosch_shc/const.py + homeassistant/components/bosch_shc/binary_sensor.py + homeassistant/components/bosch_shc/entity.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index d1f14761180..14601d72255 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -70,6 +70,7 @@ homeassistant/components/blueprint/* @home-assistant/core homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe homeassistant/components/bond/* @prystupa +homeassistant/components/bosch_shc/* @tschamm homeassistant/components/braviatv/* @bieniu homeassistant/components/broadlink/* @danielhiversen @felipediel homeassistant/components/brother/* @bieniu diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py new file mode 100644 index 00000000000..a315405365c --- /dev/null +++ b/homeassistant/components/bosch_shc/__init__.py @@ -0,0 +1,94 @@ +"""The Bosch Smart Home Controller integration.""" +import logging + +from boschshcpy import SHCSession +from boschshcpy.exceptions import SHCAuthenticationError, SHCConnectionError + +from homeassistant.components.zeroconf import async_get_instance +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr + +from .const import ( + CONF_SSL_CERTIFICATE, + CONF_SSL_KEY, + DATA_POLLING_HANDLER, + DATA_SESSION, + DOMAIN, +) + +PLATFORMS = [ + "binary_sensor", +] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Bosch SHC from a config entry.""" + data = entry.data + + zeroconf = await async_get_instance(hass) + try: + session = await hass.async_add_executor_job( + SHCSession, + data[CONF_HOST], + data[CONF_SSL_CERTIFICATE], + data[CONF_SSL_KEY], + False, + zeroconf, + ) + except SHCAuthenticationError as err: + raise ConfigEntryAuthFailed from err + except SHCConnectionError as err: + raise ConfigEntryNotReady from err + + shc_info = session.information + if shc_info.updateState.name == "UPDATE_AVAILABLE": + _LOGGER.warning("Please check for software updates in the Bosch Smart Home App") + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_SESSION: session, + } + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(shc_info.unique_id))}, + identifiers={(DOMAIN, shc_info.unique_id)}, + manufacturer="Bosch", + name=entry.title, + model="SmartHomeController", + sw_version=shc_info.version, + ) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + async def stop_polling(event): + """Stop polling service.""" + await hass.async_add_executor_job(session.stop_polling) + + await hass.async_add_executor_job(session.start_polling) + hass.data[DOMAIN][entry.entry_id][ + DATA_POLLING_HANDLER + ] = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_polling) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + session: SHCSession = hass.data[DOMAIN][entry.entry_id][DATA_SESSION] + + hass.data[DOMAIN][entry.entry_id][DATA_POLLING_HANDLER]() + hass.data[DOMAIN][entry.entry_id].pop(DATA_POLLING_HANDLER) + await hass.async_add_executor_job(session.stop_polling) + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py new file mode 100644 index 00000000000..ef2d35097e1 --- /dev/null +++ b/homeassistant/components/bosch_shc/binary_sensor.py @@ -0,0 +1,49 @@ +"""Platform for binarysensor integration.""" +from boschshcpy import SHCSession, SHCShutterContact + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_WINDOW, + BinarySensorEntity, +) + +from .const import DATA_SESSION, DOMAIN +from .entity import SHCEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the SHC binary sensor platform.""" + entities = [] + session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] + + for binary_sensor in session.device_helper.shutter_contacts: + entities.append( + ShutterContactSensor( + device=binary_sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + + if entities: + async_add_entities(entities) + + +class ShutterContactSensor(SHCEntity, BinarySensorEntity): + """Representation of a SHC shutter contact sensor.""" + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._device.state == SHCShutterContact.ShutterContactService.State.OPEN + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + switcher = { + "ENTRANCE_DOOR": DEVICE_CLASS_DOOR, + "REGULAR_WINDOW": DEVICE_CLASS_WINDOW, + "FRENCH_WINDOW": DEVICE_CLASS_DOOR, + "GENERIC": DEVICE_CLASS_WINDOW, + } + return switcher.get(self._device.device_class, DEVICE_CLASS_WINDOW) diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py new file mode 100644 index 00000000000..e795f2bdfec --- /dev/null +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -0,0 +1,227 @@ +"""Config flow for Bosch Smart Home Controller integration.""" +import logging +from os import makedirs + +from boschshcpy import SHCRegisterClient, SHCSession +from boschshcpy.exceptions import ( + SHCAuthenticationError, + SHCConnectionError, + SHCRegistrationError, + SHCSessionError, +) +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.components.zeroconf import async_get_instance +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN + +from .const import ( + CONF_HOSTNAME, + CONF_SHC_CERT, + CONF_SHC_KEY, + CONF_SSL_CERTIFICATE, + CONF_SSL_KEY, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +HOST_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + } +) + + +def write_tls_asset(hass: core.HomeAssistant, filename: str, asset: bytes) -> None: + """Write the tls assets to disk.""" + makedirs(hass.config.path(DOMAIN), exist_ok=True) + with open(hass.config.path(DOMAIN, filename), "w") as file_handle: + file_handle.write(asset.decode("utf-8")) + + +def create_credentials_and_validate(hass, host, user_input, zeroconf): + """Create and store credentials and validate session.""" + helper = SHCRegisterClient(host, user_input[CONF_PASSWORD]) + result = helper.register(host, "HomeAssistant") + + if result is not None: + write_tls_asset(hass, CONF_SHC_CERT, result["cert"]) + write_tls_asset(hass, CONF_SHC_KEY, result["key"]) + + session = SHCSession( + host, + hass.config.path(DOMAIN, CONF_SHC_CERT), + hass.config.path(DOMAIN, CONF_SHC_KEY), + True, + zeroconf, + ) + session.authenticate() + + return result + + +def get_info_from_host(hass, host, zeroconf): + """Get information from host.""" + session = SHCSession( + host, + "", + "", + True, + zeroconf, + ) + information = session.mdns_info() + return {"title": information.name, "unique_id": information.unique_id} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Bosch SHC.""" + + VERSION = 1 + info = None + host = None + hostname = None + + async def async_step_reauth(self, user_input=None): + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=HOST_SCHEMA, + ) + self.host = host = user_input[CONF_HOST] + self.info = await self._get_info(host) + return await self.async_step_credentials() + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + host = user_input[CONF_HOST] + try: + self.info = info = await self._get_info(host) + except SHCConnectionError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info["unique_id"]) + self._abort_if_unique_id_configured({CONF_HOST: host}) + self.host = host + return await self.async_step_credentials() + + return self.async_show_form( + step_id="user", data_schema=HOST_SCHEMA, errors=errors + ) + + async def async_step_credentials(self, user_input=None): + """Handle the credentials step.""" + errors = {} + if user_input is not None: + zeroconf = await async_get_instance(self.hass) + try: + result = await self.hass.async_add_executor_job( + create_credentials_and_validate, + self.hass, + self.host, + user_input, + zeroconf, + ) + except SHCAuthenticationError: + errors["base"] = "invalid_auth" + except SHCConnectionError: + errors["base"] = "cannot_connect" + except SHCSessionError as err: + _LOGGER.warning("Session error: %s", err.message) + errors["base"] = "session_error" + except SHCRegistrationError as err: + _LOGGER.warning("Registration error: %s", err.message) + errors["base"] = "pairing_failed" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + entry_data = { + CONF_SSL_CERTIFICATE: self.hass.config.path(DOMAIN, CONF_SHC_CERT), + CONF_SSL_KEY: self.hass.config.path(DOMAIN, CONF_SHC_KEY), + CONF_HOST: self.host, + CONF_TOKEN: result["token"], + CONF_HOSTNAME: result["token"].split(":", 1)[1], + } + existing_entry = await self.async_set_unique_id(self.info["unique_id"]) + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, + data=entry_data, + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=self.info["title"], + data=entry_data, + ) + else: + user_input = {} + + schema = vol.Schema( + { + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + } + ) + + return self.async_show_form( + step_id="credentials", data_schema=schema, errors=errors + ) + + async def async_step_zeroconf(self, discovery_info): + """Handle zeroconf discovery.""" + if not discovery_info.get("name", "").startswith("Bosch SHC"): + return self.async_abort(reason="not_bosch_shc") + + try: + self.info = info = await self._get_info(discovery_info["host"]) + except SHCConnectionError: + return self.async_abort(reason="cannot_connect") + + local_name = discovery_info["hostname"][:-1] + node_name = local_name[: -len(".local")] + + await self.async_set_unique_id(info["unique_id"]) + self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]}) + self.host = discovery_info["host"] + self.context["title_placeholders"] = {"name": node_name} + return await self.async_step_confirm_discovery() + + async def async_step_confirm_discovery(self, user_input=None): + """Handle discovery confirm.""" + errors = {} + if user_input is not None: + return await self.async_step_credentials() + + return self.async_show_form( + step_id="confirm_discovery", + description_placeholders={ + "model": "Bosch SHC", + "host": self.host, + }, + errors=errors, + ) + + async def _get_info(self, host): + """Get additional information.""" + zeroconf = await async_get_instance(self.hass) + + return await self.hass.async_add_executor_job( + get_info_from_host, + self.hass, + host, + zeroconf, + ) diff --git a/homeassistant/components/bosch_shc/const.py b/homeassistant/components/bosch_shc/const.py new file mode 100644 index 00000000000..ccb1f2094cb --- /dev/null +++ b/homeassistant/components/bosch_shc/const.py @@ -0,0 +1,12 @@ +"""Constants for the Bosch SHC integration.""" + +CONF_HOSTNAME = "hostname" +CONF_SHC_CERT = "bosch_shc-cert.pem" +CONF_SHC_KEY = "bosch_shc-key.pem" +CONF_SSL_CERTIFICATE = "ssl_certificate" +CONF_SSL_KEY = "ssl_key" + +DATA_SESSION = "session" +DATA_POLLING_HANDLER = "polling_handler" + +DOMAIN = "bosch_shc" diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py new file mode 100644 index 00000000000..d693b0cdfcc --- /dev/null +++ b/homeassistant/components/bosch_shc/entity.py @@ -0,0 +1,92 @@ +"""Bosch Smart Home Controller base entity.""" +from boschshcpy.device import SHCDevice + +from homeassistant.helpers.device_registry import async_get as get_dev_reg +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN + + +async def async_remove_devices(hass, entity, entry_id): + """Get item that is removed from session.""" + dev_registry = get_dev_reg(hass) + device = dev_registry.async_get_device( + identifiers={(DOMAIN, entity.device_id)}, connections=set() + ) + if device is not None: + dev_registry.async_update_device(device.id, remove_config_entry_id=entry_id) + + +class SHCEntity(Entity): + """Representation of a SHC base entity.""" + + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize the generic SHC device.""" + self._device = device + self._parent_id = parent_id + self._entry_id = entry_id + + async def async_added_to_hass(self): + """Subscribe to SHC events.""" + await super().async_added_to_hass() + + def on_state_changed(): + self.schedule_update_ha_state() + + def update_entity_information(): + if self._device.deleted: + self.hass.add_job(async_remove_devices(self.hass, self, self._entry_id)) + else: + self.schedule_update_ha_state() + + for service in self._device.device_services: + service.subscribe_callback(self.entity_id, on_state_changed) + self._device.subscribe_callback(self.entity_id, update_entity_information) + + async def async_will_remove_from_hass(self): + """Unsubscribe from SHC events.""" + await super().async_will_remove_from_hass() + for service in self._device.device_services: + service.unsubscribe_callback(self.entity_id) + self._device.unsubscribe_callback(self.entity_id) + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return self._device.serial + + @property + def name(self): + """Name of the entity.""" + return self._device.name + + @property + def device_id(self): + """Device id of the entity.""" + return self._device.id + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.device_id)}, + "name": self._device.name, + "manufacturer": self._device.manufacturer, + "model": self._device.device_model, + "via_device": ( + DOMAIN, + self._device.parent_device_id + if self._device.parent_device_id is not None + else self._parent_id, + ), + } + + @property + def available(self): + """Return false if status is unavailable.""" + return self._device.status == "AVAILABLE" + + @property + def should_poll(self): + """Report polling mode. SHC Entity is communicating via long polling.""" + return False diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json new file mode 100644 index 00000000000..7922450ccde --- /dev/null +++ b/homeassistant/components/bosch_shc/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "bosch_shc", + "name": "Bosch SHC", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/bosch_shc", + "requirements": ["boschshcpy==0.2.17"], + "zeroconf": [ + {"type": "_http._tcp.local.", "name": "bosch shc*"} + ], + "iot_class": "local_push", + "codeowners": ["@tschamm"], + "after_dependencies": ["zeroconf"] +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json new file mode 100644 index 00000000000..e7f090a4e1b --- /dev/null +++ b/homeassistant/components/bosch_shc/strings.json @@ -0,0 +1,38 @@ +{ + "title": "Bosch SHC", + "config": { + "step": { + "user": { + "description": "Set up your Bosch Smart Home Controller to allow monitoring and control with Home Assistant.", + "title": "SHC authentication parameters", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "credentials": { + "data": { + "password": "Password of the Smart Home Controller" + } + }, + "confirm_discovery": { + "description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The bosch_shc integration needs to re-authenticate your account" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "pairing_failed": "Pairing failed; please check the Bosch Smart Home Controller is in pairing mode (LED flashing) as well as your password is correct.", + "session_error": "Session error: API return Non-OK result.", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "flow_title": "Bosch SHC: {name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/en.json b/homeassistant/components/bosch_shc/translations/en.json new file mode 100644 index 00000000000..fcac72b418e --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/en.json @@ -0,0 +1,41 @@ +{ + "title": "Bosch SHC", + "config": { + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?" + }, + "credentials": { + "data": { + "password": "Password of the Smart Home Controller" + } + }, + "user": { + "description": "Set up your Bosch Smart Home Controller to allow monitoring and control with Home Assistant.", + "title": "SHC authentication parameters", + "data": { + "host": "Host" + } + }, + "reauth_confirm": { + "title": "SHC authentication parameters", + "description": "The bosch_shc integration needs to re-authenticate your account", + "data": { + "host": "Host" + } + } + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "pairing_failed": "Pairing failed; please check the Bosch Smart Home Controller is in pairing mode (LED flashing) as well as your password is correct.", + "session_error": "Session error: API return Non-OK result.", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index fe62725cc82..49170f966f5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -33,6 +33,7 @@ FLOWS = [ "blink", "bmw_connected_drive", "bond", + "bosch_shc", "braviatv", "broadlink", "brother", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 3b801bc6ddd..00eb5b53170 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -97,6 +97,10 @@ ZEROCONF = { } ], "_http._tcp.local.": [ + { + "domain": "bosch_shc", + "name": "bosch shc*" + }, { "domain": "nam", "name": "nam-*" diff --git a/requirements_all.txt b/requirements_all.txt index e9ef1b26e76..f4b4efa0ad5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -382,6 +382,9 @@ blockchain==1.4.4 # homeassistant.components.bond bond-api==0.1.12 +# homeassistant.components.bosch_shc +boschshcpy==0.2.17 + # homeassistant.components.amazon_polly # homeassistant.components.route53 boto3==1.16.52 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3de8763efff..367024fc89f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -219,6 +219,9 @@ blinkpy==0.17.0 # homeassistant.components.bond bond-api==0.1.12 +# homeassistant.components.bosch_shc +boschshcpy==0.2.17 + # homeassistant.components.braviatv bravia-tv==1.0.8 diff --git a/tests/components/bosch_shc/__init__.py b/tests/components/bosch_shc/__init__.py new file mode 100644 index 00000000000..a7ad288fadb --- /dev/null +++ b/tests/components/bosch_shc/__init__.py @@ -0,0 +1 @@ +"""Tests for the Bosch SHC integration.""" diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py new file mode 100644 index 00000000000..c75814aabc3 --- /dev/null +++ b/tests/components/bosch_shc/test_config_flow.py @@ -0,0 +1,625 @@ +"""Test the Bosch SHC config flow.""" +from unittest.mock import PropertyMock, mock_open, patch + +from boschshcpy.exceptions import ( + SHCAuthenticationError, + SHCConnectionError, + SHCRegistrationError, + SHCSessionError, +) +from boschshcpy.information import SHCInformation + +from homeassistant import config_entries, setup +from homeassistant.components.bosch_shc.config_flow import write_tls_asset +from homeassistant.components.bosch_shc.const import CONF_SHC_CERT, CONF_SHC_KEY, DOMAIN + +from tests.common import MockConfigEntry + +MOCK_SETTINGS = { + "name": "Test name", + "device": {"mac": "test-mac", "hostname": "test-host"}, +} +DISCOVERY_INFO = { + "host": "1.1.1.1", + "port": 0, + "hostname": "shc012345.local.", + "type": "_http._tcp.local.", + "name": "Bosch SHC [test-mac]._http._tcp.local.", +} + + +async def test_form_user(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["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate" + ) as mock_authenticate, patch( + "homeassistant.components.bosch_shc.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "shc012345" + assert result3["data"] == { + "host": "1.1.1.1", + "ssl_certificate": hass.config.path(DOMAIN, CONF_SHC_CERT), + "ssl_key": hass.config.path(DOMAIN, CONF_SHC_KEY), + "token": "abc:123", + "hostname": "123", + } + + assert len(mock_authenticate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_get_info_connection_error(hass): + """Test we handle connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + side_effect=SHCConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_get_info_exception(hass): + """Test we handle exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_pairing_error(hass): + """Test we handle pairing error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + side_effect=SHCRegistrationError(""), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["step_id"] == "credentials" + assert result3["errors"] == {"base": "pairing_failed"} + + +async def test_form_user_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( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=SHCAuthenticationError, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["step_id"] == "credentials" + assert result3["errors"] == {"base": "invalid_auth"} + + +async def test_form_validate_connection_error(hass): + """Test we handle connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=SHCConnectionError, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["step_id"] == "credentials" + assert result3["errors"] == {"base": "cannot_connect"} + + +async def test_form_validate_session_error(hass): + """Test we handle session error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=SHCSessionError(""), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["step_id"] == "credentials" + assert result3["errors"] == {"base": "session_error"} + + +async def test_form_validate_exception(hass): + """Test we handle exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=Exception, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["step_id"] == "credentials" + assert result3["errors"] == {"base": "unknown"} + + +async def test_form_already_configured(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="bosch_shc", unique_id="test-mac", data={"host": "0.0.0.0"} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + # Test config entry got updated with latest IP + assert entry.data["host"] == "1.1.1.1" + + +async def test_zeroconf(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm_discovery" + assert result["errors"] == {} + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert context["title_placeholders"]["name"] == "shc012345" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate", + ), patch( + "homeassistant.components.bosch_shc.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "shc012345" + assert result3["data"] == { + "host": "1.1.1.1", + "ssl_certificate": hass.config.path(DOMAIN, CONF_SHC_CERT), + "ssl_key": hass.config.path(DOMAIN, CONF_SHC_KEY), + "token": "abc:123", + "hostname": "123", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_already_configured(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="bosch_shc", unique_id="test-mac", data={"host": "0.0.0.0"} + ) + entry.add_to_hass(hass) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + # Test config entry got updated with latest IP + assert entry.data["host"] == "1.1.1.1" + + +async def test_zeroconf_cannot_connect(hass): + """Test we get the form.""" + with patch( + "boschshcpy.session.SHCSession.mdns_info", side_effect=SHCConnectionError + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +async def test_zeroconf_not_bosch_shc(hass): + """Test we filter out non-bosch_shc devices.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"host": "1.1.1.1", "name": "notboschshc"}, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "abort" + assert result["reason"] == "not_bosch_shc" + + +async def test_reauth(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id="test-mac", + data={ + "host": "1.1.1.1", + "hostname": "test-mac", + "ssl_certificate": "test-cert.pem", + "ssl_key": "test-key.pem", + }, + title="shc012345", + ) + mock_config.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=mock_config.data, + ) + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "2.2.2.2"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate" + ), patch( + "homeassistant.components.bosch_shc.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "abort" + assert result3["reason"] == "reauth_successful" + + assert mock_config.data["host"] == "2.2.2.2" + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_tls_assets_writer(hass): + """Test we write tls assets to correct location.""" + assets = { + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + } + with patch("os.mkdir"), patch("builtins.open", mock_open()) as mocked_file: + write_tls_asset(hass, CONF_SHC_CERT, assets["cert"]) + mocked_file.assert_called_with(hass.config.path(DOMAIN, CONF_SHC_CERT), "w") + mocked_file().write.assert_called_with("content_cert") + + write_tls_asset(hass, CONF_SHC_KEY, assets["key"]) + mocked_file.assert_called_with(hass.config.path(DOMAIN, CONF_SHC_KEY), "w") + mocked_file().write.assert_called_with("content_key") From f5c31b89f8790677d1570c4d8861a85f77c7c4ac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 14 May 2021 15:46:49 +0200 Subject: [PATCH 407/852] Deprecate SmartHab YAML configuration (#50602) --- homeassistant/components/smarthab/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 7759d038224..3777f35dbc2 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -17,14 +17,17 @@ PLATFORMS = ["light", "cover"] _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From f33b45ec8217970ba063c9fac4183ea40a48c466 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Fri, 14 May 2021 09:47:09 -0400 Subject: [PATCH 408/852] Add interview feedback for Z-Wave JS add node websocket (#50384) * Add interview feedback for add node websocket * cleanup leftover logging * add tests * test interview failed event * fix event type * include manufacturer & model from device registry * update test --- homeassistant/components/zwave_js/api.py | 22 +++++++++++- tests/components/zwave_js/test_api.py | 45 +++++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 81600ec6c16..e722cac9e09 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -239,9 +239,24 @@ async def websocket_add_node( websocket_api.event_message(msg[ID], {"event": event["event"]}) ) + @callback + def forward_stage(event: dict) -> None: + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": event["event"], "stage": event["stageName"]} + ) + ) + @callback def node_added(event: dict) -> None: node = event["node"] + interview_unsubs = [ + node.on("interview started", forward_event), + node.on("interview completed", forward_event), + node.on("interview stage completed", forward_stage), + node.on("interview failed", forward_event), + ] + unsubs.extend(interview_unsubs) node_details = { "node_id": node.node_id, "status": node.status, @@ -255,7 +270,12 @@ async def websocket_add_node( @callback def device_registered(device: DeviceEntry) -> None: - device_details = {"name": device.name, "id": device.id} + device_details = { + "name": device.name, + "id": device.id, + "manufacturer": device.manufacturer, + "model": device.model, + } connection.send_message( websocket_api.event_message( msg[ID], {"event": "device registered", "device": device_details} diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 57961ee89e4..f192c69b80a 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -155,7 +155,50 @@ async def test_add_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "device registered" # Check the keys of the device item - assert list(msg["event"]["device"]) == ["name", "id"] + assert list(msg["event"]["device"]) == ["name", "id", "manufacturer", "model"] + + # Test receiving interview events + event = Event( + type="interview started", + data={"source": "node", "event": "interview started", "nodeId": 53}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview started" + + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "NodeInfo", + "nodeId": 53, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview stage completed" + assert msg["event"]["stage"] == "NodeInfo" + + event = Event( + type="interview completed", + data={"source": "node", "event": "interview completed", "nodeId": 53}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview completed" + + event = Event( + type="interview failed", + data={"source": "node", "event": "interview failed", "nodeId": 53}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview failed" # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) From 20a39ab7e1fd3218fba7a2b9e0e4f7aea0cb580a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 14 May 2021 15:49:18 +0200 Subject: [PATCH 409/852] Remove unused config schema & logger from totalconnect (#50604) --- .../components/totalconnect/__init__.py | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index c122de310dd..7026abc34f9 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -1,8 +1,5 @@ """The totalconnect component.""" -import logging - from total_connect_client import TotalConnectClient -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -12,24 +9,9 @@ import homeassistant.helpers.config_validation as cv from .const import CONF_USERCODES, DOMAIN -_LOGGER = logging.getLogger(__name__) - PLATFORMS = ["alarm_control_panel", "binary_sensor"] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -43,10 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise ConfigEntryAuthFailed("No usercodes in TotalConnect configuration") temp_codes = conf[CONF_USERCODES] - usercodes = {} - for code in temp_codes: - usercodes[int(code)] = temp_codes[code] - + usercodes = {int(code): temp_codes[code] for code in temp_codes} client = await hass.async_add_executor_job( TotalConnectClient.TotalConnectClient, username, password, usercodes ) From a8e1a68d1fb5fdee669a62de0727df4a9c80b686 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 14 May 2021 15:51:25 +0200 Subject: [PATCH 410/852] Deprecate NZBGet YAML configuration (#50603) --- homeassistant/components/nzbget/__init__.py | 33 +++++++++++---------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 7b250d393ea..71f885ce491 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -34,21 +34,24 @@ from .coordinator import NZBGetDataUpdateCoordinator PLATFORMS = ["sensor", "switch"] 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=DEFAULT_SSL): cv.boolean, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + 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=DEFAULT_SSL): cv.boolean, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From c220e700081bc954d5838be81fd7f9dbfd930ae5 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 14 May 2021 17:02:11 +0200 Subject: [PATCH 411/852] Add integration kraken (#31114) Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 1 + homeassistant/components/kraken/__init__.py | 152 ++++++++++ .../components/kraken/config_flow.py | 81 ++++++ homeassistant/components/kraken/const.py | 28 ++ homeassistant/components/kraken/manifest.json | 9 + homeassistant/components/kraken/sensor.py | 230 +++++++++++++++ homeassistant/components/kraken/strings.json | 24 ++ homeassistant/components/kraken/utils.py | 16 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 3 + requirements_all.txt | 6 + requirements_test_all.txt | 6 + script/hassfest/mypy_config.py | 1 + tests/components/kraken/__init__.py | 1 + tests/components/kraken/const.py | 80 ++++++ tests/components/kraken/test_config_flow.py | 101 +++++++ tests/components/kraken/test_init.py | 66 +++++ tests/components/kraken/test_sensor.py | 267 ++++++++++++++++++ 18 files changed, 1073 insertions(+) create mode 100644 homeassistant/components/kraken/__init__.py create mode 100644 homeassistant/components/kraken/config_flow.py create mode 100644 homeassistant/components/kraken/const.py create mode 100644 homeassistant/components/kraken/manifest.json create mode 100644 homeassistant/components/kraken/sensor.py create mode 100644 homeassistant/components/kraken/strings.json create mode 100644 homeassistant/components/kraken/utils.py create mode 100644 tests/components/kraken/__init__.py create mode 100644 tests/components/kraken/const.py create mode 100644 tests/components/kraken/test_config_flow.py create mode 100644 tests/components/kraken/test_init.py create mode 100644 tests/components/kraken/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 14601d72255..00446a4f087 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -255,6 +255,7 @@ homeassistant/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/konnected/* @heythisisnate @kit-klein homeassistant/components/kostal_plenticore/* @stegm +homeassistant/components/kraken/* @eifinger homeassistant/components/kulersky/* @emlove homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py new file mode 100644 index 00000000000..0cadf051948 --- /dev/null +++ b/homeassistant/components/kraken/__init__.py @@ -0,0 +1,152 @@ +"""The kraken integration.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging + +import async_timeout +import krakenex +import pykrakenapi + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_TRACKED_ASSET_PAIRS, + DEFAULT_SCAN_INTERVAL, + DEFAULT_TRACKED_ASSET_PAIR, + DISPATCH_CONFIG_UPDATED, + DOMAIN, +) +from .utils import get_tradable_asset_pairs + +PLATFORMS = ["sensor"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up kraken from a config entry.""" + kraken_data = KrakenData(hass, config_entry) + await kraken_data.async_setup() + hass.data[DOMAIN] = kraken_data + config_entry.add_update_listener(async_options_updated) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + if unload_ok: + for unsub_listener in hass.data[DOMAIN].unsub_listeners: + unsub_listener() + hass.data.pop(DOMAIN) + + return unload_ok + + +class KrakenData: + """Define an object to hold kraken data.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Initialize.""" + self._hass = hass + self._config_entry = config_entry + self._api = pykrakenapi.KrakenAPI(krakenex.API(), retry=0, crl_sleep=0) + self.tradable_asset_pairs = None + self.coordinator = None + self.unsub_listeners = [] + + async def async_update(self) -> None: + """Get the latest data from the Kraken.com REST API. + + All tradeable asset pairs are retrieved, not the tracked asset pairs + selected by the user. This enables us to check for an unknown and + thus likely removed asset pair in sensor.py and only log a warning + once. + """ + try: + async with async_timeout.timeout(10): + return await self._hass.async_add_executor_job(self._get_kraken_data) + except pykrakenapi.pykrakenapi.KrakenAPIError as error: + if "Unknown asset pair" in str(error): + _LOGGER.info( + "Kraken.com reported an unknown asset pair. Refreshing list of tradable asset pairs" + ) + await self._async_refresh_tradable_asset_pairs() + else: + raise UpdateFailed( + f"Unable to fetch data from Kraken.com: {error}" + ) from error + except pykrakenapi.pykrakenapi.CallRateLimitError: + _LOGGER.warning( + "Exceeded the Kraken.com call rate limit. Increase the update interval to prevent this error" + ) + + def _get_kraken_data(self) -> dict: + websocket_name_pairs = self._get_websocket_name_asset_pairs() + ticker_df = self._api.get_ticker_information(websocket_name_pairs) + # Rename columns to their full name + ticker_df = ticker_df.rename( + columns={ + "a": "ask", + "b": "bid", + "c": "last_trade_closed", + "v": "volume", + "p": "volume_weighted_average", + "t": "number_of_trades", + "l": "low", + "h": "high", + "o": "opening_price", + } + ) + response_dict = ticker_df.transpose().to_dict() + return response_dict + + async def _async_refresh_tradable_asset_pairs(self) -> None: + self.tradable_asset_pairs = await self._hass.async_add_executor_job( + get_tradable_asset_pairs, self._api + ) + + async def async_setup(self) -> None: + """Set up the Kraken integration.""" + if not self._config_entry.options: + options = { + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_TRACKED_ASSET_PAIRS: [DEFAULT_TRACKED_ASSET_PAIR], + } + self._hass.config_entries.async_update_entry( + self._config_entry, options=options + ) + await self._async_refresh_tradable_asset_pairs() + await asyncio.sleep(1) # Wait 1 second to avoid triggering the CallRateLimiter + self.coordinator = DataUpdateCoordinator( + self._hass, + _LOGGER, + name=DOMAIN, + update_method=self.async_update, + update_interval=timedelta( + seconds=self._config_entry.options[CONF_SCAN_INTERVAL] + ), + ) + await self.coordinator.async_config_entry_first_refresh() + + def _get_websocket_name_asset_pairs(self) -> list: + return ",".join(wsname for wsname in self.tradable_asset_pairs.values()) + + def set_update_interval(self, update_interval: int) -> None: + """Set the coordinator update_interval to the supplied update_interval.""" + self.coordinator.update_interval = timedelta(seconds=update_interval) + + +async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Triggered by config entry options updates.""" + hass.data[DOMAIN].set_update_interval(config_entry.options[CONF_SCAN_INTERVAL]) + async_dispatcher_send(hass, DISPATCH_CONFIG_UPDATED, hass, config_entry) diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py new file mode 100644 index 00000000000..2c0afc800e6 --- /dev/null +++ b/homeassistant/components/kraken/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow for kraken integration.""" +import logging + +import krakenex +from pykrakenapi.pykrakenapi import KrakenAPI +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv + +from .const import CONF_TRACKED_ASSET_PAIRS, DEFAULT_SCAN_INTERVAL, DOMAIN +from .utils import get_tradable_asset_pairs + +_LOGGER = logging.getLogger(__name__) + + +class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for kraken.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return KrakenOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if DOMAIN in self.hass.data: + return self.async_abort(reason="already_configured") + if user_input is not None: + return self.async_create_entry(title=DOMAIN, data=user_input) + return self.async_show_form( + step_id="user", + data_schema=None, + errors={}, + ) + + +class KrakenOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Kraken client options.""" + + def __init__(self, config_entry): + """Initialize Kraken options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Kraken options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + api = KrakenAPI(krakenex.API(), retry=0, crl_sleep=0) + tradable_asset_pairs = await self.hass.async_add_executor_job( + get_tradable_asset_pairs, api + ) + tradable_asset_pairs_for_multi_select = { + v: v for v in tradable_asset_pairs.keys() + } + options = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): int, + vol.Optional( + CONF_TRACKED_ASSET_PAIRS, + default=self.config_entry.options.get(CONF_TRACKED_ASSET_PAIRS, []), + ): cv.multi_select(tradable_asset_pairs_for_multi_select), + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + + +class AlreadyConfigured(HomeAssistantError): + """Error to indicate the asset pair is already configured.""" diff --git a/homeassistant/components/kraken/const.py b/homeassistant/components/kraken/const.py new file mode 100644 index 00000000000..fb3d0aa4dc4 --- /dev/null +++ b/homeassistant/components/kraken/const.py @@ -0,0 +1,28 @@ +"""Constants for the kraken integration.""" + +DEFAULT_SCAN_INTERVAL = 60 +DEFAULT_TRACKED_ASSET_PAIR = "XBT/USD" +DISPATCH_CONFIG_UPDATED = "kraken_config_updated" + +CONF_TRACKED_ASSET_PAIRS = "tracked_asset_pairs" + +DOMAIN = "kraken" + +SENSOR_TYPES = [ + {"name": "ask", "enabled_by_default": True}, + {"name": "ask_volume", "enabled_by_default": False}, + {"name": "bid", "enabled_by_default": True}, + {"name": "bid_volume", "enabled_by_default": False}, + {"name": "volume_today", "enabled_by_default": False}, + {"name": "volume_last_24h", "enabled_by_default": False}, + {"name": "volume_weighted_average_today", "enabled_by_default": False}, + {"name": "volume_weighted_average_last_24h", "enabled_by_default": False}, + {"name": "number_of_trades_today", "enabled_by_default": False}, + {"name": "number_of_trades_last_24h", "enabled_by_default": False}, + {"name": "last_trade_closed", "enabled_by_default": False}, + {"name": "low_today", "enabled_by_default": True}, + {"name": "low_last_24h", "enabled_by_default": False}, + {"name": "high_today", "enabled_by_default": True}, + {"name": "high_last_24h", "enabled_by_default": False}, + {"name": "opening_price_today", "enabled_by_default": False}, +] diff --git a/homeassistant/components/kraken/manifest.json b/homeassistant/components/kraken/manifest.json new file mode 100644 index 00000000000..c7d1ca4d0ed --- /dev/null +++ b/homeassistant/components/kraken/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "kraken", + "name": "Kraken", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kraken", + "requirements": ["krakenex==2.1.0", "pykrakenapi==0.1.8"], + "codeowners": ["@eifinger"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py new file mode 100644 index 00000000000..e7009915fd9 --- /dev/null +++ b/homeassistant/components/kraken/sensor.py @@ -0,0 +1,230 @@ +"""The kraken integration.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import async_entries_for_config_entry +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import KrakenData +from .const import ( + CONF_TRACKED_ASSET_PAIRS, + DISPATCH_CONFIG_UPDATED, + DOMAIN, + SENSOR_TYPES, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add kraken entities from a config_entry.""" + + @callback + async def async_update_sensors( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> None: + device_registry = await hass.helpers.device_registry.async_get_registry() + + existing_devices = { + device.name: device.id + for device in async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + } + + for tracked_asset_pair in config_entry.options[CONF_TRACKED_ASSET_PAIRS]: + # Only create new devices + if create_device_name(tracked_asset_pair) in existing_devices: + existing_devices.pop(create_device_name(tracked_asset_pair)) + else: + sensors = [] + for sensor_type in SENSOR_TYPES: + sensors.append( + KrakenSensor( + hass.data[DOMAIN], + tracked_asset_pair, + sensor_type, + ) + ) + async_add_entities(sensors, True) + + # Remove devices for asset pairs which are no longer tracked + for device_id in existing_devices.values(): + device_registry.async_remove_device(device_id) + + await async_update_sensors(hass, config_entry) + + hass.data[DOMAIN].unsub_listeners.append( + async_dispatcher_connect( + hass, + DISPATCH_CONFIG_UPDATED, + async_update_sensors, + ) + ) + + +class KrakenSensor(CoordinatorEntity): + """Define a Kraken sensor.""" + + def __init__( + self, + kraken_data: KrakenData, + tracked_asset_pair: str, + sensor_type: dict[str, bool], + ) -> None: + """Initialize.""" + super().__init__(kraken_data.coordinator) + self.tracked_asset_pair_wsname = kraken_data.tradable_asset_pairs[ + tracked_asset_pair + ] + self._source_asset = tracked_asset_pair.split("/")[0] + self._target_asset = tracked_asset_pair.split("/")[1] + self._sensor_type = sensor_type["name"] + self._enabled_by_default = sensor_type["enabled_by_default"] + self._unit_of_measurement = self._target_asset + self._device_name = f"{self._source_asset} {self._target_asset}" + self._name = "_".join( + [ + tracked_asset_pair.split("/")[0], + tracked_asset_pair.split("/")[1], + sensor_type["name"], + ] + ) + self._received_data_at_least_once = False + self._available = True + self._state = None + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enabled_by_default + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def unique_id(self): + """Set unique_id for sensor.""" + return self._name.lower() + + @property + def state(self): + """Return the state.""" + return self._state + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._update_internal_state() + + def _handle_coordinator_update(self): + self._update_internal_state() + return super()._handle_coordinator_update() + + def _update_internal_state(self): + try: + self._state = self._try_get_state() + self._received_data_at_least_once = True # Received data at least one time. + except TypeError: + if self._received_data_at_least_once: + if self._available: + _LOGGER.warning( + "Asset Pair %s is no longer available", + self._device_name, + ) + self._available = False + + def _try_get_state(self) -> str: + """Try to get the state or return a TypeError.""" + if self._sensor_type == "last_trade_closed": + return self.coordinator.data[self.tracked_asset_pair_wsname][ + "last_trade_closed" + ][0] + if self._sensor_type == "ask": + return self.coordinator.data[self.tracked_asset_pair_wsname]["ask"][0] + if self._sensor_type == "ask_volume": + return self.coordinator.data[self.tracked_asset_pair_wsname]["ask"][1] + if self._sensor_type == "bid": + return self.coordinator.data[self.tracked_asset_pair_wsname]["bid"][0] + if self._sensor_type == "bid_volume": + return self.coordinator.data[self.tracked_asset_pair_wsname]["bid"][1] + if self._sensor_type == "volume_today": + return self.coordinator.data[self.tracked_asset_pair_wsname]["volume"][0] + if self._sensor_type == "volume_last_24h": + return self.coordinator.data[self.tracked_asset_pair_wsname]["volume"][1] + if self._sensor_type == "volume_weighted_average_today": + return self.coordinator.data[self.tracked_asset_pair_wsname][ + "volume_weighted_average" + ][0] + if self._sensor_type == "volume_weighted_average_last_24h": + return self.coordinator.data[self.tracked_asset_pair_wsname][ + "volume_weighted_average" + ][1] + if self._sensor_type == "number_of_trades_today": + return self.coordinator.data[self.tracked_asset_pair_wsname][ + "number_of_trades" + ][0] + if self._sensor_type == "number_of_trades_last_24h": + return self.coordinator.data[self.tracked_asset_pair_wsname][ + "number_of_trades" + ][1] + if self._sensor_type == "low_today": + return self.coordinator.data[self.tracked_asset_pair_wsname]["low"][0] + if self._sensor_type == "low_last_24h": + return self.coordinator.data[self.tracked_asset_pair_wsname]["low"][1] + if self._sensor_type == "high_today": + return self.coordinator.data[self.tracked_asset_pair_wsname]["high"][0] + if self._sensor_type == "high_last_24h": + return self.coordinator.data[self.tracked_asset_pair_wsname]["high"][1] + if self._sensor_type == "opening_price_today": + return self.coordinator.data[self.tracked_asset_pair_wsname][ + "opening_price" + ] + + @property + def icon(self): + """Return the icon.""" + if self._target_asset == "EUR": + return "mdi:currency-eur" + if self._target_asset == "GBP": + return "mdi:currency-gbp" + if self._target_asset == "USD": + return "mdi:currency-usd" + if self._target_asset == "JPY": + return "mdi:currency-jpy" + if self._target_asset == "XBT": + return "mdi:currency-btc" + return "mdi:cash" + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + if "number_of" not in self._sensor_type: + return self._unit_of_measurement + + @property + def available(self): + """Could the api be accessed during the last update call.""" + return self._available and self.coordinator.last_update_success + + @property + def device_info(self) -> dict: + """Return a device description for device registry.""" + + return { + "identifiers": {(DOMAIN, self._source_asset, self._target_asset)}, + "name": self._device_name, + "manufacturer": "Kraken.com", + "entry_type": "service", + } + + +def create_device_name(tracked_asset_pair: str) -> str: + """Create the device name for a given tracked asset pair.""" + return f"{tracked_asset_pair.split('/')[0]} {tracked_asset_pair.split('/')[1]}" diff --git a/homeassistant/components/kraken/strings.json b/homeassistant/components/kraken/strings.json new file mode 100644 index 00000000000..e94f2129a48 --- /dev/null +++ b/homeassistant/components/kraken/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "error": {}, + "step": { + "user": { + "data": {}, + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update interval", + "tracked_asset_pairs": "Tracked Asset Pairs" + } + } + } + } +} diff --git a/homeassistant/components/kraken/utils.py b/homeassistant/components/kraken/utils.py new file mode 100644 index 00000000000..66b66f12d91 --- /dev/null +++ b/homeassistant/components/kraken/utils.py @@ -0,0 +1,16 @@ +"""Utility functions for the kraken integration.""" +from __future__ import annotations + +from pykrakenapi.pykrakenapi import KrakenAPI + + +def get_tradable_asset_pairs(kraken_api: KrakenAPI) -> dict[str, str]: + """Get a list of tradable asset pairs.""" + tradable_asset_pairs = {} + asset_pairs_df = kraken_api.get_tradable_asset_pairs() + for pair in zip(asset_pairs_df.index.values, asset_pairs_df["wsname"]): + if not pair[0].endswith( + ".d" + ): # Remove darkpools https://support.kraken.com/hc/en-us/articles/360001391906-Introducing-the-Kraken-Dark-Pool + tradable_asset_pairs[pair[1]] = pair[0] + return tradable_asset_pairs diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 49170f966f5..e2a36c3b093 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -131,6 +131,7 @@ FLOWS = [ "kodi", "konnected", "kostal_plenticore", + "kraken", "kulersky", "life360", "lifx", diff --git a/mypy.ini b/mypy.ini index 94cd59e4956..3371658dc92 100644 --- a/mypy.ini +++ b/mypy.ini @@ -908,6 +908,9 @@ ignore_errors = true [mypy-homeassistant.components.kostal_plenticore.*] ignore_errors = true +[mypy-homeassistant.components.kraken.*] +ignore_errors = true + [mypy-homeassistant.components.kulersky.*] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index f4b4efa0ad5..7880f89beaf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -857,6 +857,9 @@ konnected==1.2.0 # homeassistant.components.kostal_plenticore kostal_plenticore==0.2.0 +# homeassistant.components.kraken +krakenex==2.1.0 + # homeassistant.components.eufy lakeside==0.12 @@ -1499,6 +1502,9 @@ pykmtronic==0.3.0 # homeassistant.components.kodi pykodi==0.2.5 +# homeassistant.components.kraken +pykrakenapi==0.1.8 + # homeassistant.components.kulersky pykulersky==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 367024fc89f..281b29450a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -480,6 +480,9 @@ konnected==1.2.0 # homeassistant.components.kostal_plenticore kostal_plenticore==0.2.0 +# homeassistant.components.kraken +krakenex==2.1.0 + # homeassistant.components.dyson libpurecool==0.6.4 @@ -831,6 +834,9 @@ pykmtronic==0.3.0 # homeassistant.components.kodi pykodi==0.2.5 +# homeassistant.components.kraken +pykrakenapi==0.1.8 + # homeassistant.components.kulersky pykulersky==0.5.2 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 6c8fba912ac..73752f4c51a 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -115,6 +115,7 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.kodi.*", "homeassistant.components.konnected.*", "homeassistant.components.kostal_plenticore.*", + "homeassistant.components.kraken.*", "homeassistant.components.kulersky.*", "homeassistant.components.lifx.*", "homeassistant.components.litejet.*", diff --git a/tests/components/kraken/__init__.py b/tests/components/kraken/__init__.py new file mode 100644 index 00000000000..26b12dd3789 --- /dev/null +++ b/tests/components/kraken/__init__.py @@ -0,0 +1 @@ +"""Tests for the kraken integration.""" diff --git a/tests/components/kraken/const.py b/tests/components/kraken/const.py new file mode 100644 index 00000000000..6e3174a9ae7 --- /dev/null +++ b/tests/components/kraken/const.py @@ -0,0 +1,80 @@ +"""Constants for kraken tests.""" +import pandas + +TRADEABLE_ASSET_PAIR_RESPONSE = pandas.DataFrame( + {"wsname": ["ADA/XBT", "ADA/ETH", "XBT/EUR", "XBT/GBP", "XBT/USD", "XBT/JPY"]}, + columns=["wsname"], + index=["ADAXBT", "ADAETH", "XBTEUR", "XXBTZGBP", "XXBTZUSD", "XXBTZJPY"], +) + +TICKER_INFORMATION_RESPONSE = pandas.DataFrame( + { + "a": [ + [0.000349400, 15949, 15949.000], + [0.000349400, 15949, 15949.000], + [0.000349400, 15949, 15949.000], + [0.000349400, 15949, 15949.000], + [0.000349400, 15949, 15949.000], + [0.000349400, 15949, 15949.000], + ], + "b": [ + [0.000348400, 20792, 20792.000], + [0.000348400, 20792, 20792.000], + [0.000348400, 20792, 20792.000], + [0.000348400, 20792, 20792.000], + [0.000348400, 20792, 20792.000], + [0.000348400, 20792, 20792.000], + ], + "c": [ + [0.000347800, 2809.36384377], + [0.000347800, 2809.36384377], + [0.000347800, 2809.36384377], + [0.000347800, 2809.36384377], + [0.000347800, 2809.36384377], + [0.000347800, 2809.36384377], + ], + "h": [ + [0.000351600, 0.000352100], + [0.000351600, 0.000352100], + [0.000351600, 0.000352100], + [0.000351600, 0.000352100], + [0.000351600, 0.000352100], + [0.000351600, 0.000352100], + ], + "l": [ + [0.000344600, 0.000344600], + [0.000344600, 0.000344600], + [0.000344600, 0.000344600], + [0.000344600, 0.000344600], + [0.000344600, 0.000344600], + [0.000344600, 0.000344600], + ], + "o": [ + 0.000351300, + 0.000351300, + 0.000351300, + 0.000351300, + 0.000351300, + 0.000351300, + ], + "p": [ + [0.000348573, 0.000344881], + [0.000348573, 0.000344881], + [0.000348573, 0.000344881], + [0.000348573, 0.000344881], + [0.000348573, 0.000344881], + [0.000348573, 0.000344881], + ], + "t": [[82, 128], [82, 128], [82, 128], [82, 128], [82, 128], [82, 128]], + "v": [ + [146300.24906838, 253478.04715403], + [146300.24906838, 253478.04715403], + [146300.24906838, 253478.04715403], + [146300.24906838, 253478.04715403], + [146300.24906838, 253478.04715403], + [146300.24906838, 253478.04715403], + ], + }, + columns=["a", "b", "c", "h", "l", "o", "p", "t", "v"], + index=["ADAXBT", "ADAETH", "XBTEUR", "XXBTZGBP", "XXBTZUSD", "XXBTZJPY"], +) diff --git a/tests/components/kraken/test_config_flow.py b/tests/components/kraken/test_config_flow.py new file mode 100644 index 00000000000..6f29273e1a7 --- /dev/null +++ b/tests/components/kraken/test_config_flow.py @@ -0,0 +1,101 @@ +"""Tests for the kraken config_flow.""" +from unittest.mock import patch + +from homeassistant.components.kraken.const import CONF_TRACKED_ASSET_PAIRS, DOMAIN +from homeassistant.const import CONF_SCAN_INTERVAL + +from .const import TICKER_INFORMATION_RESPONSE, TRADEABLE_ASSET_PAIR_RESPONSE + +from tests.common import MockConfigEntry + + +async def test_config_flow(hass): + """Test we can finish a config flow.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == "create_entry" + + await hass.async_block_till_done() + state = hass.states.get("sensor.xbt_usd_ask") + assert state + + +async def test_already_configured(hass): + """Test we can not add a second config flow.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == "create_entry" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "abort" + + +async def test_options(hass): + """Test options for Kraken.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + entry = MockConfigEntry( + domain=DOMAIN, + options={ + CONF_SCAN_INTERVAL: 60, + CONF_TRACKED_ASSET_PAIRS: [ + "ADA/XBT", + "ADA/ETH", + "XBT/EUR", + "XBT/GBP", + "XBT/USD", + "XBT/JPY", + ], + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.xbt_usd_ask") + + result = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + { + CONF_SCAN_INTERVAL: 10, + CONF_TRACKED_ASSET_PAIRS: ["ADA/ETH"], + }, + ) + assert result["type"] == "create_entry" + await hass.async_block_till_done() + + ada_eth_sensor = hass.states.get("sensor.ada_eth_ask") + assert ada_eth_sensor.state == "0.0003494" + + assert hass.states.get("sensor.xbt_usd_ask") is None diff --git a/tests/components/kraken/test_init.py b/tests/components/kraken/test_init.py new file mode 100644 index 00000000000..69cfde42547 --- /dev/null +++ b/tests/components/kraken/test_init.py @@ -0,0 +1,66 @@ +"""Tests for the kraken integration.""" +from unittest.mock import patch + +from pykrakenapi.pykrakenapi import CallRateLimitError, KrakenAPIError + +from homeassistant.components import kraken +from homeassistant.components.kraken.const import DOMAIN + +from .const import TICKER_INFORMATION_RESPONSE, TRADEABLE_ASSET_PAIR_RESPONSE + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass): + """Test unload for Kraken.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert await kraken.async_unload_entry(hass, entry) + assert DOMAIN not in hass.data + + +async def test_unkown_error(hass, caplog): + """Test unload for Kraken.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + side_effect=KrakenAPIError("EQuery: Error"), + ): + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert "Unable to fetch data from Kraken.com:" in caplog.text + + +async def test_callrate_limit(hass, caplog): + """Test unload for Kraken.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + side_effect=CallRateLimitError(), + ): + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert ( + "Exceeded the Kraken.com call rate limit. Increase the update interval to prevent this error" + in caplog.text + ) diff --git a/tests/components/kraken/test_sensor.py b/tests/components/kraken/test_sensor.py new file mode 100644 index 00000000000..98760a3002d --- /dev/null +++ b/tests/components/kraken/test_sensor.py @@ -0,0 +1,267 @@ +"""Tests for the kraken sensor platform.""" +from datetime import timedelta +from unittest.mock import patch + +from pykrakenapi.pykrakenapi import KrakenAPIError + +from homeassistant.components.kraken.const import ( + CONF_TRACKED_ASSET_PAIRS, + DEFAULT_SCAN_INTERVAL, + DEFAULT_TRACKED_ASSET_PAIR, + DOMAIN, +) +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START +import homeassistant.util.dt as dt_util + +from .const import TICKER_INFORMATION_RESPONSE, TRADEABLE_ASSET_PAIR_RESPONSE + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_sensor(hass): + """Test that sensor has a value.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + options={ + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_TRACKED_ASSET_PAIRS: [ + "ADA/XBT", + "ADA/ETH", + "XBT/EUR", + "XBT/GBP", + "XBT/USD", + "XBT/JPY", + ], + }, + ) + entry.add_to_hass(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_ask_volume", + suggested_object_id="xbt_usd_ask_volume", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_last_trade_closed", + suggested_object_id="xbt_usd_last_trade_closed", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_bid_volume", + suggested_object_id="xbt_usd_bid_volume", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_volume_today", + suggested_object_id="xbt_usd_volume_today", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_volume_last_24h", + suggested_object_id="xbt_usd_volume_last_24h", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_volume_weighted_average_today", + suggested_object_id="xbt_usd_volume_weighted_average_today", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_volume_weighted_average_last_24h", + suggested_object_id="xbt_usd_volume_weighted_average_last_24h", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_number_of_trades_today", + suggested_object_id="xbt_usd_number_of_trades_today", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_number_of_trades_last_24h", + suggested_object_id="xbt_usd_number_of_trades_last_24h", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_low_last_24h", + suggested_object_id="xbt_usd_low_last_24h", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_high_last_24h", + suggested_object_id="xbt_usd_high_last_24h", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_opening_price_today", + suggested_object_id="xbt_usd_opening_price_today", + disabled_by=None, + ) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + xbt_usd_sensor = hass.states.get("sensor.xbt_usd_ask") + assert xbt_usd_sensor.state == "0.0003494" + assert xbt_usd_sensor.attributes["icon"] == "mdi:currency-usd" + + xbt_eur_sensor = hass.states.get("sensor.xbt_eur_ask") + assert xbt_eur_sensor.state == "0.0003494" + assert xbt_eur_sensor.attributes["icon"] == "mdi:currency-eur" + + ada_xbt_sensor = hass.states.get("sensor.ada_xbt_ask") + assert ada_xbt_sensor.state == "0.0003494" + assert ada_xbt_sensor.attributes["icon"] == "mdi:currency-btc" + + xbt_jpy_sensor = hass.states.get("sensor.xbt_jpy_ask") + assert xbt_jpy_sensor.state == "0.0003494" + assert xbt_jpy_sensor.attributes["icon"] == "mdi:currency-jpy" + + xbt_gbp_sensor = hass.states.get("sensor.xbt_gbp_ask") + assert xbt_gbp_sensor.state == "0.0003494" + assert xbt_gbp_sensor.attributes["icon"] == "mdi:currency-gbp" + + ada_eth_sensor = hass.states.get("sensor.ada_eth_ask") + assert ada_eth_sensor.state == "0.0003494" + assert ada_eth_sensor.attributes["icon"] == "mdi:cash" + + xbt_usd_ask_volume = hass.states.get("sensor.xbt_usd_ask_volume") + assert xbt_usd_ask_volume.state == "15949" + + xbt_usd_last_trade_closed = hass.states.get("sensor.xbt_usd_last_trade_closed") + assert xbt_usd_last_trade_closed.state == "0.0003478" + + xbt_usd_bid_volume = hass.states.get("sensor.xbt_usd_bid_volume") + assert xbt_usd_bid_volume.state == "20792" + + xbt_usd_volume_today = hass.states.get("sensor.xbt_usd_volume_today") + assert xbt_usd_volume_today.state == "146300.24906838" + + xbt_usd_volume_last_24h = hass.states.get("sensor.xbt_usd_volume_last_24h") + assert xbt_usd_volume_last_24h.state == "253478.04715403" + + xbt_usd_volume_weighted_average_today = hass.states.get( + "sensor.xbt_usd_volume_weighted_average_today" + ) + assert xbt_usd_volume_weighted_average_today.state == "0.000348573" + + xbt_usd_volume_weighted_average_last_24h = hass.states.get( + "sensor.xbt_usd_volume_weighted_average_last_24h" + ) + assert xbt_usd_volume_weighted_average_last_24h.state == "0.000344881" + + xbt_usd_number_of_trades_today = hass.states.get( + "sensor.xbt_usd_number_of_trades_today" + ) + assert xbt_usd_number_of_trades_today.state == "82" + + xbt_usd_number_of_trades_last_24h = hass.states.get( + "sensor.xbt_usd_number_of_trades_last_24h" + ) + assert xbt_usd_number_of_trades_last_24h.state == "128" + + xbt_usd_low_last_24h = hass.states.get("sensor.xbt_usd_low_last_24h") + assert xbt_usd_low_last_24h.state == "0.0003446" + + xbt_usd_high_last_24h = hass.states.get("sensor.xbt_usd_high_last_24h") + assert xbt_usd_high_last_24h.state == "0.0003521" + + xbt_usd_opening_price_today = hass.states.get( + "sensor.xbt_usd_opening_price_today" + ) + assert xbt_usd_opening_price_today.state == "0.0003513" + + +async def test_missing_pair_marks_sensor_unavailable(hass): + """Test that a missing tradable asset pair marks the sensor unavailable.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ): + with patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + entry = MockConfigEntry( + domain=DOMAIN, + options={ + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_TRACKED_ASSET_PAIRS: [DEFAULT_TRACKED_ASSET_PAIR], + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.xbt_usd_ask") + assert sensor.state == "0.0003494" + + with patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + side_effect=KrakenAPIError("EQuery:Unknown asset pair"), + ): + async_fire_time_changed( + hass, utcnow + timedelta(seconds=DEFAULT_SCAN_INTERVAL * 2) + ) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.xbt_usd_ask") + assert sensor.state == "unavailable" From 77e6fc6f93c538bf07e5f93aa151f0e5c1c91b2c Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 14 May 2021 11:40:30 -0400 Subject: [PATCH 412/852] Add missing requirements and target to sonos services (#50552) --- homeassistant/components/sonos/services.yaml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 5c9ebed36f7..0fee089d114 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -6,6 +6,7 @@ join: name: Master description: Entity ID of the player that should become the coordinator of the group. + required: true example: "media_player.living_room_sonos" selector: entity: @@ -14,6 +15,7 @@ join: entity_id: name: Entity description: Name of entity that will join the master. + required: true example: "media_player.living_room_sonos" selector: entity: @@ -23,15 +25,10 @@ join: unjoin: name: Unjoin group description: Unjoin the player from a group. - fields: - entity_id: - name: Entity - description: Name of entity that will be unjoined from their group. - example: "media_player.living_room_sonos" - selector: - entity: - integration: sonos - domain: media_player + target: + entity: + integration: sonos + domain: media_player snapshot: name: Snapshot From 4d5529093281bc425a0bfb0c350f38fc9e73ef8f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 14 May 2021 18:46:37 +0200 Subject: [PATCH 413/852] Fritz code quality improvements from #48287 and #50055 (#50479) Co-authored-by: J. Nick Koston --- .../components/fritz/binary_sensor.py | 12 ++++++---- homeassistant/components/fritz/common.py | 10 ++++---- homeassistant/components/fritz/config_flow.py | 4 ++-- homeassistant/components/fritz/const.py | 2 +- .../components/fritz/device_tracker.py | 14 ++++++----- homeassistant/components/fritz/manifest.json | 3 +-- homeassistant/components/fritz/sensor.py | 23 ++++++++++--------- homeassistant/components/fritz/services.py | 8 +++---- homeassistant/components/fritz/strings.json | 12 +--------- .../components/fritz/translations/en.json | 12 +--------- requirements_all.txt | 1 - requirements_test_all.txt | 1 - tests/components/fritz/__init__.py | 15 ++++++------ tests/components/fritz/test_config_flow.py | 6 +++-- 14 files changed, 53 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index 493f1bc0d42..65780fffaa9 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -21,7 +21,7 @@ async def async_setup_entry( ) -> None: """Set up entry.""" _LOGGER.debug("Setting up FRITZ!Box binary sensors") - fritzbox_tools = hass.data[DOMAIN][entry.entry_id] + fritzbox_tools: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] if "WANIPConn1" in fritzbox_tools.connection.services: # Only routers are supported at the moment @@ -33,13 +33,15 @@ async def async_setup_entry( class FritzBoxConnectivitySensor(FritzBoxBaseEntity, BinarySensorEntity): """Define FRITZ!Box connectivity class.""" - def __init__(self, fritzbox_tools: FritzBoxTools, device_friendlyname: str) -> None: + def __init__( + self, fritzbox_tools: FritzBoxTools, device_friendly_name: str + ) -> None: """Init FRITZ!Box connectivity class.""" self._unique_id = f"{fritzbox_tools.unique_id}-connectivity" - self._name = f"{device_friendlyname} Connectivity" + self._name = f"{device_friendly_name} Connectivity" self._is_on = True self._is_available = True - super().__init__(fritzbox_tools, device_friendlyname) + super().__init__(fritzbox_tools, device_friendly_name) @property def name(self): @@ -78,7 +80,7 @@ class FritzBoxConnectivitySensor(FritzBoxBaseEntity, BinarySensorEntity): is_up = link_props["NewPhysicalLinkStatus"] self._is_on = is_up == "Up" else: - self._is_on = self._fritzbox_tools.fritzstatus.is_connected + self._is_on = self._fritzbox_tools.fritz_status.is_connected self._is_available = True diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 2708293b327..47c3ef88681 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -61,8 +61,8 @@ class FritzBoxTools: self._devices: dict[str, Any] = {} self._unique_id = None self.connection = None - self.fritzhosts = None - self.fritzstatus = None + self.fritz_hosts = None + self.fritz_status = None self.hass = hass self.host = host self.password = password @@ -86,7 +86,7 @@ class FritzBoxTools: timeout=60.0, ) - self.fritzstatus = FritzStatus(fc=self.connection) + self.fritz_status = FritzStatus(fc=self.connection) info = self.connection.call_action("DeviceInfo:1", "GetInfo") if self._unique_id is None: self._unique_id = info["NewSerialNumber"] @@ -97,7 +97,7 @@ class FritzBoxTools: async def async_start(self): """Start FritzHosts connection.""" - self.fritzhosts = FritzHosts(fc=self.connection) + self.fritz_hosts = FritzHosts(fc=self.connection) await self.hass.async_add_executor_job(self.scan_devices) @@ -135,7 +135,7 @@ class FritzBoxTools: def _update_info(self): """Retrieve latest information from the FRITZ!Box.""" - return self.fritzhosts.get_hosts_info() + return self.fritz_hosts.get_hosts_info() def scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 23e713f7966..103ddbef9d9 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -22,7 +22,7 @@ from .const import ( DEFAULT_PORT, DOMAIN, ERROR_AUTH_INVALID, - ERROR_CONNECTION_ERROR, + ERROR_CANNOT_CONNECT, ERROR_UNKNOWN, ) @@ -60,7 +60,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): except FritzSecurityError: return ERROR_AUTH_INVALID except FritzConnectionException: - return ERROR_CONNECTION_ERROR + return ERROR_CANNOT_CONNECT except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") return ERROR_UNKNOWN diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index dafe9f4d126..266e24b6be3 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -12,7 +12,7 @@ DEFAULT_PORT = 49000 DEFAULT_USERNAME = "" ERROR_AUTH_INVALID = "invalid_auth" -ERROR_CONNECTION_ERROR = "connection_error" +ERROR_CANNOT_CONNECT = "cannot_connect" ERROR_UNKNOWN = "unknown_error" FRITZ_SERVICES = "fritz_services" diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 646a8cc986e..d228d71ec10 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -19,7 +19,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType -from .common import FritzBoxTools +from .common import FritzBoxTools, FritzDevice from .const import DATA_FRITZ, DEFAULT_DEVICE_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -75,7 +75,9 @@ async def async_setup_entry( """Update the values of the router.""" _async_add_entities(router, async_add_entities, data_fritz) - async_dispatcher_connect(hass, router.signal_device_new, update_router) + entry.async_on_unload( + async_dispatcher_connect(hass, router.signal_device_new, update_router) + ) update_router() @@ -109,7 +111,7 @@ def _async_add_entities(router, async_add_entities, data_fritz): class FritzBoxTracker(ScannerEntity): """This class queries a FRITZ!Box router.""" - def __init__(self, router: FritzBoxTools, device): + def __init__(self, router: FritzBoxTools, device: FritzDevice) -> None: """Initialize a FRITZ!Box device.""" self._router = router self._mac = device.mac_address @@ -158,9 +160,9 @@ class FritzBoxTracker(ScannerEntity): return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "AVM", - "model": "FRITZ!Box Tracked device", + "default_name": self.name, + "default_manufacturer": "AVM", + "default_model": "FRITZ!Box Tracked device", "via_device": ( DOMAIN, self._router.unique_id, diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 68b1bde4f38..1158ea2e797 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -3,8 +3,7 @@ "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ - "fritzconnection==1.4.2", - "xmltodict==0.12.0" + "fritzconnection==1.4.2" ], "codeowners": [ "@mammuth", diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 5c01552582f..7bff6bd40c8 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -8,7 +8,7 @@ from typing import Callable, TypedDict from fritzconnection.core.exceptions import FritzConnectionException from fritzconnection.lib.fritzstatus import FritzStatus -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.core import HomeAssistant @@ -72,33 +72,34 @@ async def async_setup_entry( ) -> None: """Set up entry.""" _LOGGER.debug("Setting up FRITZ!Box sensors") - fritzbox_tools = hass.data[DOMAIN][entry.entry_id] + fritzbox_tools: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] if "WANIPConn1" not in fritzbox_tools.connection.services: # Only routers are supported at the moment return + entities = [] for sensor_type in SENSOR_DATA: - async_add_entities( - [FritzBoxSensor(fritzbox_tools, entry.title, sensor_type)], - True, - ) + entities.append(FritzBoxSensor(fritzbox_tools, entry.title, sensor_type)) + + if entities: + async_add_entities(entities, True) -class FritzBoxSensor(FritzBoxBaseEntity, BinarySensorEntity): +class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): """Define FRITZ!Box connectivity class.""" def __init__( - self, fritzbox_tools: FritzBoxTools, device_friendlyname: str, sensor_type: str + self, fritzbox_tools: FritzBoxTools, device_friendly_name: str, sensor_type: str ) -> None: """Init FRITZ!Box connectivity class.""" self._sensor_data: SensorData = SENSOR_DATA[sensor_type] self._unique_id = f"{fritzbox_tools.unique_id}-{sensor_type}" - self._name = f"{device_friendlyname} {self._sensor_data['name']}" + self._name = f"{device_friendly_name} {self._sensor_data['name']}" self._is_available = True self._last_value: str | None = None self._state: str | None = None - super().__init__(fritzbox_tools, device_friendlyname) + super().__init__(fritzbox_tools, device_friendly_name) @property def _state_provider(self) -> Callable: @@ -140,7 +141,7 @@ class FritzBoxSensor(FritzBoxBaseEntity, BinarySensorEntity): _LOGGER.debug("Updating FRITZ!Box sensors") try: - status: FritzStatus = self._fritzbox_tools.fritzstatus + status: FritzStatus = self._fritzbox_tools.fritz_status self._is_available = True except FritzConnectionException: _LOGGER.error("Error getting the state from the FRITZ!Box", exc_info=True) diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py index 1e3a792f9bc..7ed5ecd3c40 100644 --- a/homeassistant/components/fritz/services.py +++ b/homeassistant/components/fritz/services.py @@ -12,10 +12,10 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_services(hass: HomeAssistant): """Set up services for Fritz integration.""" - if hass.data.get(FRITZ_SERVICES, False): - return - hass.data[FRITZ_SERVICES] = True + for service in [SERVICE_REBOOT, SERVICE_RECONNECT]: + if hass.services.has_service(DOMAIN, service): + return async def async_call_fritz_service(service_call): """Call correct Fritz service.""" @@ -31,7 +31,7 @@ async def async_setup_services(hass: HomeAssistant): for entry in fritzbox_entry_ids: _LOGGER.debug("Executing service %s", service_call.service) - fritz_tools = hass.data[DOMAIN].get(entry) + fritz_tools = hass.data[DOMAIN][entry] await fritz_tools.service_fritzbox(service_call.service) for service in [SERVICE_REBOOT, SERVICE_RECONNECT]: diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 3f6cb4adba4..c32e9c7bac2 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -10,16 +10,6 @@ "password": "[%key:common::config_flow::data::password%]" } }, - "start_config": { - "title": "Setup FRITZ!Box Tools - mandatory", - "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", - "data": { - "host": "[%key:common::config_flow::data::host%]", - "port": "[%key:common::config_flow::data::port%]", - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - } - }, "reauth_confirm": { "title": "Updating FRITZ!Box Tools - credentials", "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", @@ -35,7 +25,7 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "connection_error": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index de1b61763f6..f712831021a 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -8,7 +8,7 @@ "error": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "connection_error": "Failed to connect", + "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, "flow_title": "{name}", @@ -28,16 +28,6 @@ }, "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", "title": "Updating FRITZ!Box Tools - credentials" - }, - "start_config": { - "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "username": "Username" - }, - "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", - "title": "Setup FRITZ!Box Tools - mandatory" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 7880f89beaf..f6a0cf43063 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2368,7 +2368,6 @@ xboxapi==2.0.1 xknx==0.18.1 # homeassistant.components.bluesound -# homeassistant.components.fritz # homeassistant.components.rest # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 281b29450a6..04102316aaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1271,7 +1271,6 @@ xbox-webapi==2.0.11 xknx==0.18.1 # homeassistant.components.bluesound -# homeassistant.components.fritz # homeassistant.components.rest # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/tests/components/fritz/__init__.py b/tests/components/fritz/__init__.py index 27ec391c092..a1fd1ce42fb 100644 --- a/tests/components/fritz/__init__.py +++ b/tests/components/fritz/__init__.py @@ -106,23 +106,22 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods def __init__(self): """Inint Mocking class.""" type(self).modelname = mock.PropertyMock(return_value=self.MODELNAME) - self.call_action = mock.Mock(side_effect=self._side_effect_callaction) - type(self).actionnames = mock.PropertyMock( - side_effect=self._side_effect_actionnames + self.call_action = mock.Mock(side_effect=self._side_effect_call_action) + type(self).action_names = mock.PropertyMock( + side_effect=self._side_effect_action_names ) services = { srv: None - for srv, _ in list(self.FRITZBOX_DATA.keys()) - + list(self.FRITZBOX_DATA_INDEXED.keys()) + for srv, _ in list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) } type(self).services = mock.PropertyMock(side_effect=[services]) - def _side_effect_callaction(self, service, action, **kwargs): + def _side_effect_call_action(self, service, action, **kwargs): if kwargs: index = next(iter(kwargs.values())) return self.FRITZBOX_DATA_INDEXED[(service, action)][index] return self.FRITZBOX_DATA[(service, action)] - def _side_effect_actionnames(self): - return list(self.FRITZBOX_DATA.keys()) + list(self.FRITZBOX_DATA_INDEXED.keys()) + def _side_effect_action_names(self): + return list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index a795f2073dd..b86e9934b24 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.fritz.const import ( DOMAIN, ERROR_AUTH_INVALID, - ERROR_CONNECTION_ERROR, + ERROR_CANNOT_CONNECT, ERROR_UNKNOWN, ) from homeassistant.components.ssdp import ( @@ -111,6 +111,7 @@ async def test_user_already_configured(hass: HomeAssistant, fc_class_mock): ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" + assert result["errors"]["base"] == "already_configured" async def test_exception_security(hass: HomeAssistant): @@ -156,7 +157,7 @@ async def test_exception_connection(hass: HomeAssistant): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"]["base"] == ERROR_CONNECTION_ERROR + assert result["errors"]["base"] == ERROR_CANNOT_CONNECT async def test_exception_unknown(hass: HomeAssistant): @@ -248,6 +249,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, fc_class_mock): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" + assert result["errors"]["base"] == "cannot_connect" async def test_ssdp_already_configured(hass: HomeAssistant, fc_class_mock): From 40993f3ebb43841ac138b44c739f5de227982b0c Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 14 May 2021 14:12:46 -0400 Subject: [PATCH 414/852] Add DHCP support to goalzero (#50425) --- homeassistant/components/goalzero/__init__.py | 19 ++--- .../components/goalzero/config_flow.py | 81 +++++++++++++++---- .../components/goalzero/manifest.json | 3 + .../components/goalzero/strings.json | 10 ++- .../components/goalzero/translations/en.json | 2 +- homeassistant/generated/dhcp.py | 4 + tests/components/goalzero/__init__.py | 7 ++ tests/components/goalzero/test_config_flow.py | 70 +++++++++++++++- 8 files changed, 164 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index b0883d42a5f..8838d3f20fa 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass, entry): try: await api.get_state() except exceptions.ConnectError as err: - raise UpdateFailed(f"Failed to communicating with API: {err}") from err + raise UpdateFailed(f"Failed to communicating with device {err}") from err coordinator = DataUpdateCoordinator( hass, @@ -84,21 +84,16 @@ class YetiEntity(CoordinatorEntity): @property def device_info(self): """Return the device information of the entity.""" - if self.api.data: - sw_version = self.api.data["firmwareVersion"] - else: - sw_version = None - if self.api.sysdata: - model = self.api.sysdata["model"] - else: - model = model or None - return { + info = { "identifiers": {(DOMAIN, self._server_unique_id)}, "manufacturer": "Goal Zero", - "model": model, "name": self._name, - "sw_version": sw_version, } + if self.api.sysdata: + info["model"] = self.api.sysdata["model"] + if self.api.data: + info["sw_version"] = self.api.data["firmwareVersion"] + return info @property def device_class(self): diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 269885e5c3a..575ff2ba350 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -1,12 +1,18 @@ """Config flow for Goal Zero Yeti integration.""" +from __future__ import annotations + import logging +from typing import Any from goalzero import Yeti, exceptions import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac from .const import DEFAULT_NAME, DOMAIN @@ -20,32 +26,63 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self): + """Initialize a Goal Zero Yeti flow.""" + self.ip_address = None + + async def async_step_dhcp(self, discovery_info): + """Handle dhcp discovery.""" + self.ip_address = discovery_info[IP_ADDRESS] + + await self.async_set_unique_id(discovery_info[MAC_ADDRESS]) + self._abort_if_unique_id_configured(updates={CONF_HOST: self.ip_address}) + self._async_abort_entries_match({CONF_HOST: self.ip_address}) + + _, error = await self._async_try_connect(self.ip_address) + if error is None: + return await self.async_step_confirm_discovery() + return self.async_abort(reason=error) + + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Allow the user to confirm adding the device.""" + if user_input is not None: + return self.async_create_entry( + title="Goal Zero", + data={ + CONF_HOST: self.ip_address, + CONF_NAME: DEFAULT_NAME, + }, + ) + + self._set_confirm_only() + return self.async_show_form( + step_id="confirm_discovery", + description_placeholders={ + CONF_HOST: self.ip_address, + CONF_NAME: DEFAULT_NAME, + }, + ) + async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" errors = {} - if user_input is not None: host = user_input[CONF_HOST] name = user_input[CONF_NAME] self._async_abort_entries_match({CONF_HOST: host}) - try: - await self._async_try_connect(host) - except exceptions.ConnectError: - errors["base"] = "cannot_connect" - _LOGGER.error("Error connecting to device at %s", host) - except exceptions.InvalidHost: - errors["base"] = "invalid_host" - _LOGGER.error("Invalid host at %s", host) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: + mac_address, error = await self._async_try_connect(host) + if error is None: + await self.async_set_unique_id(format_mac(mac_address)) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) return self.async_create_entry( title=name, data={CONF_HOST: host, CONF_NAME: name}, ) + errors["base"] = error user_input = user_input or {} return self.async_show_form( @@ -64,6 +101,18 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) async def _async_try_connect(self, host): - session = async_get_clientsession(self.hass) - api = Yeti(host, self.hass.loop, session) - await api.get_state() + """Try connecting to Goal Zero Yeti.""" + try: + session = async_get_clientsession(self.hass) + api = Yeti(host, self.hass.loop, session) + await api.sysinfo() + except exceptions.ConnectError: + _LOGGER.error("Error connecting to device at %s", host) + return None, "cannot_connect" + except exceptions.InvalidHost: + _LOGGER.error("Invalid host at %s", host) + return None, "invalid_host" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return None, "unknown" + return str(api.sysdata["macAddress"]), None diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json index 0a1bc4df70d..52d3a024955 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -4,6 +4,9 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/goalzero", "requirements": ["goalzero==0.1.7"], + "dhcp": [ + {"hostname": "yeti*"} + ], "codeowners": ["@tkdrob"], "iot_class": "local_polling" } diff --git a/homeassistant/components/goalzero/strings.json b/homeassistant/components/goalzero/strings.json index 92813337e77..5147299b564 100644 --- a/homeassistant/components/goalzero/strings.json +++ b/homeassistant/components/goalzero/strings.json @@ -3,11 +3,15 @@ "step": { "user": { "title": "Goal Zero Yeti", - "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. DHCP reservation must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.", + "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wi-fi network. DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual.", "data": { "host": "[%key:common::config_flow::data::host%]", "name": "[%key:common::config_flow::data::name%]" } + }, + "confirm_discovery": { + "title": "Goal Zero Yeti", + "description": "DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual." } }, "error": { @@ -16,7 +20,9 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "unknown": "[%key:common::config_flow::error::unknown%]" } } } diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json index e6c6e4a7298..2f2e1ac0d2b 100644 --- a/homeassistant/components/goalzero/translations/en.json +++ b/homeassistant/components/goalzero/translations/en.json @@ -14,7 +14,7 @@ "host": "Host", "name": "Name" }, - "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. DHCP reservation must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.", + "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wi-fi network. DHCP reservation is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Set it up in your router settings for the device. Refer to your router's user manual.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 9da371090f5..2a592953123 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -66,6 +66,10 @@ DHCP = [ "domain": "flume", "hostname": "flume-gw-*" }, + { + "domain": "goalzero", + "hostname": "yeti*" + }, { "domain": "gogogate2", "hostname": "ismartgate*" diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py index fb531dfca4b..1b5302dbc1b 100644 --- a/tests/components/goalzero/__init__.py +++ b/tests/components/goalzero/__init__.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, patch +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.const import CONF_HOST, CONF_NAME HOST = "1.2.3.4" @@ -17,6 +18,12 @@ CONF_CONFIG_FLOW = { CONF_NAME: NAME, } +CONF_DHCP_FLOW = { + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", +} + async def _create_mocked_yeti(raise_exception=False): mocked_yeti = AsyncMock() diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 10ef02bfcff..6df5465eff9 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -4,16 +4,18 @@ from unittest.mock import patch from goalzero import exceptions from homeassistant.components.goalzero.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) +from homeassistant.setup import async_setup_component from . import ( CONF_CONFIG_FLOW, CONF_DATA, + CONF_DHCP_FLOW, CONF_HOST, CONF_NAME, NAME, @@ -114,3 +116,69 @@ async def test_flow_user_unknown_error(hass): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} + + +async def test_dhcp_discovery(hass): + """Test we can process the discovery from dhcp.""" + await async_setup_component(hass, "persistent_notification", {}) + mocked_yeti = await _create_mocked_yeti() + with _patch_config_flow_yeti(mocked_yeti), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=CONF_DHCP_FLOW, + ) + assert result["type"] == "form" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_NAME: "Yeti", + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=CONF_DHCP_FLOW, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dhcp_discovery_failed(hass): + """Test failed setup from dhcp.""" + mocked_yeti = await _create_mocked_yeti(True) + with _patch_config_flow_yeti(mocked_yeti) as yetimock: + yetimock.side_effect = exceptions.ConnectError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=CONF_DHCP_FLOW, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + with _patch_config_flow_yeti(mocked_yeti) as yetimock: + yetimock.side_effect = exceptions.InvalidHost + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=CONF_DHCP_FLOW, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "invalid_host" + + with _patch_config_flow_yeti(mocked_yeti) as yetimock: + yetimock.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=CONF_DHCP_FLOW, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" From 1160a5f2390354ed2b61f35c20612e526db76408 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 14 May 2021 14:34:59 -0400 Subject: [PATCH 415/852] Add targets and selectors for services (I-K) (#50542) Co-authored-by: Franck Nijhof --- homeassistant/components/icloud/services.yaml | 3 + homeassistant/components/ifttt/services.yaml | 25 ++- homeassistant/components/ihc/services.yaml | 83 ++++++++- .../components/image_processing/services.yaml | 1 + .../components/input_boolean/services.yaml | 4 + .../components/insteon/services.yaml | 122 ++++++++++++ homeassistant/components/iperf3/services.yaml | 5 + homeassistant/components/isy994/services.yaml | 175 +++++++++++++++--- homeassistant/components/keba/services.yaml | 43 ++++- homeassistant/components/kef/services.yaml | 157 ++++++++++++---- .../components/keyboard/services.yaml | 6 + homeassistant/components/knx/services.yaml | 10 +- homeassistant/components/kodi/services.yaml | 33 +++- 13 files changed, 582 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml index 7a6559d383d..60410701da0 100644 --- a/homeassistant/components/icloud/services.yaml +++ b/homeassistant/components/icloud/services.yaml @@ -1,4 +1,5 @@ update: + name: Update description: Update iCloud devices. fields: account: @@ -10,6 +11,7 @@ update: text: play_sound: + name: Play sound description: Play sound on an Apple device. fields: account: @@ -28,6 +30,7 @@ play_sound: text: display_message: + name: Display message description: Display a message on an Apple device. fields: account: diff --git a/homeassistant/components/ifttt/services.yaml b/homeassistant/components/ifttt/services.yaml index 80bf72e5290..e81faca1acd 100644 --- a/homeassistant/components/ifttt/services.yaml +++ b/homeassistant/components/ifttt/services.yaml @@ -1,27 +1,50 @@ # Describes the format for available ifttt services push_alarm_state: + name: Push alarm state description: Update the alarm state to the specified value. fields: entity_id: description: Name of the alarm control panel which state has to be updated. + required: true example: "alarm_control_panel.downstairs" + selector: + entity: + domain: alarm_control_panel state: + name: State description: The state to which the alarm control panel has to be set. + required: true example: "armed_night" + selector: + text: trigger: + name: Trigger description: Triggers the configured IFTTT Webhook. fields: event: - description: The name of the event to sent. + name: Event + description: The name of the event to send. + required: true example: "MY_HA_EVENT" + selector: + text: value1: + name: Value 1 description: Generic field to send data via the event. example: "Hello World" + selector: + text: value2: + name: Value 2 description: Generic field to send data via the event. example: "some additional data" + selector: + text: value3: + name: Value 3 description: Generic field to send data via the event. example: "even more data" + selector: + text: diff --git a/homeassistant/components/ihc/services.yaml b/homeassistant/components/ihc/services.yaml index a65d5f5b78c..06ef0930e97 100644 --- a/homeassistant/components/ihc/services.yaml +++ b/homeassistant/components/ihc/services.yaml @@ -1,58 +1,133 @@ # Describes the format for available IHC services set_runtime_value_bool: + name: Set runtime value boolean description: Set a boolean runtime value on the IHC controller. fields: controller_id: + name: Controller ID description: | If you have multiple controller, this is the index of you controller - starting with 0 (0 is default) + starting with 0 example: 0 + default: 0 + selector: + number: + min: 0 + max: 100 ihc_id: + name: IHC ID description: The integer IHC resource ID. + required: true example: 123456 + selector: + number: + min: 0 + max: 1000000 + mode: box value: + name: Value description: The boolean value to set. + required: true example: true + selector: + boolean: set_runtime_value_int: + name: Set runtime value integer description: Set an integer runtime value on the IHC controller. fields: controller_id: + name: Controller ID description: | If you have multiple controller, this is the index of you controller - starting with 0 (0 is default) + starting with 0 example: 0 + default: 0 + selector: + number: + min: 0 + max: 100 ihc_id: + name: IHC ID description: The integer IHC resource ID. + required: true example: 123456 + selector: + number: + min: 0 + max: 1000000 + mode: box value: + name: Value description: The integer value to set. + required: true example: 50 + selector: + number: + min: 0 + max: 1000000 + mode: box set_runtime_value_float: + name: Set runtime value float description: Set a float runtime value on the IHC controller. fields: controller_id: + name: Controller ID description: | If you have multiple controller, this is the index of you controller - starting with 0 (0 is default) + starting with 0 example: 0 + default: 0 + selector: + number: + min: 0 + max: 100 ihc_id: + name: IHC ID description: The integer IHC resource ID. + required: true example: 123456 + selector: + number: + min: 0 + max: 1000000 + mode: box value: + name: Value description: The float value to set. + required: true example: 1.47 + selector: + number: + min: 0 + max: 10000 + step: 0.01 + mode: box pulse: + name: Pulse description: Pulses an input on the IHC controller. fields: controller_id: + name: Controller ID description: | If you have multiple controller, this is the index of you controller - starting with 0 (0 is default) + starting with 0 example: 0 + default: 0 + selector: + number: + min: 0 + max: 100 ihc_id: + name: IHC ID description: The integer IHC resource ID. + required: true example: 123456 + selector: + number: + min: 0 + max: 1000000 + mode: box diff --git a/homeassistant/components/image_processing/services.yaml b/homeassistant/components/image_processing/services.yaml index cd074acd9f4..ed4be6047e0 100644 --- a/homeassistant/components/image_processing/services.yaml +++ b/homeassistant/components/image_processing/services.yaml @@ -1,5 +1,6 @@ # Describes the format for available image processing services scan: + name: Scan description: Process an image immediately target: diff --git a/homeassistant/components/input_boolean/services.yaml b/homeassistant/components/input_boolean/services.yaml index 8cefe2b4974..68287cc3ff5 100644 --- a/homeassistant/components/input_boolean/services.yaml +++ b/homeassistant/components/input_boolean/services.yaml @@ -1,14 +1,18 @@ toggle: + name: Toggle description: Toggle an input boolean target: turn_off: + name: Turn off description: Turn off an input boolean target: turn_on: + name: Turn on description: Turn on an input boolean target: reload: + name: Reload description: Reload the input_boolean configuration diff --git a/homeassistant/components/insteon/services.yaml b/homeassistant/components/insteon/services.yaml index 716b9a1e040..b2f8467475e 100644 --- a/homeassistant/components/insteon/services.yaml +++ b/homeassistant/components/insteon/services.yaml @@ -1,68 +1,190 @@ add_all_link: + name: Add all link description: Tells the Insteom Modem (IM) start All-Linking mode. Once the the IM is in All-Linking mode, press the link button on the device to complete All-Linking. fields: group: + name: Group description: All-Link group number. + required: true example: 1 + selector: + number: + min: 0 + max: 255 mode: + name: Mode description: Linking mode controller - IM is controller responder - IM is responder + required: true example: "controller" + selector: + select: + options: + - 'controller' + - 'responder' delete_all_link: + name: Delete all link description: Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link Database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process. fields: group: + name: Group description: All-Link group number. + required: true example: 1 + selector: + number: + min: 0 + max: 255 load_all_link_database: + name: Load all link database description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records. fields: entity_id: + name: Entity description: Name of the device to load. Use "all" to load the database of all devices. + required: true example: "light.1a2b3c" + selector: + text: reload: + name: Reload description: Reload all records. If true the current records are cleared from memory (does not effect the device) and the records are reloaded. If false the existing records are left in place and only missing records are added. Default is false. example: "true" + default: false + selector: + boolean: print_all_link_database: + name: Print all link database description: Print the All-Link Database for a device. Requires that the All-Link Database is loaded into memory. fields: entity_id: + name: Entity description: Name of the device to print + required: true example: "light.1a2b3c" + selector: + entity: + integration: insteon print_im_all_link_database: + name: Print IM all link database description: Print the All-Link Database for the INSTEON Modem (IM). x10_all_units_off: + name: X10 all units off description: Send X10 All Units Off command fields: housecode: + name: Housecode description: X10 house code + required: true example: c + selector: + select: + options: + - 'a' + - 'b' + - 'c' + - 'd' + - 'e' + - 'f' + - 'g' + - 'h' + - 'i' + - 'j' + - 'k' + - 'l' + - 'm' + - 'n' + - 'o' + - 'p' x10_all_lights_on: + name: X10 all lights on description: Send X10 All Lights On command fields: housecode: + name: Housecode description: X10 house code + required: true example: c + selector: + select: + options: + - 'a' + - 'b' + - 'c' + - 'd' + - 'e' + - 'f' + - 'g' + - 'h' + - 'i' + - 'j' + - 'k' + - 'l' + - 'm' + - 'n' + - 'o' + - 'p' x10_all_lights_off: + name: X10 all lights off description: Send X10 All Lights Off command fields: housecode: + name: Housecode description: X10 house code + required: true example: c + selector: + select: + options: + - 'a' + - 'b' + - 'c' + - 'd' + - 'e' + - 'f' + - 'g' + - 'h' + - 'i' + - 'j' + - 'k' + - 'l' + - 'm' + - 'n' + - 'o' + - 'p' scene_on: + name: Scene on description: Trigger an INSTEON scene to turn ON. fields: group: + name: Group description: INSTEON group or scene number + required: true example: 26 + selector: + number: + min: 0 + max: 255 scene_off: + name: Scene off description: Trigger an INSTEON scene to turn OFF. fields: group: + name: Group description: INSTEON group or scene number + required: true example: 26 + selector: + number: + min: 0 + max: 255 add_default_links: + name: Add default links description: Add the default links between the device and the Insteon Modem (IM) fields: entity_id: + name: Entity description: Name of the device to load. Use "all" to load the database of all devices. + required: true example: "light.1a2b3c" + selector: + text: diff --git a/homeassistant/components/iperf3/services.yaml b/homeassistant/components/iperf3/services.yaml index aaa88c2341c..ba0fdb89712 100644 --- a/homeassistant/components/iperf3/services.yaml +++ b/homeassistant/components/iperf3/services.yaml @@ -1,6 +1,11 @@ speedtest: + name: Speedtest description: Immediately execute a speed test with iperf3 fields: host: + name: Host description: The host name of the iperf3 server (already configured) to run a test with. example: "iperf.he.net" + default: None + selector: + text: diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 04fc04083d5..94d5a3cd89d 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -4,112 +4,231 @@ # flooding the ISY with requests. To control multiple devices with a service call # the recommendation is to add a scene in the ISY and control that scene. send_raw_node_command: + name: Send raw node command description: Send a "raw" ISY REST Device Command to a Node using its Home Assistant Entity ID. + target: + entity: + integration: isy994 fields: - entity_id: - description: Name of an entity to send command. - example: "light.front_door" command: + name: Command description: The ISY REST Command to be sent to the device + required: true example: "DON" + selector: + text: value: - description: (Optional) The integer value to be sent with the command. + name: Value + description: The integer value to be sent with the command. example: 255 + selector: + number: + min: 0 + max: 255 parameters: - description: (Optional) A dict of parameters to be sent in the query string (e.g. for controlling colored bulbs). + name: Parameters + description: A dict of parameters to be sent in the query string (e.g. for controlling colored bulbs). example: { GV2: 0, GV3: 0, GV4: 255 } + default: {} + selector: + object: unit_of_measurement: - description: (Optional) The ISY Unit of Measurement (UOM) to send with the command, if required. + name: Unit of measurement + description: The ISY Unit of Measurement (UOM) to send with the command, if required. example: 67 + selector: + number: + min: 0 + max: 120 send_node_command: + name: Send node command description: >- Send a command to an ISY Device using its Home Assistant entity ID. Valid commands are: beep, brighten, dim, disable, enable, fade_down, fade_stop, fade_up, fast_off, fast_on, and query. + target: + entity: + integration: isy994 fields: - entity_id: - description: Name of an entity to send command. - example: "light.front_door" command: + name: Command description: The command to be sent to the device. + required: true example: "fast_on" + selector: + select: + options: + - 'beep' + - 'brighten' + - 'dim' + - 'disable' + - 'enable' + - 'fade_down' + - 'fade_stop' + - 'fade_up' + - 'fast_off' + - 'fast_on' + - 'query' set_on_level: + name: Set on level description: Send a ISY set_on_level command to a Node. + target: + entity: + integration: isy994 + domain: light fields: - entity_id: - description: Name of an entity to send command. - example: "light.front_door" value: - description: integer value to set (0-255). + name: Value + description: integer value to set. + required: true example: 255 + selector: + number: + min: 0 + max: 255 set_ramp_rate: + name: Set ramp rate description: Send a ISY set_ramp_rate command to a Node. + target: + entity: + integration: isy994 + domain: light fields: - entity_id: - description: Name of an entity to send command. - example: "light.front_door" value: - description: Integer value to set (0-31), see PyISY/ISY documentation for values to actual ramp times. + name: Value + description: Integer value to set, see PyISY/ISY documentation for values to actual ramp times. + required: true example: 30 + selector: + number: + min: 0 + max: 31 system_query: + name: System query description: Request the ISY Query the connected devices. fields: address: - description: (Optional) ISY Address to Query. Omitting this requests a system-wide scan (typically scheduled once per day). + name: Address + description: ISY Address to Query. Omitting this requests a system-wide scan (typically scheduled once per day). example: "1A 2B 3C 1" + selector: + text: isy: - description: (Optional) If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). Omitting this will cause all ISYs to be queried. + name: ISY + description: If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). Omitting this will cause all ISYs to be queried. example: "ISY" + selector: + text: set_variable: + name: Set variable description: Set an ISY variable's current or initial value. Variables can be set by either type/address or by name. fields: address: + name: Address description: The address of the variable for which to set the value. example: 5 + selector: + number: + min: 0 + max: 255 type: + name: Type description: The variable type, 1 = Integer, 2 = State. example: 2 + selector: + number: + min: 1 + max: 2 name: - description: (Optional) The name of the variable to set (use instead of type/address). + name: Name + description: The name of the variable to set (use instead of type/address). example: "my_variable_name" + selector: + text: init: - description: (Optional) If True, the initial (init) value will be updated instead of the current value. + name: Init + description: If True, the initial (init) value will be updated instead of the current value. example: false + default: false + selector: + boolean: value: + name: Value description: The integer value to be sent. + required: true example: 255 + selector: + number: + min: 0 + max: 255 isy: - description: (Optional) If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same variable name or address on multiple ISYs, omitting this will run the command on them all. + name: ISY + description: If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same variable name or address on multiple ISYs, omitting this will run the command on them all. example: "ISY" + selector: + text: send_program_command: + name: Send program command description: >- Send a command to control an ISY program or folder. Valid commands are run, run_then, run_else, stop, enable, disable, enable_run_at_startup, and disable_run_at_startup. fields: address: - description: The address of the program to control (optional, use either address or name). + name: Address + description: The address of the program to control (use either address or name). example: "04B1" name: - description: The name of the program to control (optional, use either address or name). + name: Name + description: The name of the program to control (use either address or name). example: "My Program" command: + name: Command description: The ISY Program Command to be sent. + required: true example: "run" + selector: + select: + options: + - 'disable' + - 'disable_run_at_startup' + - 'enable' + - 'enable_run_at_startup' + - 'run' + - 'run_else' + - 'run_then' + - 'stop' isy: - description: (Optional) If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same program name or address on multiple ISYs, omitting this will run the command on them all. + name: ISY + description: If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same program name or address on multiple ISYs, omitting this will run the command on them all. example: "ISY" + selector: + text: run_network_resource: + name: Run network resource description: Run a network resource on the ISY. fields: address: - description: The address of the network resource to execute (optional, use either address or name). + name: Address + description: The address of the network resource to execute (use either address or name). example: 121 + selector: + number: + min: 0 + max: 255 name: - description: The name of the network resource to execute (optional, use either address or name). + name: Name + description: The name of the network resource to execute (use either address or name). example: "Network Resource 1" + selector: + text: isy: - description: (Optional) If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same resource name or address on multiple ISYs, omitting this will run the command on them all. + name: ISY + description: If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same resource name or address on multiple ISYs, omitting this will run the command on them all. example: "ISY" + selector: + text: reload: + name: Reload description: Reload the ISY994 connection(s) without restarting Home Assistant. Use to pick up new devices that have been added or changed on the ISY. cleanup_entities: + name: Cleanup entities description: Cleanup old entities and devices no longer used by the ISY994 integrations. Useful if you've removed devices from the ISY or changed the options in the configuration to exclude additional items. diff --git a/homeassistant/components/keba/services.yaml b/homeassistant/components/keba/services.yaml index 3422d6cf034..10e03f83b08 100644 --- a/homeassistant/components/keba/services.yaml +++ b/homeassistant/components/keba/services.yaml @@ -1,56 +1,97 @@ # Describes the format for available services for KEBA charging staitons request_data: + name: Request data description: > Request new data from the charging station. authorize: + name: Authorize description: > Authorizes a charging process with the predefined RFID tag of the configuration file. deauthorize: + name: Deauthorize description: > Deauthorizes the running charging process with the predefined RFID tag of the configuration file. set_energy: + name: Set energy description: Sets the energy target after which the charging process stops. fields: energy: + name: Energy description: > The energy target to stop charging in kWh. Setting 0 disables the limit. example: 10.0 + selector: + number: + min: 0 + max: 100 + step: 0.1 set_current: + name: Set current description: Sets the maximum current for charging processes. fields: current: + name: Current description: > The maximum current used for the charging process in A. Allowed are values between 6 A and 63 A. Invalid values are discardedand the default is set to 6 A. The value is also depending on the DIP-switchsettings and the used cable of the charging station example: 16 + selector: + number: + min: 0 + max: 100 + step: 0.1 + enable: + name: Enable description: > Starts a charging process if charging station is authorized. disable: + name: Disable description: > Stops the charging process if charging station is authorized. set_failsafe: + name: Set failsafe description: > Set the failsafe mode of the charging station. If all parameters are 0, the failsafe mode will be disabled. fields: failsafe_timeout: + name: Failsafe timeout description: > - Timeout in seconds after which the failsafe mode is triggered, if set_current was not executed during this time. + Timeout after which the failsafe mode is triggered, if set_current was not executed during this time. example: 30 + default: 30 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds failsafe_fallback: + name: Failsafe fallback description: > Fallback current in A to be set after timeout. example: 6 + default: 6 + selector: + number: + min: 0 + max: 100 + step: 0.1 failsafe_persist: + name: Failsafe persist description: > If failsafe_persist is 0, the failsafe option is only until charging station reboot. If failsafe_persist is 1, the failsafe option will survive a reboot. example: 0 + default: 0 + selector: + number: + min: 0 + max: 1 diff --git a/homeassistant/components/kef/services.yaml b/homeassistant/components/kef/services.yaml index 2226d3b6c2d..6822c41dbc1 100644 --- a/homeassistant/components/kef/services.yaml +++ b/homeassistant/components/kef/services.yaml @@ -1,97 +1,174 @@ update_dsp: + name: Update DSP description: Update all DSP settings. - fields: - entity_id: - description: The entity_id of the KEF speaker. - example: media_player.kef_lsx + target: + entity: + integration: kef + domain: media_player set_mode: + name: Set mode description: Set the mode of the speaker. + target: + entity: + integration: kef + domain: media_player + fields: - entity_id: - description: The entity_id of the KEF speaker. - example: media_player.kef_lsx desk_mode: - description: > - "Desk mode" (true or false) + name: Desk mode + description: Desk mode. example: true + selector: + boolean: wall_mode: - description: > - "Wall mode" (true or false) + name: Wall mode + description: Wall mode. example: true + selector: + boolean: phase_correction: - description: > - "Phase correction" (true or false) + name: Phase correction + description: Phase correction. example: true + selector: + boolean: high_pass: - description: > - "High-pass mode" (true or false) + name: High pass + description: High-pass mode". example: true + selector: + boolean: sub_polarity: - description: > - "Sub polarity" ("-" or "+") + name: Subwoofer polarity + description: Sub polarity. example: "+" + selector: + select: + options: + - '-' + - '+' bass_extension: - description: > - "Bass extension" selector ("Less", "Standard", or "Extra") + name: Base extension + description: Bass extension. example: "Extra" + selector: + select: + options: + - 'Less' + - 'Standard' + - 'Extra' set_desk_db: + name: Set desk dB description: Set the "Desk mode" slider of the speaker in dB. fields: entity_id: + name: Entity description: The entity_id of the KEF speaker. example: media_player.kef_lsx db_value: - description: Value of the slider (-6 to 0 with steps of 0.5) + name: dB value + description: Value of the slider example: 0.0 + selector: + number: + min: -6 + max: 0 + step: 0.5 + unit_of_measurement: dB set_wall_db: + name: Set wall dB description: Set the "Wall mode" slider of the speaker in dB. + target: + entity: + integration: kef + domain: media_player fields: - entity_id: - description: The entity_id of the KEF speaker. - example: media_player.kef_lsx db_value: - description: Value of the slider (-6 to 0 with steps of 0.5) + name: dB value + description: Value of the slider. example: 0.0 + selector: + number: + min: -6 + max: 0 + step: 0.5 + unit_of_measurement: dB set_treble_db: + name: Set treble dB description: Set desk the "Treble trim" slider of the speaker in dB. + target: + entity: + integration: kef + domain: media_player fields: - entity_id: - description: The entity_id of the KEF speaker. - example: media_player.kef_lsx db_value: - description: Value of the slider (-2 to 2 with steps of 0.5) + name: dB value + description: Value of the slider. example: 0.0 + selector: + number: + min: -2 + max: 2 + step: 0.5 + unit_of_measurement: dB set_high_hz: + name: Set high hertz description: Set the "High-pass mode" slider of the speaker in Hz. + target: + entity: + integration: kef + domain: media_player fields: - entity_id: - description: The entity_id of the KEF speaker. - example: media_player.kef_lsx hz_value: - description: Value of the slider (50 to 120 with steps of 5) + name: Hertz value + description: Value of the slider. example: 95 + selector: + number: + min: 50 + max: 120 + step: 5 + unit_of_measurement: Hz set_low_hz: + name: Set low Hertz description: Set the "Sub out low-pass frequency" slider of the speaker in Hz. + target: + entity: + integration: kef + domain: media_player fields: - entity_id: - description: The entity_id of the KEF speaker. - example: media_player.kef_lsx hz_value: - description: Value of the slider (40 to 250 with steps of 5) + name: Hertz value + description: Value of the slider. example: 80 + selector: + number: + min: 40 + max: 250 + step: 5 + unit_of_measurement: Hz set_sub_db: + name: Set subwoofer dB description: Set the "Sub gain" slider of the speaker in dB. + target: + entity: + integration: kef + domain: media_player fields: - entity_id: - description: The entity_id of the KEF speaker. - example: media_player.kef_lsx db_value: - description: Value of the slider (-10 to 10 with steps of 1) + name: dB value + description: Value of the slider. example: 0 + selector: + number: + min: -10 + max: 10 + step: 1 + unit_of_measurement: dB diff --git a/homeassistant/components/keyboard/services.yaml b/homeassistant/components/keyboard/services.yaml index d0919d59514..07f02959c39 100644 --- a/homeassistant/components/keyboard/services.yaml +++ b/homeassistant/components/keyboard/services.yaml @@ -1,29 +1,35 @@ volume_up: + name: Volume up description: Simulates a key press of the "Volume Up" button on Home Assistant's host machine volume_down: + name: Volume down description: Simulates a key press of the "Volume Down" button on Home Assistant's host machine volume_mute: + name: Volume mute description: Simulates a key press of the "Volume Mute" button on Home Assistant's host machine media_play_pause: + name: Media play/pause description: Simulates a key press of the "Media Play/Pause" button on Home Assistant's host machine media_next_track: + name: Media next track description: Simulates a key press of the "Media Next Track" button on Home Assistant's host machine media_prev_track: + name: Media previous track description: Simulates a key press of the "Media Previous Track" button on Home Assistant's host machine diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index c13abb23d94..6090057feca 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -18,7 +18,7 @@ send: object: type: name: "Value type" - description: "Optional. If set, the payload will not be sent as raw bytes, but encoded as given DPT. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)." + description: "If set, the payload will not be sent as raw bytes, but encoded as given DPT. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)." required: false example: "temperature" selector: @@ -47,7 +47,7 @@ event_register: object: remove: name: "Remove event registration" - description: "Optional. If `True` the group address(es) will be removed." + description: "If `True` the group address(es) will be removed." default: false selector: boolean: @@ -78,19 +78,19 @@ exposure_register: entity: attribute: name: "Entity attribute" - description: "Optional. Attribute of the entity that shall be sent to the KNX bus. If not set the state will be sent. Eg. for a light the state is eigther “on” or “off” - with attribute you can expose its “brightness”." + description: "Attribute of the entity that shall be sent to the KNX bus. If not set the state will be sent. Eg. for a light the state is eigther “on” or “off” - with attribute you can expose its “brightness”." example: "brightness" selector: text: default: name: "Default value" - description: "Optional. Default value to send to the bus if the state or attribute value is None. Eg. a light with state “off” has no brightness attribute so a default value of 0 could be used. If not set (or None) no value would be sent to the bus and a GroupReadRequest to the address would return the last known value." + description: "Default value to send to the bus if the state or attribute value is None. Eg. a light with state “off” has no brightness attribute so a default value of 0 could be used. If not set (or None) no value would be sent to the bus and a GroupReadRequest to the address would return the last known value." example: "0" selector: object: remove: name: "Remove exposure" - description: "Optional. If `True` the exposure will be removed. Only `address` is required for removal." + description: "If `True` the exposure will be removed. Only `address` is required for removal." default: false selector: boolean: diff --git a/homeassistant/components/kodi/services.yaml b/homeassistant/components/kodi/services.yaml index 11287217fa8..cf6cdfc240d 100644 --- a/homeassistant/components/kodi/services.yaml +++ b/homeassistant/components/kodi/services.yaml @@ -1,30 +1,51 @@ # Describes the format for available Kodi services add_to_playlist: + name: Add to playlist description: Add music to the default playlist (i.e. playlistid=0). + target: + entity: + integration: kodi + domain: media_player fields: - entity_id: - description: Name(s) of the Kodi entities where to add the media. - example: "media_player.living_room_kodi" media_type: + name: Media type description: Media type identifier. It must be one of SONG or ALBUM. + required: true example: ALBUM + selector: + text: media_id: + name: Media ID description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library. example: 123456 + selector: + text: media_name: + name: Media Name description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist. example: "Highway to Hell" + selector: + text: artist_name: + name: Artist name description: Optional artist name for filtering media. example: "AC/DC" + selector: + text: call_method: + name: Call method description: "Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`." + target: + entity: + integration: kodi + domain: media_player fields: - entity_id: - description: Name(s) of the Kodi entities where to run the API method. - example: "media_player.living_room_kodi" method: + name: Method description: Name of the Kodi JSONRPC API method to be called. + required: true example: "VideoLibrary.GetRecentlyAddedEpisodes" + selector: + text: From 2334e98806ee548c021457b6c37721a517e98295 Mon Sep 17 00:00:00 2001 From: Raphael Date: Fri, 14 May 2021 15:41:13 -0400 Subject: [PATCH 416/852] Add Etekcity VeSync light bulbs to Homeassistant (#50272) --- homeassistant/components/vesync/common.py | 4 + homeassistant/components/vesync/light.py | 173 +++++++++++++++++----- 2 files changed, 143 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index fcab5bb5a63..d51da7a375b 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -21,6 +21,10 @@ async def async_process_devices(hass, manager): devices[VS_FANS].extend(manager.fans) _LOGGER.info("%d VeSync fans found", len(manager.fans)) + if manager.bulbs: + devices[VS_LIGHTS].extend(manager.bulbs) + _LOGGER.info("%d VeSync lights found", len(manager.bulbs)) + if manager.outlets: devices[VS_SWITCHES].extend(manager.outlets) _LOGGER.info("%d VeSync outlets found", len(manager.outlets)) diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index b98c87e5a7f..b747c10ee4e 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -1,9 +1,11 @@ -"""Support for VeSync dimmers.""" +"""Support for VeSync bulbs and wall dimmers.""" import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, + ATTR_COLOR_TEMP, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, LightEntity, ) from homeassistant.core import callback @@ -15,8 +17,10 @@ from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_LIGHTS _LOGGER = logging.getLogger(__name__) DEV_TYPE_TO_HA = { - "ESD16": "light", - "ESWD16": "light", + "ESD16": "walldimmer", + "ESWD16": "walldimmer", + "ESL100": "bulb-dimmable", + "ESL100CW": "bulb-tunable-white", } @@ -40,8 +44,10 @@ def _async_setup_entities(devices, async_add_entities): """Check if device is online and add entity.""" entities = [] for dev in devices: - if DEV_TYPE_TO_HA.get(dev.device_type) == "light": - entities.append(VeSyncDimmerHA(dev)) + if DEV_TYPE_TO_HA.get(dev.device_type) in ("walldimmer", "bulb-dimmable"): + entities.append(VeSyncDimmableLightHA(dev)) + elif DEV_TYPE_TO_HA.get(dev.device_type) in ("bulb-tunable-white"): + entities.append(VeSyncTunableWhiteLightHA(dev)) else: _LOGGER.debug( "%s - Unknown device type - %s", dev.device_name, dev.device_type @@ -51,34 +57,133 @@ def _async_setup_entities(devices, async_add_entities): async_add_entities(entities, update_before_add=True) -class VeSyncDimmerHA(VeSyncDevice, LightEntity): - """Representation of a VeSync dimmer.""" - - def __init__(self, dimmer): - """Initialize the VeSync dimmer device.""" - super().__init__(dimmer) - self.dimmer = dimmer - - def turn_on(self, **kwargs): - """Turn the device on.""" - if ATTR_BRIGHTNESS in kwargs: - # get brightness from HA data - brightness = int(kwargs[ATTR_BRIGHTNESS]) - # convert to percent that vesync api expects - brightness = round((brightness / 255) * 100) - # clamp to 1-100 - brightness = max(1, min(brightness, 100)) - self.dimmer.set_brightness(brightness) - # Avoid turning device back on if this is just a brightness adjustment - if not self.is_on: - self.device.turn_on() - - @property - def supported_features(self): - """Get supported features for this entity.""" - return SUPPORT_BRIGHTNESS +class VeSyncBaseLight(VeSyncDevice, LightEntity): + """Base class for VeSync Light Devices Representations.""" @property def brightness(self): - """Get dimmer brightness.""" - return round((int(self.dimmer.brightness) / 100) * 255) + """Get light brightness.""" + # get value from pyvesync library api, + result = self.device.brightness + try: + # check for validity of brightness value received + brightness_value = int(result) + except ValueError: + # deal if any unexpected/non numeric value + _LOGGER.debug( + "VeSync - received unexpected 'brightness' value from pyvesync api: %s", + result, + ) + return 0 + # convert percent brightness to ha expected range + return round((max(1, brightness_value) / 100) * 255) + + def turn_on(self, **kwargs): + """Turn the device on.""" + attribute_adjustment_only = False + # set white temperature + if self.color_mode in (COLOR_MODE_COLOR_TEMP) and ATTR_COLOR_TEMP in kwargs: + # get white temperature from HA data + color_temp = int(kwargs[ATTR_COLOR_TEMP]) + # ensure value between min-max supported Mireds + color_temp = max(self.min_mireds, min(color_temp, self.max_mireds)) + # convert Mireds to Percent value that api expects + color_temp = round( + ((color_temp - self.min_mireds) / (self.max_mireds - self.min_mireds)) + * 100 + ) + # flip cold/warm to what pyvesync api expects + color_temp = 100 - color_temp + # ensure value between 0-100 + color_temp = max(0, min(color_temp, 100)) + # call pyvesync library api method to set color_temp + self.device.set_color_temp(color_temp) + # flag attribute_adjustment_only, so it doesn't turn_on the device redundantly + attribute_adjustment_only = True + # set brightness level + if ( + self.color_mode in (COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP) + and ATTR_BRIGHTNESS in kwargs + ): + # get brightness from HA data + brightness = int(kwargs[ATTR_BRIGHTNESS]) + # ensure value between 1-255 + brightness = max(1, min(brightness, 255)) + # convert to percent that vesync api expects + brightness = round((brightness / 255) * 100) + # ensure value between 1-100 + brightness = max(1, min(brightness, 100)) + # call pyvesync library api method to set brightness + self.device.set_brightness(brightness) + # flag attribute_adjustment_only, so it doesn't turn_on the device redundantly + attribute_adjustment_only = True + # check flag if should skip sending the turn_on command + if attribute_adjustment_only: + return + # send turn_on command to pyvesync api + self.device.turn_on() + + +class VeSyncDimmableLightHA(VeSyncBaseLight, LightEntity): + """Representation of a VeSync dimmable light device.""" + + @property + def color_mode(self): + """Set color mode for this entity.""" + return COLOR_MODE_BRIGHTNESS + + @property + def supported_color_modes(self): + """Flag supported color_modes (in an array format).""" + return [COLOR_MODE_BRIGHTNESS] + + +class VeSyncTunableWhiteLightHA(VeSyncBaseLight, LightEntity): + """Representation of a VeSync Tunable White Light device.""" + + @property + def color_temp(self): + """Get device white temperature.""" + # get value from pyvesync library api, + result = self.device.color_temp_pct + try: + # check for validity of brightness value received + color_temp_value = int(result) + except ValueError: + # deal if any unexpected/non numeric value + _LOGGER.debug( + "VeSync - received unexpected 'color_temp_pct' value from pyvesync api: %s", + result, + ) + return 0 + # flip cold/warm + color_temp_value = 100 - color_temp_value + # ensure value between 0-100 + color_temp_value = max(0, min(color_temp_value, 100)) + # convert percent value to Mireds + color_temp_value = round( + self.min_mireds + + ((self.max_mireds - self.min_mireds) / 100 * color_temp_value) + ) + # ensure value between minimum and maximum Mireds + return max(self.min_mireds, min(color_temp_value, self.max_mireds)) + + @property + def min_mireds(self): + """Set device coldest white temperature.""" + return 154 # 154 Mireds ( 1,000,000 divided by 6500 Kelvin = 154 Mireds) + + @property + def max_mireds(self): + """Set device warmest white temperature.""" + return 370 # 370 Mireds ( 1,000,000 divided by 2700 Kelvin = 370 Mireds) + + @property + def color_mode(self): + """Set color mode for this entity.""" + return COLOR_MODE_COLOR_TEMP + + @property + def supported_color_modes(self): + """Flag supported color_modes (in an array format).""" + return [COLOR_MODE_COLOR_TEMP] From 7fd2f8090d0462ffa8135cc3f464f25b633dd882 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 14 May 2021 22:22:35 +0200 Subject: [PATCH 417/852] Fix grpc Alpine 3.13 / i386 (#50623) --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 583a2ea4211..0497ac32e86 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -41,6 +41,7 @@ jobs: echo "GRPC_BUILD_WITH_BORING_SSL_ASM=false" echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true" echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true" + echo "GRPC_PYTHON_DISABLE_LIBC_COMPATIBILITY=true" ) > .env_file - name: Upload env_file From 646af533f0cf89be1d2b7187ec35849dda8bc6aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 May 2021 13:39:57 -0700 Subject: [PATCH 418/852] Add support for Hue push updates (#50591) --- homeassistant/components/hue/bridge.py | 40 ++++++- homeassistant/components/hue/hue_event.py | 6 +- homeassistant/components/hue/light.py | 9 ++ homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hue/sensor.py | 3 - homeassistant/components/hue/sensor_base.py | 4 +- homeassistant/components/hue/sensor_device.py | 13 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/conftest.py | 46 +++----- tests/components/hue/test_bridge.py | 104 ++++++++++++++++-- .../hue/test_init_multiple_bridges.py | 43 +------- 12 files changed, 181 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 776ebbeb1f6..c01dc771f3e 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -49,7 +49,7 @@ class HueBridge: # Jobs to be executed when API is reset. self.reset_jobs = [] self.sensor_manager = None - self.unsub_config_entry_listener = None + self._update_callbacks = {} @property def host(self): @@ -111,9 +111,8 @@ class HueBridge: 3 if self.api.config.modelid == "BSB001" else 10 ) - self.unsub_config_entry_listener = self.config_entry.add_update_listener( - _update_listener - ) + self.reset_jobs.append(self.config_entry.add_update_listener(_update_listener)) + self.reset_jobs.append(asyncio.create_task(self._subscribe_events()).cancel) self.authorized = True return True @@ -168,8 +167,7 @@ class HueBridge: while self.reset_jobs: self.reset_jobs.pop()() - if self.unsub_config_entry_listener is not None: - self.unsub_config_entry_listener() + self._update_callbacks = {} # If setup was successful, we set api variable, forwarded entry and # register service @@ -236,6 +234,36 @@ class HueBridge: self.authorized = False create_config_flow(self.hass, self.host) + async def _subscribe_events(self): + """Subscribe to Hue events.""" + try: + async for updated_object in self.api.listen_events(): + key = (updated_object.ITEM_TYPE, updated_object.id) + + if key in self._update_callbacks: + self._update_callbacks[key]() + + except GeneratorExit: + pass + + @core.callback + def listen_updates(self, item_type, item_id, update_callback): + """Listen to updates.""" + callbacks = self._update_callbacks + key = (item_type, item_id) + + if key in callbacks: + _LOGGER.warning("Overwriting update callback for %s", key) + + callbacks[key] = update_callback + + @core.callback + def unsub(): + if callbacks.get(key) == update_callback: + callbacks.pop(key) + + return unsub + async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): """Create a bridge object and verify authentication.""" diff --git a/homeassistant/components/hue/hue_event.py b/homeassistant/components/hue/hue_event.py index 28c6ac3a594..f2edc129f10 100644 --- a/homeassistant/components/hue/hue_event.py +++ b/homeassistant/components/hue/hue_event.py @@ -39,7 +39,11 @@ class HueEvent(GenericHueDevice): self.async_update_callback ) ) - _LOGGER.debug("Hue event created: %s", self.event_id) + self.bridge.reset_jobs.append( + self.bridge.listen_updates( + self.sensor.ITEM_TYPE, self.sensor.id, self.async_update_callback + ) + ) @callback def async_update_callback(self): diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index e139f5a0c95..18c8444ce65 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -448,6 +448,15 @@ class HueLight(CoordinatorEntity, LightEntity): return info + async def async_added_to_hass(self) -> None: + """Handle entity being added to Home Assistant.""" + self.async_on_remove( + self.bridge.listen_updates( + self.light.ITEM_TYPE, self.light.id, self.async_write_ha_state + ) + ) + await super().async_added_to_hass() + async def async_turn_on(self, **kwargs): """Turn the specified or all lights on.""" command = {"on": True} diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 76d02d2b9fc..896c9c7a048 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.3.1"], + "requirements": ["aiohue==2.4.2"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 3cd3b002f98..988dfc31b35 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -37,9 +37,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class GenericHueGaugeSensorEntity(GenericZLLSensor, SensorEntity): """Parent class for all 'gauge' Hue device sensors.""" - async def _async_update_ha_state(self, *args, **kwargs): - await self.async_update_ha_state(self, *args, **kwargs) - class HueLightLevel(GenericHueGaugeSensorEntity): """The light level sensor entity for a Hue motion sensor device.""" diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index b8e2af138b2..bb527c63b2a 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -166,9 +166,6 @@ class GenericHueSensor(GenericHueDevice, entity.Entity): should_poll = False - async def _async_update_ha_state(self, *args, **kwargs): - raise NotImplementedError - @property def available(self): """Return if sensor is available.""" @@ -185,6 +182,7 @@ class GenericHueSensor(GenericHueDevice, entity.Entity): async def async_added_to_hass(self): """When entity is added to hass.""" + await super().async_added_to_hass() self.async_on_remove( self.bridge.sensor_manager.coordinator.async_add_listener( self.async_write_ha_state diff --git a/homeassistant/components/hue/sensor_device.py b/homeassistant/components/hue/sensor_device.py index 91719debeb5..8670f0853a3 100644 --- a/homeassistant/components/hue/sensor_device.py +++ b/homeassistant/components/hue/sensor_device.py @@ -1,8 +1,10 @@ """Support for the Philips Hue sensor devices.""" +from homeassistant.helpers import entity + from .const import DOMAIN as HUE_DOMAIN -class GenericHueDevice: +class GenericHueDevice(entity.Entity): """Representation of a Hue device.""" def __init__(self, sensor, name, bridge, primary_sensor=None): @@ -51,3 +53,12 @@ class GenericHueDevice: "sw_version": self.primary_sensor.swversion, "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), } + + async def async_added_to_hass(self) -> None: + """Handle entity being added to Home Assistant.""" + self.async_on_remove( + self.bridge.listen_updates( + self.sensor.ITEM_TYPE, self.sensor.id, self.async_write_ha_state + ) + ) + await super().async_added_to_hass() diff --git a/requirements_all.txt b/requirements_all.txt index f6a0cf43063..494dac6d1e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.3.1 +aiohue==2.4.2 # homeassistant.components.imap aioimaplib==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04102316aaf..6164462ceca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.3.1 +aiohue==2.4.2 # homeassistant.components.apache_kafka aiokafka==0.6.0 diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index f8bee35425c..ed31f00d9cc 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -1,5 +1,6 @@ """Test helpers for Hue.""" from collections import deque +import logging from unittest.mock import AsyncMock, Mock, patch from aiohue.groups import Groups @@ -30,46 +31,31 @@ def create_mock_bridge(hass): authorized=True, allow_unreachable=False, allow_groups=False, - api=Mock(), + api=create_mock_api(hass), reset_jobs=[], spec=hue.HueBridge, ) bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) - bridge.mock_requests = [] - # We're using a deque so we can schedule multiple responses - # and also means that `popleft()` will blow up if we get more updates - # than expected. - bridge.mock_light_responses = deque() - bridge.mock_group_responses = deque() - bridge.mock_sensor_responses = deque() - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - bridge.mock_requests.append(kwargs) - - if path == "lights": - return bridge.mock_light_responses.popleft() - if path == "groups": - return bridge.mock_group_responses.popleft() - if path == "sensors": - return bridge.mock_sensor_responses.popleft() - return None + bridge.mock_requests = bridge.api.mock_requests + bridge.mock_light_responses = bridge.api.mock_light_responses + bridge.mock_group_responses = bridge.api.mock_group_responses + bridge.mock_sensor_responses = bridge.api.mock_sensor_responses async def async_request_call(task): await task() bridge.async_request_call = async_request_call - bridge.api.config.apiversion = "9.9.9" - bridge.api.lights = Lights({}, mock_request) - bridge.api.groups = Groups({}, mock_request) - bridge.api.sensors = Sensors({}, mock_request) return bridge @pytest.fixture def mock_api(hass): """Mock the Hue api.""" + return create_mock_api(hass) + + +def create_mock_api(hass): + """Create a mock API.""" api = Mock(initialize=AsyncMock()) api.mock_requests = [] api.mock_light_responses = deque() @@ -92,11 +78,13 @@ def mock_api(hass): return api.mock_scene_responses.popleft() return None + logger = logging.getLogger(__name__) + api.config.apiversion = "9.9.9" - api.lights = Lights({}, mock_request) - api.groups = Groups({}, mock_request) - api.sensors = Sensors({}, mock_request) - api.scenes = Scenes({}, mock_request) + api.lights = Lights(logger, {}, mock_request) + api.groups = Groups(logger, {}, mock_request) + api.sensors = Sensors(logger, {}, mock_request) + api.scenes = Scenes(logger, {}, mock_request) return api diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index bc7573851ad..ee980c6bffe 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -1,4 +1,5 @@ """Test Hue bridge.""" +import asyncio from unittest.mock import AsyncMock, Mock, patch import pytest @@ -12,8 +13,19 @@ from homeassistant.components.hue.const import ( ) from homeassistant.exceptions import ConfigEntryNotReady +ORIG_SUBSCRIBE_EVENTS = bridge.HueBridge._subscribe_events -async def test_bridge_setup(hass): + +@pytest.fixture(autouse=True) +def mock_subscribe_events(): + """Mock subscribe events method.""" + with patch( + "homeassistant.components.hue.bridge.HueBridge._subscribe_events" + ) as mock: + yield mock + + +async def test_bridge_setup(hass, mock_subscribe_events): """Test a successful setup.""" entry = Mock() api = Mock(initialize=AsyncMock()) @@ -31,6 +43,8 @@ async def test_bridge_setup(hass): forward_entries = {c[1][1] for c in mock_forward.mock_calls} assert forward_entries == {"light", "binary_sensor", "sensor"} + assert len(mock_subscribe_events.mock_calls) == 1 + async def test_bridge_setup_invalid_username(hass): """Test we start config flow if username is no longer whitelisted.""" @@ -78,20 +92,23 @@ async def test_reset_if_entry_had_wrong_auth(hass): assert await hue_bridge.async_reset() -async def test_reset_unloads_entry_if_setup(hass): +async def test_reset_unloads_entry_if_setup(hass, mock_subscribe_events): """Test calling reset while the entry has been setup.""" entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} hue_bridge = bridge.HueBridge(hass, entry) - with patch.object(bridge, "authenticate_bridge", return_value=Mock()), patch( - "aiohue.Bridge", return_value=Mock() + with patch.object(bridge, "authenticate_bridge"), patch( + "aiohue.Bridge" ), patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: assert await hue_bridge.async_setup() is True + await asyncio.sleep(0) + assert len(hass.services.async_services()) == 0 assert len(mock_forward.mock_calls) == 3 + assert len(mock_subscribe_events.mock_calls) == 1 with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True @@ -109,9 +126,7 @@ async def test_handle_unauthorized(hass): entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} hue_bridge = bridge.HueBridge(hass, entry) - with patch.object(bridge, "authenticate_bridge", return_value=Mock()), patch( - "aiohue.Bridge", return_value=Mock() - ): + with patch.object(bridge, "authenticate_bridge"), patch("aiohue.Bridge"): assert await hue_bridge.async_setup() is True assert hue_bridge.authorized is True @@ -282,3 +297,78 @@ async def test_hue_activate_scene_scene_not_found(hass, mock_api): call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner"} with patch("aiohue.Bridge", return_value=mock_api): assert await hue_bridge.hue_activate_scene(call.data) is False + + +async def test_event_updates(hass, caplog): + """Test calling reset while the entry has been setup.""" + events = asyncio.Queue() + + async def iterate_queue(): + while True: + event = await events.get() + if event is None: + return + yield event + + async def wait_empty_queue(): + count = 0 + while not events.empty() and count < 50: + await asyncio.sleep(0) + count += 1 + + hue_bridge = bridge.HueBridge(None, None) + hue_bridge.api = Mock(listen_events=iterate_queue) + subscription_task = asyncio.create_task(ORIG_SUBSCRIBE_EVENTS(hue_bridge)) + + calls = [] + + def obj_updated(): + calls.append(True) + + unsub = hue_bridge.listen_updates("lights", "2", obj_updated) + + events.put_nowait(Mock(ITEM_TYPE="lights", id="1")) + + await wait_empty_queue() + assert len(calls) == 0 + + events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) + + await wait_empty_queue() + assert len(calls) == 1 + + unsub() + + events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) + + await wait_empty_queue() + assert len(calls) == 1 + + # Test we can override update listener. + def obj_updated_false(): + calls.append(False) + + unsub = hue_bridge.listen_updates("lights", "2", obj_updated) + unsub_false = hue_bridge.listen_updates("lights", "2", obj_updated_false) + + assert "Overwriting update callback" in caplog.text + + events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) + + await wait_empty_queue() + assert len(calls) == 2 + assert calls[-1] is False + + # Also call multiple times to make sure that works. + unsub() + unsub() + unsub_false() + unsub_false() + + events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) + + await wait_empty_queue() + assert len(calls) == 2 + + events.put_nowait(None) + await subscription_task diff --git a/tests/components/hue/test_init_multiple_bridges.py b/tests/components/hue/test_init_multiple_bridges.py index cd36a5d4f77..1e3df824a38 100644 --- a/tests/components/hue/test_init_multiple_bridges.py +++ b/tests/components/hue/test_init_multiple_bridges.py @@ -1,16 +1,13 @@ """Test Hue init with multiple bridges.""" -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import patch -from aiohue.groups import Groups -from aiohue.lights import Lights -from aiohue.scenes import Scenes -from aiohue.sensors import Sensors import pytest from homeassistant.components import hue -from homeassistant.components.hue import sensor_base as hue_sensor_base from homeassistant.setup import async_setup_component +from .conftest import create_mock_bridge + from tests.common import MockConfigEntry @@ -144,37 +141,3 @@ def mock_bridge1(hass): def mock_bridge2(hass): """Mock a Hue bridge.""" return create_mock_bridge(hass) - - -def create_mock_bridge(hass): - """Create a mock Hue bridge.""" - bridge = Mock( - hass=hass, - available=True, - authorized=True, - allow_unreachable=False, - allow_groups=False, - api=Mock(), - reset_jobs=[], - spec=hue.HueBridge, - async_setup=AsyncMock(return_value=True), - ) - bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) - bridge.mock_requests = [] - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - bridge.mock_requests.append(kwargs) - return {} - - async def async_request_call(task): - await task() - - bridge.async_request_call = async_request_call - bridge.api.config.apiversion = "9.9.9" - bridge.api.lights = Lights({}, mock_request) - bridge.api.groups = Groups({}, mock_request) - bridge.api.sensors = Sensors({}, mock_request) - bridge.api.scenes = Scenes({}, mock_request) - return bridge From 960ed13f9442f783ce3045acce5b64f8971faa14 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 14 May 2021 22:58:37 +0200 Subject: [PATCH 419/852] Update light device actions to check supported_color_modes (#50611) --- homeassistant/components/light/__init__.py | 6 +-- .../components/light/device_action.py | 43 ++++++++++++++++--- tests/components/light/test_device_action.py | 27 +++++++++--- 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 0bc68702467..caf3ac209cb 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -95,21 +95,21 @@ def valid_supported_color_modes(color_modes: Iterable[str]) -> set[str]: return color_modes -def brightness_supported(color_modes: Iterable[str]) -> bool: +def brightness_supported(color_modes: Iterable[str] | None) -> bool: """Test if brightness is supported.""" if not color_modes: return False return any(mode in COLOR_MODES_BRIGHTNESS for mode in color_modes) -def color_supported(color_modes: Iterable[str]) -> bool: +def color_supported(color_modes: Iterable[str] | None) -> bool: """Test if color is supported.""" if not color_modes: return False return any(mode in COLOR_MODES_COLOR for mode in color_modes) -def color_temp_supported(color_modes: Iterable[str]) -> bool: +def color_temp_supported(color_modes: Iterable[str] | None) -> bool: """Test if color temperature is supported.""" if not color_modes: return False diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 9cdb5764d70..3de2218d7c7 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -13,11 +13,17 @@ from homeassistant.components.light import ( ) from homeassistant.const import ATTR_ENTITY_ID, CONF_DOMAIN, CONF_TYPE, SERVICE_TURN_ON from homeassistant.core import Context, HomeAssistant, HomeAssistantError -from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS +from . import ( + ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP_PCT, + ATTR_SUPPORTED_COLOR_MODES, + DOMAIN, + brightness_supported, +) TYPE_BRIGHTNESS_INCREASE = "brightness_increase" TYPE_BRIGHTNESS_DECREASE = "brightness_decrease" @@ -37,6 +43,25 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) +def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set | None: + """Get supported color modes for a light entity. + + First try the statemachine, then entity registry. + """ + state = hass.states.get(entity_id) + if state: + return state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(entity_id) + if not entry: + raise HomeAssistantError(f"Unknown entity {entity_id}") + if not entry.capabilities: + return None + + return entry.capabilities.get(ATTR_SUPPORTED_COLOR_MODES) + + async def async_call_action_from_config( hass: HomeAssistant, config: ConfigType, @@ -77,15 +102,16 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions.""" actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN) - registry = await entity_registry.async_get_registry(hass) + entity_registry = er.async_get(hass) - for entry in entity_registry.async_entries_for_device(registry, device_id): + for entry in er.async_entries_for_device(entity_registry, device_id): if entry.domain != DOMAIN: continue + supported_color_modes = get_supported_color_modes(hass, entry.entity_id) supported_features = get_supported_features(hass, entry.entity_id) - if supported_features & SUPPORT_BRIGHTNESS: + if brightness_supported(supported_color_modes): actions.extend( ( { @@ -123,6 +149,11 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di if config[CONF_TYPE] != toggle_entity.CONF_TURN_ON: return {} + try: + supported_color_modes = get_supported_color_modes(hass, config[ATTR_ENTITY_ID]) + except HomeAssistantError: + supported_color_modes = None + try: supported_features = get_supported_features(hass, config[ATTR_ENTITY_ID]) except HomeAssistantError: @@ -130,7 +161,7 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di extra_fields = {} - if supported_features & SUPPORT_BRIGHTNESS: + if brightness_supported(supported_color_modes): extra_fields[vol.Optional(ATTR_BRIGHTNESS_PCT)] = VALID_BRIGHTNESS_PCT if supported_features & SUPPORT_FLASH: diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 5d6ca2f4a2c..440cae8f0fc 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -3,10 +3,11 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.light import ( + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_BRIGHTNESS, DOMAIN, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, SUPPORT_FLASH, ) from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON @@ -55,7 +56,8 @@ async def test_get_actions(hass, device_reg, entity_reg): "test", "5678", device_id=device_entry.id, - supported_features=SUPPORT_BRIGHTNESS | SUPPORT_FLASH, + supported_features=SUPPORT_FLASH, + capabilities={"supported_color_modes": ["brightness"]}, ) expected_actions = [ { @@ -132,13 +134,15 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): @pytest.mark.parametrize( - "set_state,num_actions,supported_features_reg,supported_features_state,expected_capabilities", + "set_state,num_actions,supported_features_reg,supported_features_state,capabilities_reg,attributes_state,expected_capabilities", [ ( False, 5, - SUPPORT_BRIGHTNESS, 0, + 0, + {ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_BRIGHTNESS]}, + {}, { "turn_on": [ { @@ -155,7 +159,9 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): True, 5, 0, - SUPPORT_BRIGHTNESS, + 0, + None, + {ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_BRIGHTNESS]}, { "turn_on": [ { @@ -173,6 +179,8 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): 4, SUPPORT_FLASH, 0, + None, + {}, { "turn_on": [ { @@ -189,6 +197,8 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): 4, 0, SUPPORT_FLASH, + None, + {}, { "turn_on": [ { @@ -210,6 +220,8 @@ async def test_get_action_capabilities_features( num_actions, supported_features_reg, supported_features_state, + capabilities_reg, + attributes_state, expected_capabilities, ): """Test we get the expected capabilities from a light action.""" @@ -225,10 +237,13 @@ async def test_get_action_capabilities_features( "5678", device_id=device_entry.id, supported_features=supported_features_reg, + capabilities=capabilities_reg, ).entity_id if set_state: hass.states.async_set( - entity_id, None, {"supported_features": supported_features_state} + entity_id, + None, + {"supported_features": supported_features_state, **attributes_state}, ) actions = await async_get_device_automations(hass, "action", device_entry.id) From 9c5f1b44064bb0d8be9435331809e8241d13461a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 14 May 2021 15:23:16 -0600 Subject: [PATCH 420/852] Fix IQVIA failing to start if any API call fails (#50615) Co-authored-by: Paulus Schoutsen --- homeassistant/components/iqvia/__init__.py | 10 ++++++++-- homeassistant/components/iqvia/sensor.py | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index f8ccf3c7e29..5d13a2373a6 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -9,6 +9,7 @@ from pyiqvia.errors import IQVIAError from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -74,9 +75,14 @@ async def async_setup_entry(hass, entry): update_interval=DEFAULT_SCAN_INTERVAL, update_method=partial(async_get_data_from_api, api_coro), ) - init_data_update_tasks.append(coordinator.async_config_entry_first_refresh()) + init_data_update_tasks.append(coordinator.async_refresh()) - await asyncio.gather(*init_data_update_tasks) + results = await asyncio.gather(*init_data_update_tasks, return_exceptions=True) + if all(isinstance(result, Exception) for result in results): + # The IQVIA API can be selectively flaky, meaning that any number of the setup + # API calls could fail. We only retry integration setup if *all* of the initial + # API calls fail: + raise ConfigEntryNotReady() hass.data[DOMAIN].setdefault(DATA_COORDINATOR, {})[entry.entry_id] = coordinators hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 48ec1cf97b1..b0420a52ee9 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -104,9 +104,12 @@ class ForecastSensor(IQVIAEntity): @callback def update_from_latest_data(self): """Update the sensor.""" - data = self.coordinator.data.get("Location") + if not self.coordinator.data: + return - if not data or not data.get("periods"): + data = self.coordinator.data.get("Location", {}) + + if not data.get("periods"): return indices = [p["Index"] for p in data["periods"]] From bcd8f43e7bd0312f028343ecd4f82be91b80b7cd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 14 May 2021 23:23:29 +0200 Subject: [PATCH 421/852] Update light intents to check supported_color_modes (#50625) --- homeassistant/components/light/intent.py | 29 ++++++++++++++++++++---- homeassistant/helpers/intent.py | 2 +- tests/components/light/test_intent.py | 14 +++++++----- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index be9346cf85b..25475ca0cb5 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -2,7 +2,7 @@ import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -10,10 +10,11 @@ import homeassistant.util.color as color_util from . import ( ATTR_BRIGHTNESS_PCT, ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, DOMAIN, SERVICE_TURN_ON, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, + brightness_supported, + color_supported, ) INTENT_SET = "HassLightSet" @@ -24,6 +25,24 @@ async def async_setup_intents(hass: HomeAssistant) -> None: hass.helpers.intent.async_register(SetIntentHandler()) +def _test_supports_color(state: State) -> None: + """Test if state supports colors.""" + supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) + if not color_supported(supported_color_modes): + raise intent.IntentHandleError( + f"Entity {state.name} does not support changing colors" + ) + + +def _test_supports_brightness(state: State) -> None: + """Test if state supports brightness.""" + supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) + if not brightness_supported(supported_color_modes): + raise intent.IntentHandleError( + f"Entity {state.name} does not support changing brightness" + ) + + class SetIntentHandler(intent.IntentHandler): """Handle set color intents.""" @@ -46,14 +65,14 @@ class SetIntentHandler(intent.IntentHandler): speech_parts = [] if "color" in slots: - intent.async_test_feature(state, SUPPORT_COLOR, "changing colors") + _test_supports_color(state) service_data[ATTR_RGB_COLOR] = slots["color"]["value"] # Use original passed in value of the color because we don't have # human readable names for that internally. speech_parts.append(f"the color {intent_obj.slots['color']['value']}") if "brightness" in slots: - intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness") + _test_supports_brightness(state) service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] speech_parts.append(f"{slots['brightness']['value']}% brightness") diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 96cfbf0e1b5..cfc89240b78 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -119,7 +119,7 @@ def async_match_state( @callback def async_test_feature(state: State, feature: int, feature_name: str) -> None: - """Test is state supports a feature.""" + """Test if state supports a feature.""" if state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & feature == 0: raise IntentHandleError(f"Entity {state.name} does not support {feature_name}") diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py index 4adba921d5e..6a5add41cff 100644 --- a/tests/components/light/test_intent.py +++ b/tests/components/light/test_intent.py @@ -1,7 +1,11 @@ """Tests for the light intents.""" from homeassistant.components import light -from homeassistant.components.light import intent -from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON +from homeassistant.components.light import ( + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_HS, + intent, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON from homeassistant.helpers.intent import IntentHandleError from tests.common import async_mock_service @@ -10,7 +14,7 @@ from tests.common import async_mock_service async def test_intent_set_color(hass): """Test the set color intent.""" hass.states.async_set( - "light.hello_2", "off", {ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR} + "light.hello_2", "off", {ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS]} ) hass.states.async_set("switch.hello", "off") calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) @@ -55,9 +59,7 @@ async def test_intent_set_color_tests_feature(hass): async def test_intent_set_color_and_brightness(hass): """Test the set color intent.""" hass.states.async_set( - "light.hello_2", - "off", - {ATTR_SUPPORTED_FEATURES: (light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS)}, + "light.hello_2", "off", {ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS]} ) hass.states.async_set("switch.hello", "off") calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) From e32dc306e13bcbab272ff0b089b0e1576273d80c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 14 May 2021 23:39:14 +0200 Subject: [PATCH 422/852] Fix oauth2 helper user step typing (#50618) --- homeassistant/helpers/config_entry_oauth2_flow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 74cc56fd9f6..514c31f355b 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -316,7 +316,11 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): """ return self.async_create_entry(title=self.flow_impl.name, data=data) - async_step_user = async_step_pick_implementation + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow start.""" + return await self.async_step_pick_implementation(user_input) @classmethod def async_register_implementation( From 7221b1e09d3cd1726775af83ff76744cc09d6b36 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 06:43:43 +0200 Subject: [PATCH 423/852] Sort effect lists in light groups (#50620) --- homeassistant/components/group/light.py | 4 ++++ tests/components/group/test_light.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 84c218b5d72..4357085ef8a 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -371,6 +371,10 @@ class LightGroup(GroupEntity, light.LightEntity): if all_effect_lists: # Merge all effects from all effect_lists with a union merge. self._effect_list = list(set().union(*all_effect_lists)) + self._effect_list.sort() + if "None" in self._effect_list: + self._effect_list.remove("None") + self._effect_list.insert(0, "None") self._effect = None all_effects = list(_find_state_attributes(on_states, ATTR_EFFECT)) diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index c9b861a46a9..b409786dc07 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -656,6 +656,10 @@ async def test_effect_list(hass): await hass.async_block_till_done() state = hass.states.get("light.light_group") assert set(state.attributes[ATTR_EFFECT_LIST]) == {"None", "Random", "Colorloop"} + # These ensure the output is sorted as expected + assert state.attributes[ATTR_EFFECT_LIST][0] == "None" + assert state.attributes[ATTR_EFFECT_LIST][1] == "Colorloop" + assert state.attributes[ATTR_EFFECT_LIST][2] == "Random" hass.states.async_set( "light.test2", From ed10856cc48239cfc64774cfb6d1872247004e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 15 May 2021 07:49:41 +0300 Subject: [PATCH 424/852] UpCloud API and typing update (#50624) --- .strict-typing | 1 + homeassistant/components/upcloud/__init__.py | 25 +++++++++++-------- .../components/upcloud/binary_sensor.py | 9 ++++++- .../components/upcloud/config_flow.py | 25 +++++++++++++++---- .../components/upcloud/manifest.json | 2 +- homeassistant/components/upcloud/switch.py | 15 ++++++++--- homeassistant/helpers/entity.py | 2 +- mypy.ini | 14 ++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/mypy_config.py | 1 - 11 files changed, 71 insertions(+), 27 deletions(-) diff --git a/.strict-typing b/.strict-typing index 77e833bca7a..87bef3033c4 100644 --- a/.strict-typing +++ b/.strict-typing @@ -46,6 +46,7 @@ homeassistant.components.switch.* homeassistant.components.synology_dsm.* homeassistant.components.systemmonitor.* homeassistant.components.tts.* +homeassistant.components.upcloud.* homeassistant.components.vacuum.* homeassistant.components.water_heater.* homeassistant.components.weather.* diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 21c99416673..a2bd2e6e88c 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import dataclasses from datetime import timedelta import logging -from typing import Dict +from typing import Any, Dict import requests.exceptions import upcloud_api @@ -28,6 +28,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -117,7 +118,7 @@ class UpCloudHassData: scan_interval_migrations: dict[str, int] = dataclasses.field(default_factory=dict) -async def async_setup(hass: HomeAssistant, config) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up UpCloud component.""" domain_config = config.get(DOMAIN) if not domain_config: @@ -228,7 +229,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload the config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( config_entry, CONFIG_ENTRY_DOMAINS @@ -242,7 +243,11 @@ async def async_unload_entry(hass, config_entry): class UpCloudServerEntity(CoordinatorEntity): """Entity class for UpCloud servers.""" - def __init__(self, coordinator, uuid): + def __init__( + self, + coordinator: DataUpdateCoordinator[dict[str, upcloud_api.Server]], + uuid: str, + ) -> None: """Initialize the UpCloud server entity.""" super().__init__(coordinator) self.uuid = uuid @@ -257,7 +262,7 @@ class UpCloudServerEntity(CoordinatorEntity): return self.uuid @property - def name(self): + def name(self) -> str: """Return the name of the component.""" try: return DEFAULT_COMPONENT_NAME.format(self._server.title) @@ -265,12 +270,12 @@ class UpCloudServerEntity(CoordinatorEntity): return DEFAULT_COMPONENT_NAME.format(self.uuid) @property - def icon(self): + def icon(self) -> str: """Return the icon of this server.""" return "mdi:server" if self.is_on else "mdi:server-off" @property - def state(self): + def state(self) -> str | None: """Return state of the server.""" try: return STATE_MAP.get(self._server.state, self._server.state) @@ -278,17 +283,17 @@ class UpCloudServerEntity(CoordinatorEntity): return None @property - def is_on(self): + def is_on(self) -> bool: """Return true if the server is on.""" return self.state == STATE_ON @property - def device_class(self): + def device_class(self) -> str: """Return the class of this server.""" return DEFAULT_COMPONENT_DEVICE_CLASS @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the UpCloud server.""" return { x: getattr(self._server, x, None) diff --git a/homeassistant/components/upcloud/binary_sensor.py b/homeassistant/components/upcloud/binary_sensor.py index d64dc0f7ea9..de55a577610 100644 --- a/homeassistant/components/upcloud/binary_sensor.py +++ b/homeassistant/components/upcloud/binary_sensor.py @@ -3,8 +3,11 @@ import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_USERNAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import CONF_SERVERS, DATA_UPCLOUD, UpCloudServerEntity @@ -13,7 +16,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the UpCloud server binary sensor.""" coordinator = hass.data[DATA_UPCLOUD].coordinators[config_entry.data[CONF_USERNAME]] entities = [UpCloudBinarySensor(coordinator, uuid) for uuid in coordinator.data] diff --git a/homeassistant/components/upcloud/config_flow.py b/homeassistant/components/upcloud/config_flow.py index 62260ebc8ca..3ad5976df1f 100644 --- a/homeassistant/components/upcloud/config_flow.py +++ b/homeassistant/components/upcloud/config_flow.py @@ -1,6 +1,9 @@ """Config flow for UpCloud.""" +from __future__ import annotations + import logging +from typing import Any import requests.exceptions import upcloud_api @@ -9,6 +12,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_SCAN_INTERVAL, DOMAIN @@ -23,7 +27,9 @@ class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): username: str password: str - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle user initiated flow.""" if user_input is None: return self._async_show_form(step_id="user") @@ -51,7 +57,7 @@ class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input) - async def async_step_import(self, user_input=None): + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Handle import initiated flow.""" await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() @@ -59,7 +65,12 @@ class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(user_input=user_input) @callback - def _async_show_form(self, step_id, user_input=None, errors=None): + def _async_show_form( + self, + step_id: str, + user_input: dict[str, Any] | None = None, + errors: dict[str, str] | None = None, + ) -> FlowResult: """Show our form.""" if user_input is None: user_input = {} @@ -80,7 +91,9 @@ class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> UpCloudOptionsFlow: """Get options flow.""" return UpCloudOptionsFlow(config_entry) @@ -92,7 +105,9 @@ class UpCloudOptionsFlow(config_entries.OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle options flow.""" if user_input is not None: diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index 064cfa224e1..a9e0f74462e 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -3,7 +3,7 @@ "name": "UpCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upcloud", - "requirements": ["upcloud-api==1.0.1"], + "requirements": ["upcloud-api==2.0.0"], "codeowners": ["@scop"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/upcloud/switch.py b/homeassistant/components/upcloud/switch.py index a9e4ab56dd1..91676ab1d5a 100644 --- a/homeassistant/components/upcloud/switch.py +++ b/homeassistant/components/upcloud/switch.py @@ -1,11 +1,16 @@ """Support for interacting with UpCloud servers.""" +from typing import Any + import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_USERNAME, STATE_OFF +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import CONF_SERVERS, DATA_UPCLOUD, SIGNAL_UPDATE_UPCLOUD, UpCloudServerEntity @@ -14,7 +19,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the UpCloud server switch.""" coordinator = hass.data[DATA_UPCLOUD].coordinators[config_entry.data[CONF_USERNAME]] entities = [UpCloudSwitch(coordinator, uuid) for uuid in coordinator.data] @@ -24,13 +33,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class UpCloudSwitch(UpCloudServerEntity, SwitchEntity): """Representation of an UpCloud server switch.""" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Start the server.""" if self.state == STATE_OFF: self._server.start() dispatcher_send(self.hass, SIGNAL_UPDATE_UPCLOUD) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Stop the server.""" if self.is_on: self._server.stop() diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a9843387fd9..2e2c1b3b3f9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -749,7 +749,7 @@ class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" @property - def state(self) -> str: + def state(self) -> str | None: """Return the state.""" return STATE_ON if self.is_on else STATE_OFF diff --git a/mypy.ini b/mypy.ini index 3371658dc92..8bc55b0d82e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -517,6 +517,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.upcloud.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.vacuum.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1220,9 +1231,6 @@ ignore_errors = true [mypy-homeassistant.components.unifi.*] ignore_errors = true -[mypy-homeassistant.components.upcloud.*] -ignore_errors = true - [mypy-homeassistant.components.updater.*] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index 494dac6d1e3..092f1ece333 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2286,7 +2286,7 @@ unifiled==0.11 upb_lib==0.4.12 # homeassistant.components.upcloud -upcloud-api==1.0.1 +upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6164462ceca..ba1b5c40889 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1228,7 +1228,7 @@ twinkly-client==0.0.2 upb_lib==0.4.12 # homeassistant.components.upcloud -upcloud-api==1.0.1 +upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 73752f4c51a..72bb8879312 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -219,7 +219,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.tradfri.*", "homeassistant.components.tuya.*", "homeassistant.components.unifi.*", - "homeassistant.components.upcloud.*", "homeassistant.components.updater.*", "homeassistant.components.upnp.*", "homeassistant.components.velbus.*", From 2d5f5bfa9f4a3785840d93bc653ce81d2b9b88e3 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 15 May 2021 01:07:17 -0400 Subject: [PATCH 425/852] Add targets and selectors for services (P-R) (#50628) --- .../persistent_notification/services.yaml | 29 +++- homeassistant/components/person/services.yaml | 1 + .../components/pilight/services.yaml | 5 + homeassistant/components/ping/services.yaml | 1 + .../components/qvr_pro/services.yaml | 10 ++ homeassistant/components/rachio/services.yaml | 16 +- .../components/rainbird/services.yaml | 9 ++ .../components/rainmachine/services.yaml | 149 +++++++++++++----- .../components/recorder/services.yaml | 4 +- .../remember_the_milk/services.yaml | 15 +- homeassistant/components/rest/services.yaml | 1 + homeassistant/components/rflink/services.yaml | 9 ++ homeassistant/components/rfxtrx/services.yaml | 5 + homeassistant/components/ring/services.yaml | 1 + homeassistant/components/risco/services.yaml | 18 ++- homeassistant/components/roku/services.yaml | 12 +- homeassistant/components/roon/services.yaml | 12 +- .../components/route53/services.yaml | 1 + .../components/rpi_gpio/services.yaml | 1 + 19 files changed, 234 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/persistent_notification/services.yaml b/homeassistant/components/persistent_notification/services.yaml index 7917a06a3d7..5695a3c3b82 100644 --- a/homeassistant/components/persistent_notification/services.yaml +++ b/homeassistant/components/persistent_notification/services.yaml @@ -1,26 +1,47 @@ create: + name: Create description: Show a notification in the frontend. fields: message: + name: Message description: Message body of the notification. [Templates accepted] + required: true example: Please check your configuration.yaml. + selector: + text: title: - description: Optional title for your notification. [Optional, Templates accepted] + name: Title + description: Optional title for your notification. [Templates accepted] example: Test notification + selector: + text: notification_id: - description: Target ID of the notification, will replace a notification with the same ID. [Optional] + name: Notification ID + description: Target ID of the notification, will replace a notification with the same ID. example: 1234 + selector: + text: dismiss: + name: Dismiss description: Remove a notification from the frontend. fields: notification_id: - description: Target ID of the notification, which should be removed. [Required] + name: Notification ID + description: Target ID of the notification, which should be removed. + required: true example: 1234 + selector: + text: mark_read: + name: Mark read description: Mark a notification read. fields: notification_id: - description: Target ID of the notification, which should be mark read. [Required] + name: Notification ID + description: Target ID of the notification, which should be mark read. + required: true example: 1234 + selector: + text: diff --git a/homeassistant/components/person/services.yaml b/homeassistant/components/person/services.yaml index 0af934f56b8..265c6049563 100644 --- a/homeassistant/components/person/services.yaml +++ b/homeassistant/components/person/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload the person configuration. diff --git a/homeassistant/components/pilight/services.yaml b/homeassistant/components/pilight/services.yaml index 9faa8908efb..6dc052043bf 100644 --- a/homeassistant/components/pilight/services.yaml +++ b/homeassistant/components/pilight/services.yaml @@ -1,6 +1,11 @@ send: + name: Send description: Send RF code to Pilight device fields: protocol: + name: Protocol description: "Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports" + required: true example: "lirc" + selector: + object: diff --git a/homeassistant/components/ping/services.yaml b/homeassistant/components/ping/services.yaml index e2da0c28627..1f7e523e685 100644 --- a/homeassistant/components/ping/services.yaml +++ b/homeassistant/components/ping/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all ping entities. diff --git a/homeassistant/components/qvr_pro/services.yaml b/homeassistant/components/qvr_pro/services.yaml index 0f305d1fa8d..edb879c784a 100644 --- a/homeassistant/components/qvr_pro/services.yaml +++ b/homeassistant/components/qvr_pro/services.yaml @@ -1,13 +1,23 @@ start_record: + name: Start record description: Start QVR Pro recording on specified channel. fields: guid: + name: GUID description: GUID of the channel to start recording. + required: true example: "245EBE933C0A597EBE865C0A245E0002" + selector: + text: stop_record: + name: Stop record description: Stop QVR Pro recording on specified channel. fields: guid: + name: GUID description: GUID of the channel to stop recording. + required: true example: "245EBE933C0A597EBE865C0A245E0002" + selector: + text: diff --git a/homeassistant/components/rachio/services.yaml b/homeassistant/components/rachio/services.yaml index e40ccc6df29..93f63fcb9c3 100644 --- a/homeassistant/components/rachio/services.yaml +++ b/homeassistant/components/rachio/services.yaml @@ -1,5 +1,5 @@ set_zone_moisture_percent: - name: Set Zone Moisture Percent + name: Set zone moisture percent description: Set the moisture percentage of a zone or list of zones. target: entity: @@ -8,7 +8,7 @@ set_zone_moisture_percent: fields: percent: name: Percent - description: Set the desired zone moisture percentage from 0 to 100. + description: Set the desired zone moisture percentage. required: true example: 50 selector: @@ -17,9 +17,8 @@ set_zone_moisture_percent: max: 100 mode: slider unit_of_measurement: "%" - step: 1 start_multiple_zone_schedule: - name: Start Multiple Zones + name: Start multiple zones description: Create a custom schedule of zones and runtimes. Note that all zones should be on the same controller to avoid issues. target: entity: @@ -34,7 +33,7 @@ start_multiple_zone_schedule: selector: object: pause_watering: - name: Pause Watering + name: Pause watering description: Pause any currently running zones or schedules. fields: devices: @@ -45,8 +44,8 @@ pause_watering: text: duration: name: Duration - description: The number of minutes to pause running schedules. Accepts 1-60. Default is 60 minutes if not provided. - example: 60 + description: The time to pause running schedules. + example: 30 default: 60 selector: number: @@ -54,9 +53,8 @@ pause_watering: max: 60 mode: slider unit_of_measurement: "minutes" - step: 1 resume_watering: - name: Resume Watering + name: Resume watering description: Resume any paused zone runs or schedules. fields: devices: diff --git a/homeassistant/components/rainbird/services.yaml b/homeassistant/components/rainbird/services.yaml index ed1ec8b62df..795fe5343d2 100644 --- a/homeassistant/components/rainbird/services.yaml +++ b/homeassistant/components/rainbird/services.yaml @@ -1,9 +1,18 @@ start_irrigation: + name: Start irrigation description: Start the irrigation fields: entity_id: + name: Entity description: Name of a single irrigation to turn on + required: true example: "switch.sprinkler_1" + selector: + entity: + integration: rainbird + domain: switch duration: + name: Duration description: Duration for this sprinkler to be turned on + required: true example: 1 diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index a73dc5c899d..fa270692142 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -1,97 +1,174 @@ # Describes the format for available RainMachine services disable_program: + name: Disable program description: Disable a program. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 program_id: + name: Program ID description: The program to disable. + required: true example: 3 + selector: + number: + min: 1 + max: 255 disable_zone: + name: Disable zone description: Disable a zone. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 zone_id: + name: Zone ID description: The zone to disable. + required: true example: 3 + selector: + number: + min: 1 + max: 255 enable_program: + name: Enable program description: Enable a program. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 program_id: + name: Program ID description: The program to enable. + required: true example: 3 + selector: + number: + min: 1 + max: 255 enable_zone: + name: Enable zone description: Enable a zone. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 zone_id: + name: Zone ID description: The zone to enable. + required: true example: 3 + selector: + number: + min: 1 + max: 255 pause_watering: + name: Pause watering description: Pause all watering for a number of seconds. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 seconds: - description: The number of seconds to pause. + name: Seconds + description: The time to pause. + required: true example: 30 + selector: + number: + min: 1 + max: 86400 + unit_of_measurement: seconds start_program: + name: Start program description: Start a program. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 program_id: + name: Program ID description: The program to start. + required: true example: 3 + selector: + number: + min: 1 + max: 255 start_zone: + name: Start zone description: Start a zone for a set number of seconds. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 zone_id: + name: Zone ID description: The zone to start. + required: true example: 3 + selector: + number: + min: 1 + max: 255 zone_run_time: + name: Zone run time description: The number of seconds to run the zone. example: 120 + default: 600 stop_all: + name: Stop all description: Stop all watering activities. - fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 + target: + entity: + integration: rainmachine + domain: switch stop_program: + name: Stop program description: Stop a program. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 program_id: + name: Program ID description: The program to stop. + required: true example: 3 + selector: + number: + min: 1 + max: 255 stop_zone: + name: Stop zone description: Stop a zone. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 zone_id: + name: Zone ID description: The zone to stop. + required: true example: 3 + selector: + number: + min: 1 + max: 255 unpause_watering: + name: Unpause watering description: Unpause all watering. - fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 + target: + entity: + integration: rainmachine + domain: switch diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index 2c4f35b5e7a..dcd8477d4bd 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -34,7 +34,9 @@ purge: boolean: disable: + name: Disable description: Stop the recording of events and state changes -enabled: +enable: + name: Enable description: Start the recording of events and state changes diff --git a/homeassistant/components/remember_the_milk/services.yaml b/homeassistant/components/remember_the_milk/services.yaml index 448b35a3a04..1458075fbd5 100644 --- a/homeassistant/components/remember_the_milk/services.yaml +++ b/homeassistant/components/remember_the_milk/services.yaml @@ -1,21 +1,34 @@ # Describes the format for available Remember The Milk services create_task: + name: Create task description: >- Create (or update) a new task in your Remember The Milk account. If you want to update a task later on, you have to set an "id" when creating the task. Note: Updating a tasks does not support the smart syntax. fields: name: + name: Name description: name of the new task, you can use the smart syntax here + required: true example: "do this ^today #from_hass" + selector: + text: id: - description: (optional) identifier for the task you're creating, can be used to update or complete the task later on + name: ID + description: Identifier for the task you're creating, can be used to update or complete the task later on example: myid + selector: + text: complete_task: + name: Complete task description: Complete a tasks that was privously created. fields: id: + name: ID description: identifier that was defined when creating the task + required: true example: myid + selector: + text: diff --git a/homeassistant/components/rest/services.yaml b/homeassistant/components/rest/services.yaml index 7e324670134..9ba509b63f6 100644 --- a/homeassistant/components/rest/services.yaml +++ b/homeassistant/components/rest/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all rest entities and notify services diff --git a/homeassistant/components/rflink/services.yaml b/homeassistant/components/rflink/services.yaml index 3a44d04f75d..8e233bc7aac 100644 --- a/homeassistant/components/rflink/services.yaml +++ b/homeassistant/components/rflink/services.yaml @@ -1,9 +1,18 @@ send_command: + name: Send command description: Send device command through RFLink. fields: command: + name: Command description: The command to be sent. + required: true example: "on" + selector: + text: device_id: + name: Device ID description: RFLink device ID. + required: true example: newkaku_0000c6c2_1 + selector: + text: diff --git a/homeassistant/components/rfxtrx/services.yaml b/homeassistant/components/rfxtrx/services.yaml index 088082758b6..43695554ed0 100644 --- a/homeassistant/components/rfxtrx/services.yaml +++ b/homeassistant/components/rfxtrx/services.yaml @@ -1,6 +1,11 @@ send: + name: Send description: Sends a raw event on radio. fields: event: + name: Event description: A hexadecimal string to send. + required: true example: "0b11009e00e6116202020070" + selector: + text: diff --git a/homeassistant/components/ring/services.yaml b/homeassistant/components/ring/services.yaml index bcc9b2f7ff4..c648f02139b 100644 --- a/homeassistant/components/ring/services.yaml +++ b/homeassistant/components/ring/services.yaml @@ -1,2 +1,3 @@ update: + name: Update description: Updates the data we have for all your ring devices diff --git a/homeassistant/components/risco/services.yaml b/homeassistant/components/risco/services.yaml index 8b6c8c06f01..c271df7b462 100644 --- a/homeassistant/components/risco/services.yaml +++ b/homeassistant/components/risco/services.yaml @@ -1,15 +1,17 @@ # Describes the format for available Risco services bypass_zone: + name: Bypass zone description: Bypass a Risco Zone - fields: - entity_id: - description: Entity ID of the zone to bypass - example: "binary_sensor.living_room_motion" + target: + entity: + integration: risco + domain: binary_sensor unbypass_zone: + name: Unbypass zone description: Unbypass a Risco Zone - fields: - entity_id: - description: Entity ID of the zone to unbypass - example: "binary_sensor.living_room_motion" + target: + entity: + integration: risco + domain: binary_sensor diff --git a/homeassistant/components/roku/services.yaml b/homeassistant/components/roku/services.yaml index 1d215306157..16fd51ea95b 100644 --- a/homeassistant/components/roku/services.yaml +++ b/homeassistant/components/roku/services.yaml @@ -1,9 +1,15 @@ search: + name: Search description: Emulates opening the search screen and entering the search keyword. + target: + entity: + integration: roku + domain: media_player fields: - entity_id: - description: The entities to search on. - example: "media_player.roku" keyword: + name: Keyword description: The keyword to search for. + required: true example: "Space Jam" + selector: + text: diff --git a/homeassistant/components/roon/services.yaml b/homeassistant/components/roon/services.yaml index 6622d9b4c31..0697911c07c 100644 --- a/homeassistant/components/roon/services.yaml +++ b/homeassistant/components/roon/services.yaml @@ -1,9 +1,15 @@ transfer: + name: Transfer description: Transfer playback from one player to another. + target: + entity: + integration: roon + domain: media_player fields: - entity_id: - description: id of the source player. - example: "media_player.bedroom" transfer_id: + name: Transfer ID description: id of the destination player. + required: true example: "media_player.study" + selector: + text: diff --git a/homeassistant/components/route53/services.yaml b/homeassistant/components/route53/services.yaml index 3ca109fcb36..4936a499764 100644 --- a/homeassistant/components/route53/services.yaml +++ b/homeassistant/components/route53/services.yaml @@ -1,2 +1,3 @@ update_records: + name: Update records description: Trigger update of records. diff --git a/homeassistant/components/rpi_gpio/services.yaml b/homeassistant/components/rpi_gpio/services.yaml index d0564941cdb..1858c5a9fa2 100644 --- a/homeassistant/components/rpi_gpio/services.yaml +++ b/homeassistant/components/rpi_gpio/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all rpi_gpio entities. From d37a3cded059d0cde95eb5826ebf42a0e03f97dd Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 15 May 2021 01:30:41 -0400 Subject: [PATCH 426/852] Add targets and selectors for services (S-T) (#50629) --- .../components/sabnzbd/services.yaml | 6 + .../components/sensibo/services.yaml | 10 + .../components/shopping_list/services.yaml | 6 + .../components/simplisafe/services.yaml | 76 +++ homeassistant/components/smtp/services.yaml | 1 + .../components/snapcast/services.yaml | 56 ++- homeassistant/components/snips/services.yaml | 51 +- .../components/songpal/services.yaml | 17 +- .../components/soundtouch/services.yaml | 62 ++- .../components/speedtestdotnet/services.yaml | 1 + .../components/squeezebox/services.yaml | 52 +- .../components/starline/services.yaml | 21 +- .../components/statistics/services.yaml | 1 + .../components/streamlabswater/services.yaml | 9 +- .../components/surepetcare/services.yaml | 16 +- homeassistant/components/switch/services.yaml | 3 + .../components/switcher_kis/services.yaml | 29 +- homeassistant/components/tado/services.yaml | 59 ++- .../components/telegram/services.yaml | 1 + .../components/telegram_bot/services.yaml | 458 ++++++++++++++++-- .../components/template/services.yaml | 1 + .../components/todoist/services.yaml | 69 ++- homeassistant/components/toon/services.yaml | 2 +- .../components/transmission/services.yaml | 39 +- homeassistant/components/trend/services.yaml | 1 + homeassistant/components/tuya/services.yaml | 2 + 26 files changed, 944 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/sabnzbd/services.yaml b/homeassistant/components/sabnzbd/services.yaml index 654cb50fa1e..38f68bfe5dd 100644 --- a/homeassistant/components/sabnzbd/services.yaml +++ b/homeassistant/components/sabnzbd/services.yaml @@ -1,11 +1,17 @@ pause: + name: Pause description: Pauses downloads. resume: + name: Resume description: Resumes downloads. set_speed: + name: Set speed description: Sets the download speed limit. fields: speed: + name: Speed description: Speed limit. If specified as a number with no units, will be interpreted as a percent. If units are provided (e.g., 500K) will be interpreted absolutely. example: 100 default: 100 + selector: + text: diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml index f981ef7fd32..23a53313dc5 100644 --- a/homeassistant/components/sensibo/services.yaml +++ b/homeassistant/components/sensibo/services.yaml @@ -1,9 +1,19 @@ assume_state: + name: Assume state description: Set Sensibo device to external state. fields: entity_id: + name: Entity description: Name(s) of entities to change. example: "climate.kitchen" + selector: + entity: + integration: sensibo + domain: climate state: + name: State description: State to set. + required: true example: "idle" + selector: + text: diff --git a/homeassistant/components/shopping_list/services.yaml b/homeassistant/components/shopping_list/services.yaml index 73540210232..9f5437701ed 100644 --- a/homeassistant/components/shopping_list/services.yaml +++ b/homeassistant/components/shopping_list/services.yaml @@ -23,14 +23,20 @@ complete_item: text: incomplete_item: + name: Incomplete item description: Marks an item as incomplete in the shopping list. fields: name: description: The name of the item to mark as incomplete. example: Beer + required: true + selector: + text: complete_all: + name: Complete call description: Marks all items as completed in the shopping list. It does not remove the items. incomplete_all: + name: Incomplete all description: Marks all items as incomplete in the shopping list. diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index e52af4f2665..865ba4c8b2c 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -1,52 +1,128 @@ # Describes the format for available SimpliSafe services remove_pin: + name: Remove PIN description: Remove a PIN by its label or value. fields: system_id: + name: System ID description: The SimpliSafe system ID to affect. + required: true example: 123987 + selector: + text: label_or_pin: + name: Label/PIN description: The label/value to remove. + required: true example: Test PIN + selector: + text: set_pin: + name: Set PIN description: Set/update a PIN fields: system_id: + name: System ID description: The SimpliSafe system ID to affect + required: true example: 123987 + selector: + text: label: + name: Label description: The label of the PIN + required: true example: Test PIN + selector: + text: pin: + name: PIN description: The value of the PIN + required: true example: 1256 + selector: + text: set_system_properties: + name: Set system properties description: Set one or more system properties fields: alarm_duration: + name: Alarm duration description: The length of a triggered alarm example: 300 + selector: + number: + min: 30 + max: 480 + unit_of_measurement: seconds alarm_volume: + name: Alarm volume description: The volume level of a triggered alarm example: 2 + selector: + select: + options: + - 'low' + - 'medium' + - 'high' + - 'off' chime_volume: + name: Chime volume description: The volume level of the door chime example: 2 + selector: + select: + options: + - 'low' + - 'medium' + - 'high' + - 'off' entry_delay_away: + name: Entry delay away description: How long to delay when entering while "away" example: 45 + selector: + number: + min: 30 + max: 255 entry_delay_home: + name: Entry delay home description: How long to delay when entering while "home" example: 45 + selector: + number: + min: 0 + max: 255 exit_delay_away: + name: Exit delay away description: How long to delay when exiting while "away" example: 45 + selector: + number: + min: 45 + max: 255 exit_delay_home: + name: Exit delay home description: How long to delay when exiting while "home" example: 45 + selector: + number: + min: 0 + max: 255 light: + name: Light description: Whether the armed light should be visible example: true + selector: + boolean: voice_prompt_volume: + name: Voice prompt volume description: The volume level of the voice prompt example: 2 + selector: + select: + options: + - 'low' + - 'medium' + - 'high' + - 'off' diff --git a/homeassistant/components/smtp/services.yaml b/homeassistant/components/smtp/services.yaml index 77ff7d22adf..c4380a4fc62 100644 --- a/homeassistant/components/smtp/services.yaml +++ b/homeassistant/components/smtp/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload smtp notify services. diff --git a/homeassistant/components/snapcast/services.yaml b/homeassistant/components/snapcast/services.yaml index 3b83aa3774d..79839c33df2 100644 --- a/homeassistant/components/snapcast/services.yaml +++ b/homeassistant/components/snapcast/services.yaml @@ -1,39 +1,63 @@ join: + name: Join description: Group players together. fields: master: + name: Master description: Entity ID of the player to synchronize to. + required: true example: "media_player.living_room" + selector: + entity: + integration: snapcast + domain: media_player entity_id: - description: Entity ID of the players to join to the "master". - example: "media_player.bedroom" + name: Entity + description: The players to join to the "master". + selector: + target: + entity: + integration: snapcast + domain: media_player unjoin: + name: Unjoin description: Unjoin the player from a group. - fields: - entity_id: - description: Entity ID of the player to unjoin. - example: "media_player.living_room" + target: + entity: + integration: snapcast + domain: media_player snapshot: + name: Snapshot description: Take a snapshot of the media player. - fields: - entity_id: - description: Name(s) of entities that will be snapshotted. Platform dependent. - example: "media_player.living_room" + target: + entity: + integration: snapcast + domain: media_player restore: + name: Restore description: Restore a snapshot of the media player. - fields: - entity_id: - description: Name(s) of entities that will be restored. Platform dependent. - example: "media_player.living_room" + target: + entity: + integration: snapcast + domain: media_player set_latency: + name: Set latency description: Set client set_latency + target: + entity: + integration: snapcast + domain: media_player fields: - entity_id: - description: Name of entities that will have adjusted latency latency: + name: Latency description: Latency in master + required: true example: 14 + selector: + number: + min: 1 + max: 1000 diff --git a/homeassistant/components/snips/services.yaml b/homeassistant/components/snips/services.yaml index f06b94b9eaa..f4a36b6e781 100644 --- a/homeassistant/components/snips/services.yaml +++ b/homeassistant/components/snips/services.yaml @@ -1,42 +1,85 @@ feedback_off: + name: Feedback off description: Turns feedback sounds off. fields: site_id: - description: Site to turn sounds on, defaults to all sites (optional) + name: Site ID + description: Site to turn sounds on, defaults to all sites. example: bedroom + default: default + selector: + text: feedback_on: + name: Feedback on description: Turns feedback sounds on. fields: site_id: - description: Site to turn sounds on, defaults to all sites (optional) + name: Site ID + description: Site to turn sounds on, defaults to all sites. example: bedroom + default: default + selector: + text: say: + name: Say description: Send a TTS message to Snips. fields: custom_data: + name: Custom data description: custom data that will be included with all messages in this session example: user=UserName + default: '' + selector: + text: site_id: - description: Site to use to start session, defaults to default (optional) + name: Site ID + description: Site to use to start session, defaults to default. example: bedroom + default: default + selector: + text: text: + name: Text description: Text to say. + required: true example: My name is snips + selector: + text: say_action: + name: Say action description: Send a TTS message to Snips to listen for a response. fields: can_be_enqueued: + name: Can be enqueued description: If True, session waits for an open session to end, if False session is dropped if one is running example: true + default: true + selector: + boolean: custom_data: + name: Custom data description: custom data that will be included with all messages in this session example: user=UserName + default: '' + selector: + text: intent_filter: + name: Intent filter description: Optional Array of Strings - A list of intents names to restrict the NLU resolution to on the first query. example: "turnOnLights, turnOffLights" + selector: + object: site_id: - description: Site to use to start session, defaults to default (optional) + name: Site ID + description: Site to use to start session, defaults to default. example: bedroom + default: default + selector: + text: text: + name: Text description: Text to say + required: true example: My name is snips + selector: + text: diff --git a/homeassistant/components/songpal/services.yaml b/homeassistant/components/songpal/services.yaml index e08ae2098fe..93485ce4788 100644 --- a/homeassistant/components/songpal/services.yaml +++ b/homeassistant/components/songpal/services.yaml @@ -1,13 +1,22 @@ set_sound_setting: + name: Set sound setting description: Change sound setting. - + target: + entity: + integration: songpal + domain: media_player fields: - entity_id: - description: Target device. - example: "media_player.my_soundbar" name: + name: Name description: Name of the setting. + required: true example: "nightMode" + selector: + text: value: + name: Value description: Value to set. + required: true example: "on" + selector: + text: diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml index 79fd1d41665..0de37c6daa2 100644 --- a/homeassistant/components/soundtouch/services.yaml +++ b/homeassistant/components/soundtouch/services.yaml @@ -1,36 +1,78 @@ play_everywhere: + name: Play everywhere description: Play on all Bose Soundtouch devices. fields: master: + name: Master description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices - example: "media_player.soundtouch_home" + required: true + selector: + entity: + integration: soundtouch + domain: media_player create_zone: - description: Create a Sountouch multi-room zone. + name: Create zone + description: Create a Soundtouch multi-room zone. fields: master: + name: Master description: Name of the master entity that will coordinate the multi-room zone. Platform dependent. - example: "media_player.soundtouch_home" + required: true + selector: + entity: + integration: soundtouch + domain: media_player slaves: + name: Slaves description: Name of slaves entities to add to the new zone. - example: "media_player.soundtouch_bedroom" + required: true + selector: + target: + entity: + integration: soundtouch + domain: media_player add_zone_slave: - description: Add a slave to a Sountouch multi-room zone. + name: Add zone slave + description: Add a slave to a Soundtouch multi-room zone. fields: master: + name: Master description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. - example: "media_player.soundtouch_home" + required: true + selector: + entity: + integration: soundtouch + domain: media_player slaves: + name: Slaves description: Name of slaves entities to add to the existing zone. - example: "media_player.soundtouch_bedroom" + required: true + selector: + target: + entity: + integration: soundtouch + domain: media_player remove_zone_slave: - description: Remove a slave from the Sounttouch multi-room zone. + name: Remove zone slave + description: Remove a slave from the Soundtouch multi-room zone. fields: master: + name: Master description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. - example: "media_player.soundtouch_home" + required: true + selector: + entity: + integration: soundtouch + domain: media_player slaves: + name: Slaves description: Name of slaves entities to remove from the existing zone. - example: "media_player.soundtouch_bedroom" + required: true + selector: + target: + entity: + integration: soundtouch + domain: media_player diff --git a/homeassistant/components/speedtestdotnet/services.yaml b/homeassistant/components/speedtestdotnet/services.yaml index 489261ba77a..fdc6be746f8 100644 --- a/homeassistant/components/speedtestdotnet/services.yaml +++ b/homeassistant/components/speedtestdotnet/services.yaml @@ -1,2 +1,3 @@ speedtest: + name: Speedtest description: Immediately execute a speed test with Speedtest.net diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index ef69ea67dbf..293b89fe35a 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -1,42 +1,70 @@ call_method: + name: Call method description: Call a custom Squeezebox JSONRPC API. + target: + entity: + integration: squeezebox + domain: media_player fields: - entity_id: - description: Name(s) of the Squeezebox entities where to run the API method. - example: "media_player.squeezebox_radio" command: + name: Command description: Command to pass to Logitech Media Server (p0 in the CLI documentation). + required: true example: "playlist" + selector: + text: parameters: + name: Parameters description: > Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). example: '["loadtracks", "album.titlesearch=Revolver"]' + advanced: true + selector: + object: call_query: + name: Call query description: > Call a custom Squeezebox JSONRPC API. Result will be stored in 'query_result' attribute of the Squeezebox entity. + target: + entity: + integration: squeezebox + domain: media_player fields: - entity_id: - description: Name(s) of the Squeezebox entities where to run the API method. - example: 'media_player.squeezebox_radio' command: + name: Command description: Command to pass to Logitech Media Server (p0 in the CLI documentation). + required: true example: 'albums' + selector: + text: parameters: + name: Parameters description: > Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). example: '["0", "20", "search:Revolver"]' + advanced: true + selector: + object: sync: + name: Sync description: > Add another player to this player's sync group. If the other player is already in a sync group, it will leave it. + target: + entity: + integration: squeezebox + domain: media_player fields: - entity_id: - description: Name of the Squeezebox entity where to run the API method. - example: "media_player.bedroom" other_player: + name: Other player description: Name of the other Squeezebox player to link. + required: true example: "media_player.living_room" + selector: + text: unsync: + name: Unsync description: Remove this player from its sync group. - fields: - entity_id: - description: Name of the Squeezebox entity to unsync. + target: + entity: + integration: squeezebox + domain: media_player diff --git a/homeassistant/components/starline/services.yaml b/homeassistant/components/starline/services.yaml index 4eab51b94d7..970010ffea0 100644 --- a/homeassistant/components/starline/services.yaml +++ b/homeassistant/components/starline/services.yaml @@ -1,17 +1,34 @@ update_state: + name: Update state description: > Fetch the last state of the devices from the StarLine server. set_scan_interval: + name: Set scan interval description: > Set update frequency. fields: scan_interval: - description: Update frequency (in seconds). + name: Scan interval + description: Update frequency. example: 180 + selector: + number: + min: 10 + max: 86400 + step: 5 + unit_of_measurement: seconds set_scan_obd_interval: + name: Set scan OBD interval description: > Set OBD info update frequency. fields: scan_interval: - description: Update frequency (in seconds). + name: Scan interval + description: Update frequency. example: 10800 + selector: + number: + min: 180 + max: 86400 + step: 5 + unit_of_measurement: seconds diff --git a/homeassistant/components/statistics/services.yaml b/homeassistant/components/statistics/services.yaml index 608e2991334..8c2c8f8464a 100644 --- a/homeassistant/components/statistics/services.yaml +++ b/homeassistant/components/statistics/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all statistics entities. diff --git a/homeassistant/components/streamlabswater/services.yaml b/homeassistant/components/streamlabswater/services.yaml index 63ab2be9759..3a483fec264 100644 --- a/homeassistant/components/streamlabswater/services.yaml +++ b/homeassistant/components/streamlabswater/services.yaml @@ -1,6 +1,13 @@ set_away_mode: + name: Set away mode description: "Set the home/away mode for a Streamlabs Water Monitor." fields: away_mode: + name: Away mode description: home or away - example: "home" + required: true + selector: + select: + options: + - 'away' + - 'home' diff --git a/homeassistant/components/surepetcare/services.yaml b/homeassistant/components/surepetcare/services.yaml index 145256efe86..6542cfce188 100644 --- a/homeassistant/components/surepetcare/services.yaml +++ b/homeassistant/components/surepetcare/services.yaml @@ -1,9 +1,23 @@ set_lock_state: + name: Set lock state description: Sets lock state fields: flap_id: + name: Flap ID description: Flap ID to lock/unlock + required: true example: "123456" + selector: + text: lock_state: - description: New lock state - unlocked, locked_in, locked_out or locked_all + name: Lock state + description: New lock state. + required: true example: "unlocked" + selector: + select: + options: + - 'locked_all' + - 'locked_in' + - 'locked_out' + - 'unlocked' diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index de45995797f..64304fa22e5 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -1,13 +1,16 @@ # Describes the format for available switch services turn_on: + name: Turn on description: Turn a switch on target: turn_off: + name: Turn off description: Turn a switch off target: toggle: + name: Toggle description: Toggles a switch state target: diff --git a/homeassistant/components/switcher_kis/services.yaml b/homeassistant/components/switcher_kis/services.yaml index be812f8d7e1..eed3cac0268 100644 --- a/homeassistant/components/switcher_kis/services.yaml +++ b/homeassistant/components/switcher_kis/services.yaml @@ -1,19 +1,34 @@ set_auto_off: + name: Set auto off description: "Update Switcher device auto off setting." + target: + entity: + integration: switcher_kis + domain: switch fields: - entity_id: - description: "Name of the entity id associated with the integration, used for permission validation." - example: "switch.switcher_kis_boiler" auto_off: + name: Auto off description: "Time period string containing hours and minutes." + required: true example: '"02:30"' + selector: + text: turn_on_with_timer: + name: Turn on with timer description: 'Turn on the Switcher device with timer.' + target: + entity: + integration: switcher_kis + domain: switch fields: - entity_id: - description: "Name of the entity id associated with the integration, used for permission validation." - example: "switch.switcher_kis_boiler" timer_minutes: - description: 'Minutes to turn on (valid range from 1 to 150)' + name: Timer + description: 'Time to turn on.' + required: true example: '30' + selector: + number: + min: 1 + max: 150 + unit_of_measurement: minutes diff --git a/homeassistant/components/tado/services.yaml b/homeassistant/components/tado/services.yaml index c9bba7c0ea8..f73eaa8a183 100644 --- a/homeassistant/components/tado/services.yaml +++ b/homeassistant/components/tado/services.yaml @@ -1,35 +1,74 @@ set_climate_timer: + name: Set climate timer description: Turn on climate entities for a set time. + target: + entity: + integration: tado + domain: climate fields: - entity_id: - description: Entity ID for the tado component to turn on for a set time. - example: climate.heating time_period: + name: Time period description: Set the time period for the boost. + required: true example: "01:30:00" + default: "01:00:00" + selector: + text: temperature: + name: Temperature description: Temperature to set climate entity to + required: true example: 25 + selector: + number: + min: 0 + max: 100 + step: 0.5 + unit_of_measurement: '°' set_water_heater_timer: + name: Set water heater timer description: Turn on water heater for a set time. + target: + entity: + integration: tado + domain: climate fields: - entity_id: - description: Entity ID for the tado component to turn on for a set time. - example: water_heater.hot_water time_period: + name: Time period description: Set the time period for the boost. + required: true example: "01:30:00" + default: "01:00:00" + selector: + text: temperature: + name: Temperature description: Temperature to set heater to example: 25 + selector: + number: + min: 0 + max: 100 + step: 0.5 + unit_of_measurement: '°' set_climate_temperature_offset: + name: Set climate temperature offset description: Set the temperature offset of climate entities + target: + entity: + integration: tado + domain: climate fields: - entity_id: - description: Entity ID for the tado component to set the temperature offset - example: climate.heating offset: - description: Offset you would like, can be to 2 decimal places (depending on your device) positive or negative + name: Offset + description: Offset you would like (depending on your device). example: -1.2 + default: 0 + selector: + number: + min: -10 + max: 10 + step: 0.01 + unit_of_measurement: '°' diff --git a/homeassistant/components/telegram/services.yaml b/homeassistant/components/telegram/services.yaml index c467de06fe5..bbdd82768f5 100644 --- a/homeassistant/components/telegram/services.yaml +++ b/homeassistant/components/telegram/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload telegram notify services. diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index 5e2b06564dd..de537aac5ad 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -1,389 +1,781 @@ # Describes the format for available Telegram bot services send_message: + name: Send message description: Send a notification. fields: message: + name: Message description: Message body of the notification. + required: true example: The garage door has been open for 10 minutes. + selector: + text: title: + name: Title description: Optional title for your notification. Will be composed as '%title\n%message' example: "Your Garage Door Friend" + selector: + text: target: + name: Target description: An array of pre-authorized chat_ids to send the notification to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + selector: + object: parse_mode: - description: "Parser for the message text: `markdownv2`, `html` or `markdown`." - example: "html" + name: Parse mode + description: "Parser for the message text." + selector: + select: + options: + - 'html' + - 'markdown' + - 'markdown2' disable_notification: + name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. - example: true + selector: + boolean: disable_web_page_preview: + name: Disable web page preview description: Disables link previews for links in the message. - example: true + selector: + boolean: timeout: + name: Timeout description: Timeout for send message. Will help with timeout errors (poor internet connection, etc) example: "1000" keyboard: + name: Keyboard description: List of rows of commands, comma-separated, to make a custom keyboard. Empty list clears a previously set keyboard. example: '["/command1, /command2", "/command3"]' + selector: + object: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or ["Text button1:/button1, Text button2:/button2", "Text button3:/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: message_tag: + name: Message tag description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' example: "msg_to_edit" + selector: + text: send_photo: + name: Send photo description: Send a photo. fields: url: + name: URL description: Remote path to an image. example: "http://example.org/path/to/the/image.png" + selector: + text: file: + name: File description: Local path to an image. example: "/path/to/the/image.png" + selector: + text: caption: + name: Caption description: The title of the image. example: "My image" + selector: + text: username: + name: Username description: Username for a URL which require HTTP basic authentication. example: myuser + selector: + text: password: + name: Password description: Password for a URL which require HTTP basic authentication. example: myuser_pwd + selector: + text: target: + name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + selector: + object: parse_mode: - description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + name: Parse mode + description: "Parser for the message text." example: "html" + selector: + select: + options: + - 'html' + - 'markdown' + - 'markdown2' disable_notification: + name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. - example: true + selector: + boolean: verify_ssl: + name: Verify SSL description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. - example: false + selector: + boolean: timeout: + name: Timeout description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc) example: "1000" + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds keyboard: + name: Keyboard description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' + selector: + object: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: message_tag: + name: Message tag description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' example: "msg_to_edit" + selector: + text: send_sticker: + name: Send sticker description: Send a sticker. fields: url: + name: URL description: Remote path to a static .webp or animated .tgs sticker. example: "http://example.org/path/to/the/sticker.webp" + selector: + text: file: + name: File description: Local path to a static .webp or animated .tgs sticker. example: "/path/to/the/sticker.webp" + selector: + text: username: + name: Username description: Username for a URL which require HTTP basic authentication. example: myuser + selector: + text: password: + name: Password description: Password for a URL which require HTTP basic authentication. example: myuser_pwd + selector: + text: target: + name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + selector: + object: disable_notification: + name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. - example: true + selector: + boolean: verify_ssl: + name: Verify SSL description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. - example: false + selector: + boolean: timeout: + name: Timeout description: Timeout for send sticker. Will help with timeout errors (poor internet connection, etc) - example: "1000" + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds keyboard: + name: Keyboard description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' + selector: + object: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: message_tag: + name: Message tag description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' example: "msg_to_edit" + selector: + text: send_animation: + name: Send animation description: Send an anmiation. fields: url: + name: URL description: Remote path to a GIF or H.264/MPEG-4 AVC video without sound. example: "http://example.org/path/to/the/animation.gif" + selector: + text: file: + name: File description: Local path to a GIF or H.264/MPEG-4 AVC video without sound. example: "/path/to/the/animation.gif" + selector: + text: caption: + name: Caption description: The title of the animation. example: "My animation" + selector: + text: username: + name: Username description: Username for a URL which require HTTP basic authentication. example: myuser + selector: + text: password: + name: Password description: Password for a URL which require HTTP basic authentication. example: myuser_pwd + selector: + text: target: + name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + selector: + object: parse_mode: - description: "Parser for the message text: `markdownv2`, `html` or `markdown`." - example: "html" + name: Parse Mode + description: "Parser for the message text." + selector: + select: + options: + - 'html' + - 'markdown' + - 'markdown2' disable_notification: + name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. example: true + selector: + boolean: verify_ssl: + name: Verify SSL description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. example: false + selector: + boolean: timeout: + name: Timeout description: Timeout for send sticker. Will help with timeout errors (poor internet connection, etc) - example: "1000" + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds keyboard: + name: Keyboard description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' + selector: + object: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: send_video: + name: Send video description: Send a video. fields: url: + name: URL description: Remote path to a video. example: "http://example.org/path/to/the/video.mp4" + selector: + text: file: + name: File description: Local path to a video. example: "/path/to/the/video.mp4" + selector: + text: caption: + name: Caption description: The title of the video. example: "My video" + selector: + text: username: + name: Username description: Username for a URL which require HTTP basic authentication. example: myuser + selector: + text: password: + name: Password description: Password for a URL which require HTTP basic authentication. example: myuser_pwd + selector: + text: target: + name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + selector: + object: parse_mode: - description: "Parser for the message text: `markdownv2`, `html` or `markdown`." - example: "html" + name: Parse mode + description: "Parser for the message text." + selector: + select: + options: + - 'html' + - 'markdown' + - 'markdown2' disable_notification: + name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. - example: true + selector: + boolean: verify_ssl: + name: Verify SSL description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. - example: false + selector: + boolean: timeout: + name: Timeout description: Timeout for send video. Will help with timeout errors (poor internet connection, etc) - example: "1000" + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds keyboard: + name: Keyboard description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' + selector: + object: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: message_tag: + name: Message tag description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' example: "msg_to_edit" + selector: + text: send_voice: + name: Send voice description: Send a voice message. fields: url: + name: URL description: Remote path to a voice message. example: "http://example.org/path/to/the/voice.opus" + selector: + text: file: + name: File description: Local path to a voice message. example: "/path/to/the/voice.opus" + selector: + text: caption: + name: Caption description: The title of the voice message. example: "My microphone recording" + selector: + text: username: + name: Username description: Username for a URL which require HTTP basic authentication. example: myuser + selector: + text: password: + name: Password description: Password for a URL which require HTTP basic authentication. example: myuser_pwd + selector: + text: target: + name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + selector: + object: disable_notification: + name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. - example: true + selector: + boolean: verify_ssl: + name: Verify SSL description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. - example: false + selector: + boolean: timeout: + name: Timeout description: Timeout for send voice. Will help with timeout errors (poor internet connection, etc) - example: "1000" + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds keyboard: + name: Keyboard description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' + selector: + object: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: message_tag: + name: Message tag description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' example: "msg_to_edit" + selector: + text: send_document: + name: Send document description: Send a document. fields: url: + name: URL description: Remote path to a document. example: "http://example.org/path/to/the/document.odf" + selector: + text: file: + name: File description: Local path to a document. example: "/tmp/whatever.odf" + selector: + text: caption: + name: Caption description: The title of the document. example: Document Title xy + selector: + text: username: + name: Username description: Username for a URL which require HTTP basic authentication. example: myuser + selector: + text: password: + name: Password description: Password for a URL which require HTTP basic authentication. example: myuser_pwd + selector: + text: target: + name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + selector: + object: parse_mode: - description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + name: Parse mode + description: "Parser for the message text." example: "html" + selector: + select: + options: + - 'html' + - 'markdown' + - 'markdown2' disable_notification: + name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. - example: true + selector: + boolean: verify_ssl: + name: Verify SSL description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. - example: false + selector: + boolean: timeout: + name: Timeout description: Timeout for send document. Will help with timeout errors (poor internet connection, etc) - example: "1000" + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds keyboard: + name: Keyboard description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' + selector: + object: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: message_tag: + name: Message tag description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' example: "msg_to_edit" + selector: + text: send_location: + name: Send location description: Send a location. fields: latitude: + name: Latitude description: The latitude to send. - example: -15.123 + required: true + selector: + number: + min: -90 + max: 90 + step: 0.001 + unit_of_measurement: '°' longitude: + name: Longitude description: The longitude to send. - example: 38.123 + required: true + selector: + number: + min: -180 + max: 180 + step: 0.001 + unit_of_measurement: '°' target: + name: Target description: An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + selector: + object: disable_notification: + name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. - example: true + selector: + boolean: timeout: + name: Timeout description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc) - example: "1000" + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds keyboard: + name: Keyboard description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' + selector: + object: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: message_tag: + name: Message tag description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' example: "msg_to_edit" + selector: + text: edit_message: + name: Edit message description: Edit a previously sent message. fields: message_id: + name: Message ID description: id of the message to edit. + required: true example: "{{ trigger.event.data.message.message_id }}" + selector: + text: chat_id: + name: Chat ID description: The chat_id where to edit the message. + required: true example: 12345 + selector: + text: message: + name: Message description: Message body of the notification. example: The garage door has been open for 10 minutes. + selector: + text: title: + name: Title description: Optional title for your notification. Will be composed as '%title\n%message' example: "Your Garage Door Friend" + selector: + text: parse_mode: - description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + name: Parse mode + description: "Parser for the message text." example: "html" + selector: + select: + options: + - 'html' + - 'markdown' + - 'markdown2' disable_web_page_preview: + name: Disable web page preview description: Disables link previews for links in the message. - example: true + selector: + boolean: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: edit_caption: + name: Edit caption description: Edit the caption of a previously sent message. fields: message_id: + name: Message ID description: id of the message to edit. + required: true example: "{{ trigger.event.data.message.message_id }}" + selector: + text: chat_id: + name: Chat ID description: The chat_id where to edit the caption. + required: true example: 12345 + selector: + text: caption: + name: Caption description: Message body of the notification. + required: true example: The garage door has been open for 10 minutes. + selector: + text: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: edit_replymarkup: + name: Edit reply markup description: Edit the inline keyboard of a previously sent message. fields: message_id: + name: Message ID description: id of the message to edit. + required: true example: "{{ trigger.event.data.message.message_id }}" + selector: + text: chat_id: + name: Chat ID description: The chat_id where to edit the reply_markup. + required: true example: 12345 + selector: + text: inline_keyboard: + name: Inline keyboard description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. + required: true example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + selector: + object: answer_callback_query: + name: Answer callback query description: Respond to a callback query originated by clicking on an online keyboard button. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. fields: message: + name: Message description: Unformatted text message body of the notification. + required: true example: "OK, I'm listening" + selector: + text: callback_query_id: + name: Callback query ID description: Unique id of the callback response. + required: true example: "{{ trigger.event.data.id }}" + selector: + text: show_alert: + name: Show alert description: Show a permanent notification. - example: true + required: true + selector: + boolean: timeout: + name: Timeout description: Timeout for sending the answer. Will help with timeout errors (poor internet connection, etc) - example: "1000" + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds delete_message: + name: Delete message description: Delete a previously sent message. fields: message_id: + name: Message ID description: id of the message to delete. + required: true example: "{{ trigger.event.data.message.message_id }}" + selector: + text: chat_id: + name: Chat ID description: The chat_id where to delete the message. + required: true example: 12345 + selector: + text: diff --git a/homeassistant/components/template/services.yaml b/homeassistant/components/template/services.yaml index d36111d608e..b6fa371e818 100644 --- a/homeassistant/components/template/services.yaml +++ b/homeassistant/components/template/services.yaml @@ -1,3 +1,4 @@ reload: + name: Reload description: Reload all template entities. diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index 88c057c1ef8..7f2ddd0091d 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -1,33 +1,96 @@ new_task: + name: New task description: Create a new task and add it to a project. fields: content: + name: Content description: The name of the task. + required: true example: Pick up the mail. + selector: + text: project: - description: The name of the project this task should belong to. Defaults to Inbox. + name: Project + description: The name of the project this task should belong to. example: Errands + default: Inbox + selector: + text: labels: + name: Labels description: Any labels that you want to apply to this task, separated by a comma. example: Chores,Delivieries + selector: + text: priority: + name: Priority description: The priority of this task, from 1 (normal) to 4 (urgent). example: 2 + selector: + number: + min: 1 + max: 4 due_date_string: + name: Dure date string description: The day this task is due, in natural language. example: Tomorrow + selector: + text: due_date_lang: + name: Due data language description: The language of due_date_string. - example: en + selector: + select: + options: + - 'da' + - 'de' + - 'en' + - 'es' + - 'fr' + - 'it' + - 'ja' + - 'ko' + - 'nl' + - 'pl' + - 'pt' + - 'ru' + - 'sv' + - 'zh' due_date: + name: Due date description: The time this task is due, in format YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS, in UTC timezone. example: "2019-10-22" + selector: + text: reminder_date_string: + name: Reminder date string description: When should user be reminded of this task, in natural language. example: Tomorrow + selector: + text: reminder_date_lang: + name: Reminder data language description: The language of reminder_date_string. - example: en + selector: + select: + options: + - 'da' + - 'de' + - 'en' + - 'es' + - 'fr' + - 'it' + - 'ja' + - 'ko' + - 'nl' + - 'pl' + - 'pt' + - 'ru' + - 'sv' + - 'zh' reminder_date: + name: Reminder date description: When should user be reminded of this task, in format YYYY-MM-DDTHH:MM:SS, in UTC timezone. example: "2019-10-22T10:30:00" + selector: + text: diff --git a/homeassistant/components/toon/services.yaml b/homeassistant/components/toon/services.yaml index 909018f820b..d01cf32994b 100644 --- a/homeassistant/components/toon/services.yaml +++ b/homeassistant/components/toon/services.yaml @@ -4,7 +4,7 @@ update: fields: display: name: Display - description: Toon display to update (optional) + description: Toon display to update. advanced: true example: eneco-001-123456 selector: diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index 04ac5472d4c..74861df5a70 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -1,42 +1,79 @@ add_torrent: + name: Add torrent description: Add a new torrent to download (URL, magnet link or Base64 encoded). fields: name: + name: Name description: Instance name as entered during entry config + required: true example: Transmission + selector: + text: torrent: + name: Torrent description: URL, magnet link or Base64 encoded file. + required: true example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent + selector: + text: remove_torrent: + name: Remove torrent description: Remove a torrent fields: name: + name: Name description: Instance name as entered during entry config + required: true example: Transmission + selector: + text: id: + name: ID description: ID of a torrent + required: true example: 123 + selector: + text: delete_data: + name: Delete data description: Delete torrent data - example: false + default: false + selector: + boolean: start_torrent: + name: Start torrent description: Start a torrent fields: name: + name: Name description: Instance name as entered during entry config example: Transmission + selector: + text: id: + name: ID description: ID of a torrent example: 123 + selector: + text: stop_torrent: + name: Stop torrent description: Stop a torrent fields: name: + name: Name description: Instance name as entered during entry config + required: true example: Transmission + selector: + text: id: + name: ID description: ID of a torrent + required: true example: 123 + selector: + text: diff --git a/homeassistant/components/trend/services.yaml b/homeassistant/components/trend/services.yaml index 6c4b027ef99..1d29e08dccf 100644 --- a/homeassistant/components/trend/services.yaml +++ b/homeassistant/components/trend/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all trend entities. diff --git a/homeassistant/components/tuya/services.yaml b/homeassistant/components/tuya/services.yaml index c96ea3fd09f..42fba3ad37b 100644 --- a/homeassistant/components/tuya/services.yaml +++ b/homeassistant/components/tuya/services.yaml @@ -1,7 +1,9 @@ # Describes the format for available Tuya services pull_devices: + name: Pull devices description: Pull device list from Tuya server. force_update: + name: Force update description: Force all Tuya devices to pull data. From 25b2fd0ceee84bae63f02a12e0a681eecac77822 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 15 May 2021 07:54:11 +0200 Subject: [PATCH 427/852] Add strict typing to fritzbox (#50486) * enable strict typing * apply suggestions * set defaults for FritzboxConfigFlow * improvements and suggestions * another suggestion * tweaks * tweaks --- .strict-typing | 1 + homeassistant/components/fritzbox/__init__.py | 24 +++---- .../components/fritzbox/binary_sensor.py | 8 ++- homeassistant/components/fritzbox/climate.py | 55 ++++++++-------- .../components/fritzbox/config_flow.py | 62 ++++++++++++------- homeassistant/components/fritzbox/const.py | 35 ++++++----- homeassistant/components/fritzbox/model.py | 43 +++++++++++++ homeassistant/components/fritzbox/sensor.py | 17 ++--- homeassistant/components/fritzbox/switch.py | 32 ++++++---- homeassistant/const.py | 10 +-- mypy.ini | 14 ++++- script/hassfest/mypy_config.py | 1 - 12 files changed, 195 insertions(+), 107 deletions(-) create mode 100644 homeassistant/components/fritzbox/model.py diff --git a/.strict-typing b/.strict-typing index 87bef3033c4..e6f0f96368b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -13,6 +13,7 @@ homeassistant.components.camera.* homeassistant.components.cover.* homeassistant.components.device_automation.* homeassistant.components.elgato.* +homeassistant.components.fritzbox.* homeassistant.components.frontend.* homeassistant.components.geo_location.* homeassistant.components.gios.* diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index dc200195748..124719b93c1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -17,14 +17,16 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) from .const import CONF_CONNECTIONS, CONF_COORDINATOR, DOMAIN, LOGGER, PLATFORMS +from .model import EntityInfo async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -63,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data[device.ain] = device return data - async def async_update_coordinator(): + async def async_update_coordinator() -> dict[str, FritzhomeDevice]: """Fetch all device data.""" return await hass.async_add_executor_job(_update_fritz_devices) @@ -81,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - def logout_fritzbox(event): + def logout_fritzbox(event: Event) -> None: """Close connections to this fritzbox.""" fritz.logout() @@ -109,10 +111,10 @@ class FritzBoxEntity(CoordinatorEntity): def __init__( self, - entity_info: dict[str, str], - coordinator: DataUpdateCoordinator, + entity_info: EntityInfo, + coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], ain: str, - ): + ) -> None: """Initialize the FritzBox entity.""" super().__init__(coordinator) @@ -128,7 +130,7 @@ class FritzBoxEntity(CoordinatorEntity): return self.coordinator.data[self.ain] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return { "name": self.device.name, @@ -139,21 +141,21 @@ class FritzBoxEntity(CoordinatorEntity): } @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the device.""" return self._unique_id @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" return self._unit_of_measurement @property - def device_class(self): + def device_class(self) -> str | None: """Return the device class.""" return self._device_class diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 807ca41ca64..242e3d6e644 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -1,4 +1,6 @@ """Support for Fritzbox binary sensors.""" +from __future__ import annotations + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_WINDOW, BinarySensorEntity, @@ -21,7 +23,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the FRITZ!SmartHome binary sensor from ConfigEntry.""" - entities = [] + entities: list[FritzboxBinarySensor] = [] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] for ain, device in coordinator.data.items(): @@ -48,8 +50,8 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): """Representation of a binary FRITZ!SmartHome device.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if sensor is on.""" if not self.device.present: return False - return self.device.alert_state + return self.device.alert_state # type: ignore [no-any-return] diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 947a95a7a5b..c50e0d4f270 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -1,4 +1,8 @@ """Support for AVM FRITZ!SmartHome thermostate devices.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, @@ -34,6 +38,7 @@ from .const import ( CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, ) +from .model import ClimateExtraAttributes SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -55,7 +60,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the FRITZ!SmartHome thermostat from ConfigEntry.""" - entities = [] + entities: list[FritzboxThermostat] = [] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] for ain, device in coordinator.data.items(): @@ -82,53 +87,53 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): """The thermostat class for FRITZ!SmartHome thermostates.""" @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_FLAGS @property - def available(self): + def available(self) -> bool: """Return if thermostat is available.""" - return self.device.present + return self.device.present # type: ignore [no-any-return] @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement that is used.""" return TEMP_CELSIUS @property - def precision(self): + def precision(self) -> float: """Return precision 0.5.""" return PRECISION_HALVES @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" - return self.device.actual_temperature + return self.device.actual_temperature # type: ignore [no-any-return] @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the temperature we try to reach.""" if self.device.target_temperature == ON_API_TEMPERATURE: return ON_REPORT_SET_TEMPERATURE if self.device.target_temperature == OFF_API_TEMPERATURE: return OFF_REPORT_SET_TEMPERATURE - return self.device.target_temperature + return self.device.target_temperature # type: ignore [no-any-return] - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - if ATTR_HVAC_MODE in kwargs: - hvac_mode = kwargs.get(ATTR_HVAC_MODE) + if kwargs.get(ATTR_HVAC_MODE) is not None: + hvac_mode = kwargs[ATTR_HVAC_MODE] await self.async_set_hvac_mode(hvac_mode) - elif ATTR_TEMPERATURE in kwargs: - temperature = kwargs.get(ATTR_TEMPERATURE) + elif kwargs.get(ATTR_TEMPERATURE) is not None: + temperature = kwargs[ATTR_TEMPERATURE] await self.hass.async_add_executor_job( self.device.set_target_temperature, temperature ) await self.coordinator.async_refresh() @property - def hvac_mode(self): + def hvac_mode(self) -> str: """Return the current operation mode.""" if ( self.device.target_temperature == OFF_REPORT_SET_TEMPERATURE @@ -139,11 +144,11 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): return HVAC_MODE_HEAT @property - def hvac_modes(self): + def hvac_modes(self) -> list[str]: """Return the list of available operation modes.""" return OPERATION_LIST - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new operation mode.""" if hvac_mode == HVAC_MODE_OFF: await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) @@ -153,7 +158,7 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): ) @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return current preset mode.""" if self.device.target_temperature == self.device.comfort_temperature: return PRESET_COMFORT @@ -162,11 +167,11 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): return None @property - def preset_modes(self): + def preset_modes(self) -> list[str]: """Return supported preset modes.""" return [PRESET_ECO, PRESET_COMFORT] - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" if preset_mode == PRESET_COMFORT: await self.async_set_temperature( @@ -176,19 +181,19 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): await self.async_set_temperature(temperature=self.device.eco_temperature) @property - def min_temp(self): + def min_temp(self) -> int: """Return the minimum temperature.""" return MIN_TEMPERATURE @property - def max_temp(self): + def max_temp(self) -> int: """Return the maximum temperature.""" return MAX_TEMPERATURE @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> ClimateExtraAttributes: """Return the device specific state attributes.""" - attrs = { + attrs: ClimateExtraAttributes = { ATTR_STATE_BATTERY_LOW: self.device.battery_low, ATTR_STATE_DEVICE_LOCKED: self.device.device_lock, ATTR_STATE_LOCKED: self.device.lock, diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 50a9a5f0117..3ae3368f4ae 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -1,17 +1,22 @@ """Config flow for AVM FRITZ!SmartHome.""" +from __future__ import annotations + +from typing import Any from urllib.parse import urlparse from pyfritzhome import Fritzhome, LoginError from requests.exceptions import HTTPError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN, ) +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN @@ -36,22 +41,22 @@ RESULT_NOT_SUPPORTED = "not_supported" RESULT_SUCCESS = "success" -class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a AVM FRITZ!SmartHome config flow.""" VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize flow.""" - self._entry = None - self._host = None - self._name = None - self._password = None - self._username = None + self._entry: ConfigEntry | None = None + self._host: str | None = None + self._name: str | None = None + self._password: str | None = None + self._username: str | None = None - def _get_entry(self): + def _get_entry(self, name: str) -> FlowResult: return self.async_create_entry( - title=self._name, + title=name, data={ CONF_HOST: self._host, CONF_PASSWORD: self._password, @@ -59,7 +64,8 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def _update_entry(self): + async def _update_entry(self) -> None: + assert self._entry is not None self.hass.config_entries.async_update_entry( self._entry, data={ @@ -70,7 +76,7 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) await self.hass.config_entries.async_reload(self._entry.entry_id) - def _try_connect(self): + def _try_connect(self) -> str: """Try to connect and check auth.""" fritzbox = Fritzhome( host=self._host, user=self._username, password=self._password @@ -87,7 +93,9 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except OSError: return RESULT_NO_DEVICES_FOUND - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -95,14 +103,14 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) self._host = user_input[CONF_HOST] - self._name = user_input[CONF_HOST] + self._name = str(user_input[CONF_HOST]) self._password = user_input[CONF_PASSWORD] self._username = user_input[CONF_USERNAME] result = await self.hass.async_add_executor_job(self._try_connect) if result == RESULT_SUCCESS: - return self._get_entry() + return self._get_entry(self._name) if result != RESULT_INVALID_AUTH: return self.async_abort(reason=result) errors["base"] = result @@ -111,9 +119,10 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors ) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname + assert isinstance(host, str) self.context[CONF_HOST] = host uuid = discovery_info.get(ATTR_UPNP_UDN) @@ -135,12 +144,14 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") self._host = host - self._name = discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) or host + self._name = str(discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) or host) self.context["title_placeholders"] = {"name": self._name} return await self.async_step_confirm() - async def async_step_confirm(self, user_input=None): + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle user-confirmation of discovered node.""" errors = {} @@ -150,7 +161,8 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): result = await self.hass.async_add_executor_job(self._try_connect) if result == RESULT_SUCCESS: - return self._get_entry() + assert self._name is not None + return self._get_entry(self._name) if result != RESULT_INVALID_AUTH: return self.async_abort(reason=result) errors["base"] = result @@ -162,16 +174,20 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data): + async def async_step_reauth(self, data: dict[str, str]) -> FlowResult: """Trigger a reauthentication flow.""" - self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry is not None + self._entry = entry self._host = data[CONF_HOST] - self._name = data[CONF_HOST] + self._name = str(data[CONF_HOST]) self._username = data[CONF_USERNAME] return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle reauthorization flow.""" errors = {} diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index edfc13d49fe..af2ec30312f 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -1,26 +1,29 @@ """Constants for the AVM FRITZ!SmartHome integration.""" +from __future__ import annotations + import logging +from typing import Final -ATTR_STATE_BATTERY_LOW = "battery_low" -ATTR_STATE_DEVICE_LOCKED = "device_locked" -ATTR_STATE_HOLIDAY_MODE = "holiday_mode" -ATTR_STATE_LOCKED = "locked" -ATTR_STATE_SUMMER_MODE = "summer_mode" -ATTR_STATE_WINDOW_OPEN = "window_open" +ATTR_STATE_BATTERY_LOW: Final = "battery_low" +ATTR_STATE_DEVICE_LOCKED: Final = "device_locked" +ATTR_STATE_HOLIDAY_MODE: Final = "holiday_mode" +ATTR_STATE_LOCKED: Final = "locked" +ATTR_STATE_SUMMER_MODE: Final = "summer_mode" +ATTR_STATE_WINDOW_OPEN: Final = "window_open" -ATTR_TEMPERATURE_UNIT = "temperature_unit" +ATTR_TEMPERATURE_UNIT: Final = "temperature_unit" -ATTR_TOTAL_CONSUMPTION = "total_consumption" -ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" +ATTR_TOTAL_CONSUMPTION: Final = "total_consumption" +ATTR_TOTAL_CONSUMPTION_UNIT: Final = "total_consumption_unit" -CONF_CONNECTIONS = "connections" -CONF_COORDINATOR = "coordinator" +CONF_CONNECTIONS: Final = "connections" +CONF_COORDINATOR: Final = "coordinator" -DEFAULT_HOST = "fritz.box" -DEFAULT_USERNAME = "admin" +DEFAULT_HOST: Final = "fritz.box" +DEFAULT_USERNAME: Final = "admin" -DOMAIN = "fritzbox" +DOMAIN: Final = "fritzbox" -LOGGER: logging.Logger = logging.getLogger(__package__) +LOGGER: Final[logging.Logger] = logging.getLogger(__package__) -PLATFORMS = ["binary_sensor", "climate", "switch", "sensor"] +PLATFORMS: Final[list[str]] = ["binary_sensor", "climate", "switch", "sensor"] diff --git a/homeassistant/components/fritzbox/model.py b/homeassistant/components/fritzbox/model.py new file mode 100644 index 00000000000..1cde7b9ca70 --- /dev/null +++ b/homeassistant/components/fritzbox/model.py @@ -0,0 +1,43 @@ +"""Models for the AVM FRITZ!SmartHome integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class EntityInfo(TypedDict): + """TypedDict for EntityInfo.""" + + name: str + entity_id: str + unit_of_measurement: str | None + device_class: str | None + + +class ClimateExtraAttributes(TypedDict, total=False): + """TypedDict for climates extra attributes.""" + + battery_low: bool + device_locked: bool + locked: bool + battery_level: int + holiday_mode: bool + summer_mode: bool + window_open: bool + + +class SensorExtraAttributes(TypedDict): + """TypedDict for sensors extra attributes.""" + + device_locked: bool + locked: bool + + +class SwitchExtraAttributes(TypedDict, total=False): + """TypedDict for sensors extra attributes.""" + + device_locked: bool + locked: bool + total_consumption: str + total_consumption_unit: str + temperature: str + temperature_unit: str diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 836b4fb407c..ceae0bb757f 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,4 +1,6 @@ """Support for AVM FRITZ!SmartHome temperature sensor only devices.""" +from __future__ import annotations + from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -20,13 +22,14 @@ from .const import ( CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, ) +from .model import SensorExtraAttributes async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the FRITZ!SmartHome sensor from ConfigEntry.""" - entities = [] + entities: list[FritzBoxEntity] = [] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] for ain, device in coordinator.data.items(): @@ -69,23 +72,23 @@ class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity): """The entity class for FRITZ!SmartHome sensors.""" @property - def state(self): + def state(self) -> int | None: """Return the state of the sensor.""" - return self.device.battery_level + return self.device.battery_level # type: ignore [no-any-return] class FritzBoxTempSensor(FritzBoxEntity, SensorEntity): """The entity class for FRITZ!SmartHome temperature sensors.""" @property - def state(self): + def state(self) -> float | None: """Return the state of the sensor.""" - return self.device.temperature + return self.device.temperature # type: ignore [no-any-return] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> SensorExtraAttributes: """Return the state attributes of the device.""" - attrs = { + attrs: SensorExtraAttributes = { ATTR_STATE_DEVICE_LOCKED: self.device.device_lock, ATTR_STATE_LOCKED: self.device.lock, } diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 39d8c4e8ec2..82581473714 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -1,4 +1,8 @@ """Support for AVM FRITZ!SmartHome switch devices.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -23,6 +27,7 @@ from .const import ( CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, ) +from .model import SwitchExtraAttributes ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR @@ -31,7 +36,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the FRITZ!SmartHome switch from ConfigEntry.""" - entities = [] + entities: list[FritzboxSwitch] = [] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] for ain, device in coordinator.data.items(): @@ -58,31 +63,32 @@ class FritzboxSwitch(FritzBoxEntity, SwitchEntity): """The switch class for FRITZ!SmartHome switches.""" @property - def available(self): + def available(self) -> bool: """Return if switch is available.""" - return self.device.present + return self.device.present # type: ignore [no-any-return] @property - def is_on(self): + def is_on(self) -> bool: """Return true if the switch is on.""" - return self.device.switch_state + return self.device.switch_state # type: ignore [no-any-return] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.hass.async_add_executor_job(self.device.set_switch_state_on) await self.coordinator.async_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.hass.async_add_executor_job(self.device.set_switch_state_off) await self.coordinator.async_refresh() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> SwitchExtraAttributes: """Return the state attributes of the device.""" - attrs = {} - attrs[ATTR_STATE_DEVICE_LOCKED] = self.device.device_lock - attrs[ATTR_STATE_LOCKED] = self.device.lock + attrs: SwitchExtraAttributes = { + ATTR_STATE_DEVICE_LOCKED: self.device.device_lock, + ATTR_STATE_LOCKED: self.device.lock, + } if self.device.has_powermeter: attrs[ @@ -99,6 +105,6 @@ class FritzboxSwitch(FritzBoxEntity, SwitchEntity): return attrs @property - def current_power_w(self): + def current_power_w(self) -> float: """Return the current power usage in W.""" - return self.device.power / 1000 + return self.device.power / 1000 # type: ignore [no-any-return] diff --git a/homeassistant/const.py b/homeassistant/const.py index 489652b3c12..8b965995754 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -294,7 +294,7 @@ ATTR_ID = "id" ATTR_NAME: Final = "name" # Contains one string or a list of strings, each being an entity id -ATTR_ENTITY_ID = "entity_id" +ATTR_ENTITY_ID: Final = "entity_id" # Contains one string or a list of strings, each being an area id ATTR_AREA_ID = "area_id" @@ -314,7 +314,7 @@ ATTR_IDENTIFIERS: Final = "identifiers" ATTR_ICON = "icon" # The unit of measurement if applicable -ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement" +ATTR_UNIT_OF_MEASUREMENT: Final = "unit_of_measurement" CONF_UNIT_SYSTEM_METRIC: str = "metric" CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial" @@ -332,7 +332,7 @@ ATTR_MODEL: Final = "model" ATTR_SW_VERSION: Final = "sw_version" ATTR_BATTERY_CHARGING = "battery_charging" -ATTR_BATTERY_LEVEL = "battery_level" +ATTR_BATTERY_LEVEL: Final = "battery_level" ATTR_WAKEUP = "wake_up_interval" # For devices which support a code attribute @@ -379,10 +379,10 @@ ATTR_RESTORED = "restored" ATTR_SUPPORTED_FEATURES = "supported_features" # Class of device within its domain -ATTR_DEVICE_CLASS = "device_class" +ATTR_DEVICE_CLASS: Final = "device_class" # Temperature attribute -ATTR_TEMPERATURE = "temperature" +ATTR_TEMPERATURE: Final = "temperature" # #### UNITS OF MEASUREMENT #### # Power units diff --git a/mypy.ini b/mypy.ini index 8bc55b0d82e..3fc323dab44 100644 --- a/mypy.ini +++ b/mypy.ini @@ -154,6 +154,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.fritzbox.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.frontend.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -781,9 +792,6 @@ ignore_errors = true [mypy-homeassistant.components.freebox.*] ignore_errors = true -[mypy-homeassistant.components.fritzbox.*] -ignore_errors = true - [mypy-homeassistant.components.garmin_connect.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 72bb8879312..5e412517628 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -69,7 +69,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.fortios.*", "homeassistant.components.foscam.*", "homeassistant.components.freebox.*", - "homeassistant.components.fritzbox.*", "homeassistant.components.garmin_connect.*", "homeassistant.components.geniushub.*", "homeassistant.components.glances.*", From 0eca26607db4821e8ac5ac68a163a0f2e6fc0450 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 15 May 2021 03:25:19 -0400 Subject: [PATCH 428/852] Add targets and selectors for services (D-E) (#50190) --- .../components/debugpy/services.yaml | 1 + homeassistant/components/demo/services.yaml | 1 + .../components/denonavr/services.yaml | 14 +- .../components/dominos/services.yaml | 4 + .../components/duckdns/services.yaml | 5 + .../components/dynalite/services.yaml | 32 +++- homeassistant/components/dyson/services.yaml | 87 +++++++-- homeassistant/components/ebusd/services.yaml | 5 + .../components/eight_sleep/services.yaml | 21 +++ homeassistant/components/elkm1/services.yaml | 178 ++++++++++++++---- .../components/envisalink/services.yaml | 24 ++- homeassistant/components/epson/services.yaml | 12 +- .../components/evohome/services.yaml | 45 ++++- 13 files changed, 363 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/debugpy/services.yaml b/homeassistant/components/debugpy/services.yaml index 6bf9ad67288..c864684226f 100644 --- a/homeassistant/components/debugpy/services.yaml +++ b/homeassistant/components/debugpy/services.yaml @@ -1,3 +1,4 @@ # Describes the format for available Remote Python Debugger services start: + name: Start description: Start the Remote Python Debugger diff --git a/homeassistant/components/demo/services.yaml b/homeassistant/components/demo/services.yaml index aed23eed95a..a09b4498035 100644 --- a/homeassistant/components/demo/services.yaml +++ b/homeassistant/components/demo/services.yaml @@ -1,2 +1,3 @@ randomize_device_tracker_data: + name: Randomize device tracker data description: Demonstrates using a device tracker to see where devices are located diff --git a/homeassistant/components/denonavr/services.yaml b/homeassistant/components/denonavr/services.yaml index d79652dd1f8..cea14a8b361 100644 --- a/homeassistant/components/denonavr/services.yaml +++ b/homeassistant/components/denonavr/services.yaml @@ -1,15 +1,22 @@ # Describes the format for available denonavr services get_command: + name: Get command description: "Send a generic HTTP get command." + target: + entity: + integration: denonavr + domain: media_player fields: - entity_id: - description: Name(s) of the denonavr entities where to run the API method. - example: "media_player.living_room_receiver" command: + name: Command description: Endpoint of the command, including associated parameters. example: "/goform/formiPhoneAppDirect.xml?RCKSK0410370" + required: true + selector: + text: set_dynamic_eq: + name: Set dynamic equalizer description: "Enable or disable DynamicEQ." target: entity: @@ -23,6 +30,7 @@ set_dynamic_eq: selector: boolean: update_audyssey: + name: Update audyssey description: "Update Audyssey settings." target: entity: diff --git a/homeassistant/components/dominos/services.yaml b/homeassistant/components/dominos/services.yaml index 93f8b2851f1..6a354bc3a63 100644 --- a/homeassistant/components/dominos/services.yaml +++ b/homeassistant/components/dominos/services.yaml @@ -1,6 +1,10 @@ order: + name: Order description: Places a set of orders with Dominos Pizza. fields: order_entity_id: + name: Order Entity description: The ID (as specified in the configuration) of an order to place. If provided as an array, all of the identified orders will be placed. example: dominos.medium_pan + selector: + text: diff --git a/homeassistant/components/duckdns/services.yaml b/homeassistant/components/duckdns/services.yaml index e0ba27390df..6c8b5af8199 100644 --- a/homeassistant/components/duckdns/services.yaml +++ b/homeassistant/components/duckdns/services.yaml @@ -1,6 +1,11 @@ set_txt: + name: Set TXT description: Set the TXT record of your DuckDNS subdomain. fields: txt: + name: TXT description: Payload for the TXT record. + required: true example: "This domain name is reserved for use in documentation" + selector: + text: diff --git a/homeassistant/components/dynalite/services.yaml b/homeassistant/components/dynalite/services.yaml index afdb01bf351..7aee7627f14 100644 --- a/homeassistant/components/dynalite/services.yaml +++ b/homeassistant/components/dynalite/services.yaml @@ -1,26 +1,54 @@ request_area_preset: + name: Request area preset description: "Requests Dynalite to report the preset for an area." fields: host: description: "Host gateway IP to send to or all configured gateways if not specified." example: "192.168.0.101" + selector: + text: area: description: "Area to request the preset reported" + required: true example: 2 + selector: + number: + min: 1 + max: 9999 channel: - description: "Channel to request the preset to be reported from. Default is channel 1" + description: "Channel to request the preset to be reported from." example: 1 + default: 1 + selector: + number: + min: 1 + max: 9999 request_channel_level: + name: Request channel level description: "Requests Dynalite to report the level of a specific channel." fields: host: + name: Host description: "Host gateway IP to send to or all configured gateways if not specified." example: "192.168.0.101" + selector: + text: area: + name: Area description: "Area for the requested channel" + required: true example: 2 + selector: + number: + min: 1 + max: 9999 channel: + name: Channel description: "Channel to request the level for." + required: true example: 1 - + selector: + number: + min: 1 + max: 9999 diff --git a/homeassistant/components/dyson/services.yaml b/homeassistant/components/dyson/services.yaml index f96aa9315c1..6f6bc2d9c8a 100644 --- a/homeassistant/components/dyson/services.yaml +++ b/homeassistant/components/dyson/services.yaml @@ -1,64 +1,115 @@ # Describes the format for available fan services set_night_mode: + name: Set night mode description: Set the fan in night mode. + target: + entity: + integration: dyson + domain: fan fields: - entity_id: - description: Name(s) of the entities to enable/disable night mode - example: "fan.living_room" night_mode: + name: Night mode description: Night mode status + required: true example: true + selector: + boolean: set_auto_mode: + name: Set auto mode description: Set the fan in auto mode. + target: + entity: + integration: dyson + domain: fan fields: - entity_id: - description: Name(s) of the entities to enable/disable auto mode - example: "fan.living_room" auto_mode: + name: Auto Mode description: Auto mode status + required: true example: true + selector: + boolean: set_angle: + name: Set angle description: Set the oscillation angle of the selected fan(s). + target: + entity: + integration: dyson + domain: fan fields: - entity_id: - description: Name(s) of the entities for which to set the angle - example: "fan.living_room" angle_low: + name: Angle low description: The angle at which the oscillation should start + required: true example: 1 + selector: + number: + min: 5 + max: 355 + unit_of_measurement: '°' angle_high: + name: Angle high description: The angle at which the oscillation should end + required: true example: 255 + selector: + number: + min: 5 + max: 355 + unit_of_measurement: '°' set_flow_direction_front: + name: Set flow direction front description: Set the fan flow direction. + target: + entity: + integration: dyson + domain: fan fields: - entity_id: - description: Name(s) of the entities to set frontal flow direction for - example: "fan.living_room" flow_direction_front: + name: Flow direction front description: Frontal flow direction + required: true example: true + selector: + boolean: set_timer: + name: Set timer description: Set the sleep timer. + target: + entity: + integration: dyson + domain: fan fields: - entity_id: - description: Name(s) of the entities to set the sleep timer for - example: "fan.living_room" timer: + name: Timer description: The value in minutes to set the timer to, 0 to disable it + required: true example: 30 + selector: + number: + min: 0 + max: 720 + unit_of_measurement: minutes set_speed: + name: Set speed description: Set the exact speed of the fan. + target: + entity: + integration: dyson + domain: fan fields: - entity_id: - description: Name(s) of the entities to set the speed for - example: "fan.living_room" dyson_speed: + name: Speed description: Speed + required: true example: 1 + selector: + number: + min: 1 + max: 10 diff --git a/homeassistant/components/ebusd/services.yaml b/homeassistant/components/ebusd/services.yaml index eee9896da10..dc356bec226 100644 --- a/homeassistant/components/ebusd/services.yaml +++ b/homeassistant/components/ebusd/services.yaml @@ -1,6 +1,11 @@ write: + name: Write description: Call ebusd write command. fields: call: + name: Call description: Property name and value to set + required: true example: '{"name": "Hc1MaxFlowTempDesired", "value": 21}' + selector: + object: diff --git a/homeassistant/components/eight_sleep/services.yaml b/homeassistant/components/eight_sleep/services.yaml index 05354bccc68..7ef2f22d298 100644 --- a/homeassistant/components/eight_sleep/services.yaml +++ b/homeassistant/components/eight_sleep/services.yaml @@ -1,12 +1,33 @@ heat_set: + name: Heat set description: Set heating/cooling level for eight sleep. fields: duration: + name: Duration description: Duration to heat/cool at the target level in seconds. + required: true example: 3600 + selector: + number: + min: 0 + max: 28800 + unit_of_measurement: seconds entity_id: + name: Entity description: Entity id of the bed state to adjust. + required: true example: sensor.eight_left_bed_state + selector: + entity: + integration: eight_sleep + domain: sensor target: + name: Target description: Target cooling/heating level from -100 to 100. + required: true example: 35 + selector: + number: + min: -100 + max: 100 + unit_of_measurement: '°' diff --git a/homeassistant/components/elkm1/services.yaml b/homeassistant/components/elkm1/services.yaml index 9f9fb2c39e5..f380e4f4b18 100644 --- a/homeassistant/components/elkm1/services.yaml +++ b/homeassistant/components/elkm1/services.yaml @@ -1,126 +1,228 @@ alarm_bypass: + name: Alarm bypass description: Bypass all zones for the area. + target: + entity: + integration: elkm1 + domain: alarm_control_panel fields: - entity_id: - description: Name of alarm control panel to bypass. - example: "alarm_control_panel.main" code: + name: Code description: An code to authorize the bypass of the alarm control panel. + required: true example: 4242 + selector: + text: alarm_clear_bypass: + name: Alarm clear bypass description: Remove bypass on all zones for the area. + target: + entity: + integration: elkm1 + domain: alarm_control_panel fields: - entity_id: - description: Name of alarm control panel to clear bypass. - example: "alarm_control_panel.main" code: + name: Code description: An code to authorize the bypass clear of the alarm control panel. + required: true example: 4242 + selector: + text: alarm_arm_home_instant: + name: Alarm are home instant description: Arm the ElkM1 in home instant mode. + target: + entity: + integration: elkm1 + domain: alarm_control_panel fields: - entity_id: - description: Name of alarm control panel to arm. - example: "alarm_control_panel.main" code: + name: Code description: An code to arm the alarm control panel. + required: true example: 1234 + selector: + text: alarm_arm_night_instant: + name: Alarm arm night instant description: Arm the ElkM1 in night instant mode. + target: + entity: + integration: elkm1 + domain: alarm_control_panel fields: - entity_id: - description: Name of alarm control panel to arm. - example: "alarm_control_panel.main" code: + name: Code description: An code to arm the alarm control panel. + required: true example: 1234 + selector: + text: alarm_arm_vacation: + name: Alarm arm vacation description: Arm the ElkM1 in vacation mode. + target: + entity: + integration: elkm1 + domain: alarm_control_panel fields: - entity_id: - description: Name of alarm control panel to arm. - example: "alarm_control_panel.main" code: + name: Code description: An code to arm the alarm control panel. + required: true example: 1234 + selector: + text: alarm_display_message: + name: Alarm display message description: Display a message on all of the ElkM1 keypads for an area. + target: + entity: + integration: elkm1 + domain: alarm_control_panel fields: - entity_id: - description: Name of alarm control panel to display messages on. - example: "alarm_control_panel.main" clear: - description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2 + name: Clear + description: 0=clear message, 1=clear message with * key, 2=Display until timeout example: 1 + default: 2 + selector: + number: + min: 0 + max: 2 beep: - description: 0=no beep, 1=beep; default 0 + name: Beep + description: 0=no beep, 1=beep example: 1 + default: 0 + selector: + boolean: timeout: - description: Time to display message, 0=forever, max 65535, default 0 + name: Timeout + description: Time to display message, 0=forever, max 65535 example: 4242 + default: 0 + selector: + number: + min: 0 + max: 65535 line1: - description: Up to 16 characters of text (truncated if too long). Default blank. - example: The answer to life, + name: Line 1 + description: Up to 16 characters of text (truncated if too long). + example: The answer to life. + default: '' + selector: + text: line2: - description: Up to 16 characters of text (truncated if too long). Default blank. + name: Line 2 + description: Up to 16 characters of text (truncated if too long). example: the universe, and everything. + default: '' + selector: + text: set_time: + name: Set time description: Set the time for the panel. fields: prefix: + name: Prefix description: Prefix for the panel. example: gatehouse + selector: + text: speak_phrase: + name: Speak phrase description: Speak a phrase. See list of phrases in ElkM1 ASCII Protocol documentation. fields: number: + name: Phrase number description: Phrase number to speak. + required: true example: 42 + selector: + text: + prefix: + name: Prefix + description: Prefix to identify panel when multiple panels configured. + example: gatehouse + default: "" + selector: + text: speak_word: + name: Speak word description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation. fields: number: + name: Word number description: Word number to speak. + required: true example: 142 + selector: + number: + min: 1 + max: 473 + prefix: + name: Prefix + description: Prefix to identify panel when multiple panels configured. + example: gatehouse + default: "" + selector: + text: sensor_counter_refresh: + name: Sensor counter refresh description: Refresh the value of a counter from the panel. - fields: - entity_id: - description: Name of counter to refresh. - example: "sensor.counting_sheep" + target: + entity: + integration: elkm1 + domain: sensor sensor_counter_set: + name: Sensor counter set description: Set the value of a counter on the panel. + target: + entity: + integration: elkm1 + domain: sensor fields: - entity_id: - description: Name of counter to set. - example: "sensor.test42" value: + name: Value description: Value to set the counter to. + required: true example: 4242 + selector: + number: + min: 0 + max: 65536 sensor_zone_bypass: + name: Sensor zone bypass description: Bypass zone. + target: + entity: + integration: elkm1 + domain: sensor fields: - entity_id: - description: Name of zone to bypass. - example: "sensor.window42" code: + name: Code description: An code to authorize the bypass of the zone. + required: true example: 4242 + selector: + text: sensor_zone_trigger: + name: Sensor zone trigger description: Trigger zone. - fields: - entity_id: - description: Name of zone to trigger. - example: "sensor.motion42" + target: + entity: + integration: elkm1 + domain: sensor diff --git a/homeassistant/components/envisalink/services.yaml b/homeassistant/components/envisalink/services.yaml index e9229ad838d..57c64e0c54a 100644 --- a/homeassistant/components/envisalink/services.yaml +++ b/homeassistant/components/envisalink/services.yaml @@ -1,25 +1,47 @@ # Describes the format for available Envisalink services. alarm_keypress: + name: Alarm keypress description: Send custom keypresses to the alarm. fields: entity_id: + name: Entity description: Name of the alarm control panel to trigger. + required: true example: "alarm_control_panel.downstairs" + selector: + entity: + integration: envisalink + domain: alarm_control_panel keypress: + name: Keypress description: "String to send to the alarm panel (1-6 characters)." + required: true example: "*71" + selector: + text: invoke_custom_function: + name: Invoke custom function description: > Allows users with DSC panels to trigger a PGM output (1-4). Note that you need to specify the alarm panel's "code" parameter for this to work. fields: partition: + name: Partition description: > The alarm panel partition to trigger the PGM output on. Typically this is just "1". + required: true example: "1" + selector: + text: pgm: - description: The PGM number to trigger on the alarm panel. This will be 1-4. + name: PGM + description: The PGM number to trigger on the alarm panel. + required: true example: "2" + selector: + number: + min: 1 + max: 4 diff --git a/homeassistant/components/epson/services.yaml b/homeassistant/components/epson/services.yaml index a463cd35512..37add1bc202 100644 --- a/homeassistant/components/epson/services.yaml +++ b/homeassistant/components/epson/services.yaml @@ -1,9 +1,15 @@ select_cmode: + name: Select color mode description: Select Color mode of Epson projector + target: + entity: + integration: epson + domain: media_player fields: - entity_id: - description: Name of projector - example: "media_player.epson_projector" cmode: + name: Color mode description: Name of Cmode + required: true example: "cinema" + selector: + text: diff --git a/homeassistant/components/evohome/services.yaml b/homeassistant/components/evohome/services.yaml index 04f7a3ac2aa..5a6993f6ad7 100644 --- a/homeassistant/components/evohome/services.yaml +++ b/homeassistant/components/evohome/services.yaml @@ -2,52 +2,95 @@ # Describes the format for available services set_system_mode: + name: Set system mode description: >- Set the system mode, either indefinitely, or for a specified period of time, after which it will revert to Auto. Not all systems support all modes. fields: mode: - description: "One of: Auto, AutoWithEco, Away, DayOff, HeatingOff, or Custom." + name: Mode + description: "Mode to set thermostat." example: Away + selector: + select: + options: + - 'Auto' + - 'AutoWithEco' + - 'Away' + - 'Custom' + - 'DayOff' + - 'HeatingOff' period: + name: Period description: >- A period of time in days; used only with Away, DayOff, or Custom. The system will revert to Auto at midnight (up to 99 days, today is day 1). example: '{"days": 28}' + selector: + object: duration: + name: Duration description: The duration in hours; used only with AutoWithEco (up to 24 hours). example: '{"hours": 18}' + selector: + object: reset_system: + name: Reset system description: >- Set the system to Auto mode and reset all the zones to follow their schedules. Not all Evohome systems support this feature (i.e. AutoWithReset mode). refresh_system: + name: Refresh system description: >- Pull the latest data from the vendor's servers now, rather than waiting for the next scheduled update. set_zone_override: + name: Set zone override description: >- Override a zone's setpoint, either indefinitely, or for a specified period of time, after which it will revert to following its schedule. fields: entity_id: + name: Entity description: The entity_id of the Evohome zone. + required: true example: climate.bathroom + selector: + entity: + integration: evohome + domain: climate setpoint: + name: Setpoint description: The temperature to be used instead of the scheduled setpoint. + required: true example: 5.0 + selector: + number: + min: 4.0 + max: 35.0 + step: 0.1 duration: + name: Duration description: >- The zone will revert to its schedule after this time. If 0 the change is until the next scheduled setpoint. example: '{"minutes": 135}' + selector: + object: clear_zone_override: + name: Clear zone override description: Set a zone to follow its schedule. fields: entity_id: + name: Entity description: The entity_id of the zone. + required: true example: climate.bathroom + selector: + entity: + integration: evohome + domain: climate From bd443af6a29ad6e3115fb1837fdc040e9256f3d2 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 15 May 2021 04:30:18 -0400 Subject: [PATCH 429/852] Add targets and selectors for services (N-O) (#50608) --- .../components/ness_alarm/services.yaml | 19 ++- homeassistant/components/nest/services.yaml | 38 ++++- .../components/netgear_lte/services.yaml | 37 ++++- homeassistant/components/nexia/services.yaml | 35 ++++- .../components/nissan_leaf/services.yaml | 10 ++ homeassistant/components/notify/services.yaml | 12 +- homeassistant/components/nuki/services.yaml | 12 +- homeassistant/components/nx584/services.yaml | 28 +++- homeassistant/components/nzbget/services.yaml | 12 +- homeassistant/components/ombi/services.yaml | 25 ++- .../components/omnilogic/services.yaml | 14 +- homeassistant/components/onvif/services.yaml | 69 +++++++-- .../components/openhome/services.yaml | 14 +- .../components/opentherm_gw/services.yaml | 145 +++++++++++++++++- homeassistant/components/openuv/services.yaml | 3 + homeassistant/components/ozw/services.yaml | 92 +++++++++-- 16 files changed, 508 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/ness_alarm/services.yaml b/homeassistant/components/ness_alarm/services.yaml index eb35c48b9f4..8e4219a7921 100644 --- a/homeassistant/components/ness_alarm/services.yaml +++ b/homeassistant/components/ness_alarm/services.yaml @@ -1,19 +1,34 @@ # Describes the format for available ness alarm services aux: + name: Aux description: Trigger an aux output. fields: output_id: - description: The aux output you wish to change. A number from 1-4. + name: Output ID + description: The aux output you wish to change. + required: true example: 1 + selector: + number: + min: 1 + max: 4 state: - description: The On/Off State, represented as true/false. Default is true. If P14xE 8E is enabled then a value of true will pulse output x for the time specified in P14(x+4)E. + name: State + description: The On/Off State. If P14xE 8E is enabled then a value of true will pulse output x for the time specified in P14(x+4)E. example: true default: true + selector: + boolean: panic: + name: Panic description: Trigger a panic fields: code: + name: Code description: The user code to use to trigger the panic. + required: true example: 1234 + selector: + text: diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index e10e6264643..b2ae06c3430 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -1,37 +1,71 @@ # Describes the format for available Nest services set_away_mode: + name: Set away mode description: Set the away mode for a Nest structure. fields: away_mode: - description: New mode to set. Valid modes are "away" or "home". + name: Away mode + description: New mode to set. example: "away" + required: true + selector: + select: + options: + - 'away' + - 'home' structure: + name: Structure description: Name(s) of structure(s) to change. Defaults to all structures if not specified. example: "Apartment" + selector: + object: set_eta: + name: Set estimated time of arrival description: Set or update the estimated time of arrival window for a Nest structure. fields: eta: + name: ETA description: Estimated time of arrival from now. example: "00:10:30" + required: true + selector: + time: eta_window: - description: Estimated time of arrival window. Default is 1 minute. + name: ETA window + description: Estimated time of arrival window. example: "00:05" + default: "00:01" + selector: + time: trip_id: + name: Trip ID description: Unique ID for the trip. Default is auto-generated using a timestamp. example: "Leave Work" + selector: + text: structure: + name: Structure description: Name(s) of structure(s) to change. Defaults to all structures if not specified. example: "Apartment" + selector: + object: cancel_eta: + name: Cancel ETA description: Cancel an existing estimated time of arrival window for a Nest structure. fields: trip_id: + name: Trip ID description: Unique ID for the trip. + required: true example: "Leave Work" + selector: + text: structure: + name: Structure description: Name(s) of structure(s) to change. Defaults to all structures if not specified. example: "Apartment" + selector: + object: diff --git a/homeassistant/components/netgear_lte/services.yaml b/homeassistant/components/netgear_lte/services.yaml index 564fb914cf9..116c2f61a2e 100644 --- a/homeassistant/components/netgear_lte/services.yaml +++ b/homeassistant/components/netgear_lte/services.yaml @@ -1,36 +1,69 @@ delete_sms: + name: Delete SMS description: Delete messages from the modem inbox. fields: host: + name: Host description: The modem that should have a message deleted. example: 192.168.5.1 + selector: + text: sms_id: + name: SMS ID description: Integer or list of integers with inbox IDs of messages to delete. + required: true example: 7 + selector: + object: set_option: + name: Set option description: Set options on the modem. fields: host: + name: Host description: The modem to set options on. example: 192.168.5.1 + selector: + text: failover: - description: Failover mode, auto/wire/mobile. + name: Failover + description: Failover mode. example: auto + selector: + select: + options: + - 'auto' + - 'mobile' + - 'wire' autoconnect: - description: Auto-connect mode, never/home/always. + name: Auto-connect + description: Auto-connect mode. example: home + selector: + select: + options: + - 'always' + - 'home' + - 'never' connect_lte: + name: Connect LTE description: Ask the modem to establish the LTE connection. fields: host: + name: Host description: The modem that should connect. example: 192.168.5.1 + selector: + text: disconnect_lte: + name: Disconnect LTE description: Ask the modem to close the LTE connection. fields: host: description: The modem that should disconnect. example: 192.168.5.1 + selector: + text: diff --git a/homeassistant/components/nexia/services.yaml b/homeassistant/components/nexia/services.yaml index 23a9498746b..0b822dce186 100644 --- a/homeassistant/components/nexia/services.yaml +++ b/homeassistant/components/nexia/services.yaml @@ -1,19 +1,38 @@ set_aircleaner_mode: + name: Set air cleaner mode description: "The air cleaner mode." + target: + entity: + integration: nexia + domain: climate fields: - entity_id: - description: "This setting will affect all zones connected to the thermostat." - example: climate.master_bedroom aircleaner_mode: - description: 'The air cleaner mode to set. Options include "auto", "quick", or "allergy".' + name: Air cleaner mode + description: 'The air cleaner mode to set.' + required: true example: allergy + selector: + select: + options: + - 'allergy' + - 'auto' + - 'quick' set_humidify_setpoint: + name: Set humidify set point description: "The humidification set point." + target: + entity: + integration: nexia + domain: climate fields: - entity_id: - description: "This setting will affect all zones connected to the thermostat." - example: climate.master_bedroom humidity: - description: "The humidification setpoint as an int, range 35-65." + name: Humidify + description: "The humidification setpoint." + required: true example: 45 + selector: + number: + min: 35 + max: 65 + unit_of_measurement: '%' diff --git a/homeassistant/components/nissan_leaf/services.yaml b/homeassistant/components/nissan_leaf/services.yaml index 096f4f5b8b4..901e70de414 100644 --- a/homeassistant/components/nissan_leaf/services.yaml +++ b/homeassistant/components/nissan_leaf/services.yaml @@ -1,20 +1,30 @@ # Describes the format for available services for nissan_leaf start_charge: + name: Start charge description: > Start the vehicle charging. It must be plugged in first! fields: vin: + name: VIN description: > The vehicle identification number (VIN) of the vehicle, 17 characters + required: true example: WBANXXXXXX1234567 + selector: + text: update: + name: Update description: > Fetch the last state of the vehicle of all your accounts, requesting an update from of the state from the car if possible. fields: vin: + name: VIN description: > The vehicle identification number (VIN) of the vehicle, 17 characters + required: true example: WBANXXXXXX1234567 + selector: + text: diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index f6918b6c09c..6bbd15c94ca 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -7,12 +7,13 @@ notify: message: name: Message description: Message body of the notification. + required: true example: The garage door has been open for 10 minutes. selector: text: title: name: Title - description: Optional title for your notification. + description: Title for your notification. example: "Your Garage Door Friend" selector: text: @@ -21,6 +22,8 @@ notify: An array of targets to send the notification to. Optional depending on the platform. example: platform specific + selector: + object: data: name: Data description: @@ -36,10 +39,15 @@ persistent_notification: fields: message: description: Message body of the notification. + required: true example: The garage door has been open for 10 minutes. + selector: + text: title: - description: Optional title for your notification. + description: Title for your notification. example: "Your Garage Door Friend" + selector: + text: apns_register: name: Register APNS device diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml index 9e3be794cb7..85e0e67ea50 100644 --- a/homeassistant/components/nuki/services.yaml +++ b/homeassistant/components/nuki/services.yaml @@ -1,9 +1,15 @@ lock_n_go: + name: Lock 'n' go description: "Nuki Lock 'n' Go" + target: + entity: + integration: nuki + domain: lock fields: - entity_id: - description: Entity id of the Nuki lock. - example: "lock.front_door" unlatch: + name: unlatch description: Whether to unlatch the lock. example: false + default: false + selector: + boolean: diff --git a/homeassistant/components/nx584/services.yaml b/homeassistant/components/nx584/services.yaml index 13f5da8db25..25ef4c20702 100644 --- a/homeassistant/components/nx584/services.yaml +++ b/homeassistant/components/nx584/services.yaml @@ -1,21 +1,37 @@ # Describes the format for available nx584 services bypass_zone: + name: Bypass zone description: Bypass a zone. + target: + entity: + integration: nx584 + domain: alarm_control_panel fields: - entity_id: - description: Name of the alarm control panel which state has to be updated. - example: "alarm_control_panel.downstairs" zone: + name: Zone description: The number of the zone to be bypassed. + required: true example: "1" + selector: + number: + min: 1 + max: 255 unbypass_zone: + name: Un-bypass zone description: Un-Bypass a zone. + target: + entity: + integration: nx584 + domain: alarm_control_panel fields: - entity_id: - description: Name of the alarm control panel which state has to be updated. - example: "alarm_control_panel.downstairs" zone: + name: Zone description: The number of the zone to be un-bypassed. + required: true example: "1" + selector: + number: + min: 1 + max: 255 diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml index 88a6267860e..290b3761ab8 100644 --- a/homeassistant/components/nzbget/services.yaml +++ b/homeassistant/components/nzbget/services.yaml @@ -1,14 +1,24 @@ # Describes the format for available nzbget services pause: + name: Pause description: Pause download queue. resume: + name: Resume description: Resume download queue. set_speed: + name: Set speed description: Set download speed limit fields: speed: - description: Speed limit in kB/s. 0 is unlimited. + name: Speed + description: Speed limit. 0 is unlimited. example: 1000 + default: 1000 + selector: + number: + min: 0 + max: 1000000 + unit_of_measurement: 'kB/s' diff --git a/homeassistant/components/ombi/services.yaml b/homeassistant/components/ombi/services.yaml index 6c7f5ced489..c6f154d073e 100644 --- a/homeassistant/components/ombi/services.yaml +++ b/homeassistant/components/ombi/services.yaml @@ -1,24 +1,47 @@ # Ombi services.yaml entries submit_movie_request: + name: Sumbit movie request description: Searches for a movie and requests the first result. fields: name: + name: Name description: Search parameter + required: true example: "beverly hills cop" + selector: + text: submit_tv_request: + name: Submit tv request description: Searches for a TV show and requests the first result. fields: name: + name: Name description: Search parameter + required: true example: "breaking bad" + selector: + text: season: - description: Which season(s) to request (first, latest or all) + name: Season + description: Which season(s) to request. example: "latest" + default: latest + selector: + select: + options: + - 'all' + - 'first' + - 'latest' submit_music_request: + name: Submit music request description: Searches for a music album and requests the first result. fields: name: + name: Name description: Search parameter + required: true example: "nevermind" + selector: + text: diff --git a/homeassistant/components/omnilogic/services.yaml b/homeassistant/components/omnilogic/services.yaml index 32ad2716ade..b886fe7f7f7 100644 --- a/homeassistant/components/omnilogic/services.yaml +++ b/homeassistant/components/omnilogic/services.yaml @@ -1,9 +1,17 @@ set_pump_speed: + name: Set pump speed description: Set the run speed of a variable speed pump. + target: + entity: + integration: omnilogic + domain: switch fields: - entity_id: - description: Target switch entity - example: switch.pool_pump speed: + name: Speed description: Speed for the VSP between min and max speed. + required: true example: 85 + selector: + number: + min: 0 + max: 100000 diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml index bed426e9924..ee5af2ae77e 100644 --- a/homeassistant/components/onvif/services.yaml +++ b/homeassistant/components/onvif/services.yaml @@ -1,34 +1,85 @@ ptz: + name: PTZ description: If your ONVIF camera supports PTZ, you will be able to pan, tilt or zoom your camera. + target: + entity: + integration: onvif + domain: camera fields: - entity_id: - description: "String or list of strings that point at entity_ids of cameras. Else targets all." - example: "camera.living_room_camera" tilt: - description: "Tilt direction. Allowed values: UP, DOWN" + name: Tilt + description: "Tilt direction." example: "UP" + selector: + select: + options: + - 'DOWN' + - 'UP' pan: - description: "Pan direction. Allowed values: RIGHT, LEFT" + name: Pan + description: "Pan direction." example: "RIGHT" + selector: + select: + options: + - 'LEFT' + - 'RIGHT' zoom: - description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT" + name: Zoom + description: "Zoom." example: "ZOOM_IN" + selector: + select: + options: + - 'ZOOM_IN' + - 'ZOOM_OUT' distance: - description: "Distance coefficient. Sets how much PTZ should be executed in one request. Allowed values: floating point numbers, 0 to 1" + name: Distance + description: "Distance coefficient. Sets how much PTZ should be executed in one request." default: 0.1 example: 0.1 + selector: + number: + min: 0 + max: 1 + step: 0.01 speed: - description: "Speed coefficient. Sets how fast PTZ will be executed. Allowed values: floating point numbers, 0 to 1" + name: Speed + description: "Speed coefficient. Sets how fast PTZ will be executed." default: 0.5 example: 0.5 + selector: + number: + min: 0 + max: 1 + step: 0.01 continuous_duration: + name: Continuous duration description: "Set ContinuousMove delay in seconds before stopping the move" default: 0.5 example: 0.5 + selector: + number: + min: 0 + max: 1 + step: 0.01 preset: + name: Preset description: "PTZ preset profile token. Sets the preset profile token which is executed with GotoPreset" example: "1" + default: "0" + selector: + text: move_mode: - description: "PTZ moving mode. One of ContinuousMove, RelativeMove, AbsoluteMove, GotoPreset, or Stop" + name: Move Mode + description: "PTZ moving mode." default: "RelativeMove" example: "ContinuousMove" + selector: + select: + options: + - 'AbsoluteMove' + - 'ContinuousMove' + - 'GotoPreset' + - 'RelativeMove' + - 'Stop' diff --git a/homeassistant/components/openhome/services.yaml b/homeassistant/components/openhome/services.yaml index e8ae5fb55da..29b07500c3f 100644 --- a/homeassistant/components/openhome/services.yaml +++ b/homeassistant/components/openhome/services.yaml @@ -1,11 +1,19 @@ # Describes the format for available openhome services invoke_pin: + name: Invoke PIN description: Invoke a pin on the specified device. + target: + entity: + integration: openhome + domain: media_player fields: - entity_id: - description: The name of the openhome device to invoke the pin on - example: media_player.main_room pin: + name: PIN description: Which pin to invoke + required: true example: 4 + selector: + number: + min: 0 + max: 1000 diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index 8a1bddc2100..fe3ecc157c5 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -1,13 +1,19 @@ # Describes the format for available opentherm_gw services reset_gateway: + name: Reset gateway description: Reset the OpenTherm Gateway. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: set_central_heating_ovrd: + name: Set central heating override description: > Set the central heating override option on the gateway. When overriding the control setpoint (via a set_control_setpoint service call with a value other than 0), the gateway automatically enables the central heating override to start heating. @@ -16,49 +22,83 @@ set_central_heating_ovrd: You will only need this if you are writing your own software thermostat. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" ch_override: + name: Central heating override description: > The desired boolean value for the central heating override. + required: true example: "on" + selector: + boolean: set_clock: + name: Set clock description: Set the clock and day of the week on the connected thermostat. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" date: + name: Date description: Optional date from which the day of the week will be extracted. Defaults to today. example: "2018-10-23" + selector: + text: time: + name: Name description: Optional time in 24h format which will be provided to the thermostat. Defaults to the current time. example: "19:34" + selector: + text: set_control_setpoint: + name: Set control set point description: > Set the central heating control setpoint override on the gateway. You will only need this if you are writing your own software thermostat. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: temperature: + name: Temperature description: > The central heating setpoint to set on the gateway. Values between 0 and 90 are accepted, but not all boilers support this range. A value of 0 disables the central heating setpoint override. + required: true example: "37.5" + selector: + number: + min: 0 + max: 90 + step: 0.1 + unit_of_measurement: '°' set_hot_water_ovrd: + name: Set hot water override description: > Set the domestic hot water enable option on the gateway. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: dhw_override: + name: Domestic hot water override description: > Control the domestic hot water enable option. If the boiler has been configured to let the room unit control when to keep a @@ -66,88 +106,187 @@ set_hot_water_ovrd: that. Value should be 0 or 1 to enable the override in off or on state, or "A" to disable the override. + required: true example: "1" + selector: + text: set_hot_water_setpoint: + name: Set hot water set point description: > Set the domestic hot water setpoint on the gateway. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: temperature: + name: Temperature description: > The domestic hot water setpoint to set on the gateway. Not all boilers support this feature. Values between 0 and 90 are accepted, but not all boilers support this range. Check the values of the slave_dhw_min_setp and slave_dhw_max_setp sensors to see the supported range on your boiler. example: "60" + selector: + number: + min: 0 + max: 90 + step: 0.1 + unit_of_measurement: '°' set_gpio_mode: + name: Set gpio mode description: Change the function of the GPIO pins of the gateway. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: id: - description: The ID of the GPIO pin. Either "A" or "B". + name: ID + description: The ID of the GPIO pin. + required: true example: "B" + selector: + select: + options: + - 'A' + - 'B' mode: + name: Mode description: > 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/integrations/opentherm_gw/#gpio-modes for an explanation of the values. + required: true example: "5" + selector: + number: + min: 0 + max: 7 set_led_mode: + name: Set LED mode description: Change the function of the LEDs of the gateway. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: id: - description: The ID of the LED. Possible values are "A" through "F". + name: ID + description: The ID of the LED. + required: true example: "C" + selector: + select: + options: + - 'A' + - 'B' + - 'C' + - 'D' + - 'E' + - 'F' mode: + name: 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/integrations/opentherm_gw/#led-modes for an explanation of the values. + required: true example: "F" + selector: + select: + options: + - 'B' + - 'C' + - 'E' + - 'F' + - 'H' + - 'M' + - 'O' + - 'P' + - 'R' + - 'T' + - 'W' + - 'X' set_max_modulation: + name: Set max modulation description: > Override the maximum relative modulation level. You will only need this if you are writing your own software thermostat. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: level: + name: Level description: > The modulation level to provide to the gateway. - Values between 0 and 100 will set the modulation level. Provide a value of -1 to clear the override and forward the value from the thermostat again. + required: true example: "42" + selector: + number: + min: -1 + max: 100 set_outside_temperature: + name: Set outside temperature description: > Provide an outside temperature to the thermostat. If your thermostat is unable to display an outside temperature and does not support OTC (Outside Temperature Correction), this has no effect. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: temperature: + name: Temperature description: > The temperature to provide to the thermostat. Values between -40.0 and 64.0 will be accepted, but not all thermostats can display the full range. Any value above 64.0 will clear a previously configured value (suggestion: 99) + required: true example: "-2.3" + selector: + number: + min: -40 + max: 99 set_setback_temperature: + name: Set setback temperature description: Configure the setback temperature to be used with the GPIO away mode function. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: temperature: + name: Temperature description: The setback temperature to configure on the gateway. Values between 0.0 and 30.0 are accepted. + required: true example: "16.0" + selector: + number: + min: 0 + max: 30 + step: 0.1 diff --git a/homeassistant/components/openuv/services.yaml b/homeassistant/components/openuv/services.yaml index ea353e84892..e4886dfa7d8 100644 --- a/homeassistant/components/openuv/services.yaml +++ b/homeassistant/components/openuv/services.yaml @@ -1,9 +1,12 @@ # Describes the format for available OpenUV services update_data: + name: Update data description: Request new data from OpenUV. Consumes two API calls. update_uv_index_data: + name: Update UV index data description: Request new UV index data from OpenUV. update_protection_data: + name: Update protection data description: Request new protection window data from OpenUV. diff --git a/homeassistant/components/ozw/services.yaml b/homeassistant/components/ozw/services.yaml index 641c086f524..2919aceceb6 100644 --- a/homeassistant/components/ozw/services.yaml +++ b/homeassistant/components/ozw/services.yaml @@ -1,58 +1,126 @@ # Describes the format for available Z-Wave services add_node: + name: Add node description: Add a new node to the Z-Wave network. fields: secure: + name: Secure description: Add the new node with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. + default: false + selector: + boolean: instance_id: - description: (Optional) The OZW Instance/Controller to use, defaults to 1. + name: Instance ID + description: The OZW Instance/Controller to use. + default: 1 + selector: + number: + min: 1 + max: 255 remove_node: + name: Remove node description: Remove a node from the Z-Wave network. Will set the controller into exclusion mode. fields: instance_id: - description: (Optional) The OZW Instance/Controller to use, defaults to 1. + name: Instance ID + description: The OZW Instance/Controller to use. + default: 1 + selector: + number: + min: 1 + max: 255 cancel_command: + name: Cancel command description: Cancel a pending add or remove node command. fields: instance_id: - description: (Optional) The OZW Instance/Controller to use, defaults to 1. + name: Instance ID + description: The OZW Instance/Controller to use. + default: 1 + selector: + number: + min: 1 + max: 255 set_config_parameter: + name: Set config parameter description: Set a config parameter to a node on the Z-Wave network. fields: node_id: - description: Node id of the device to set config parameter to (integer). + name: Node ID + description: Node id of the device to set config parameter to. + required: true example: 10 + selector: + number: + min: 1 + max: 255 parameter: - description: Parameter number to set (integer). + name: Parameter + description: Parameter number to set. + required: true example: 8 + selector: + number: + min: 1 + max: 255 value: + name: Value description: Value to set for parameter. (String value for list and bool parameters, integer for others). + required: true example: 50268673 + selector: + text: instance_id: - description: (Optional) The OZW Instance/Controller to use, defaults to 1. + name: Instance ID + description: The OZW Instance/Controller to use. + default: 1 + selector: + number: + min: 1 + max: 255 clear_usercode: + name: Clear usercode description: Clear a usercode from lock. + target: + entity: + integration: ozw + domain: lock fields: - entity_id: - description: Lock entity_id. - example: lock.front_door_locked code_slot: + name: Code slot description: Code slot to clear code from. + required: true example: 1 + selector: + number: + min: 1 + max: 255 set_usercode: + name: Set usercode description: Set a usercode to lock. + target: + entity: + integration: ozw + domain: lock fields: - entity_id: - description: Lock entity_id. - example: lock.front_door_locked code_slot: + name: Code slot description: Code slot to set the code. + required: true example: 1 + selector: + number: + min: 1 + max: 255 usercode: + name: Usercode description: Code to set. + required: true example: 1234 + selector: + text: From 5d6f4068d3c58ba2a784513143f82b69a03b6a55 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 15 May 2021 04:49:10 -0400 Subject: [PATCH 430/852] Add targets and selectors for services (U-W) (#50630) Co-authored-by: Franck Nijhof --- .../components/universal/services.yaml | 1 + homeassistant/components/upb/services.yaml | 169 ++++++--- .../components/utility_meter/services.yaml | 4 +- homeassistant/components/vallox/services.yaml | 46 ++- homeassistant/components/velbus/services.yaml | 13 +- homeassistant/components/velux/services.yaml | 1 + homeassistant/components/vesync/services.yaml | 1 + homeassistant/components/vicare/services.yaml | 23 +- .../components/water_heater/services.yaml | 47 ++- homeassistant/components/wemo/services.yaml | 28 +- homeassistant/components/wink/services.yaml | 351 ++++++++++++++---- 11 files changed, 523 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/universal/services.yaml b/homeassistant/components/universal/services.yaml index 8b515151fd9..e0af28bf3a6 100644 --- a/homeassistant/components/universal/services.yaml +++ b/homeassistant/components/universal/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all universal entities diff --git a/homeassistant/components/upb/services.yaml b/homeassistant/components/upb/services.yaml index 661c95ba991..6c5c1a8b606 100644 --- a/homeassistant/components/upb/services.yaml +++ b/homeassistant/components/upb/services.yaml @@ -1,88 +1,161 @@ light_fade_start: + name: Start light fade description: Start fading a light either up or down from current brightness. + target: + entity: + integration: upb + domain: light fields: - entity_id: - description: Name(s) of lights to start fading - example: "light.kitchen" brightness: - description: Number between 0 and 255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness. - example: 142 + name: Brightness + description: Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness. + selector: + number: + min: 0 + max: 255 brightness_pct: - description: Number between 0 and 100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness. - example: 42 + name: Brightness percentage + description: Number indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' rate: + name: Rate description: Rate for light to transition to new brightness - example: 3 + default: -1 + selector: + number: + min: -1 + max: 3600 + step: 0.01 + unit_of_measurement: seconds light_fade_stop: + name: Stop light fade description: Stop a light fade. - fields: - entity_id: - description: Name(s) of lights to stop fadding - example: "light.kitchen, light.family_room" + target: + entity: + integration: upb + domain: light light_blink: + name: Blink light description: Blink a light + target: + entity: + integration: upb + domain: light fields: - entity_id: - description: Name(s) of lights to start fading - example: "light.kitchen" rate: - description: Number of seconds between 0 and 4.25 that the link flashes on. - example: 4.2 + name: Rate + description: Amount of time that the link flashes on. + default: 0.5 + selector: + number: + min: 0 + max: 4.25 + step: 0.01 + unit_of_measurement: seconds link_deactivate: + name: Deactivate link description: Deactivate a UPB scene. - fields: - entity_id: - description: Name(s) of scenes to deactivate - example: "scene.hygge" + target: + entity: + integration: upb + domain: light link_goto: + name: Go to link description: Set scene to brightness. + target: + entity: + integration: upb + domain: scene fields: - entity_id: - description: Name(s) of scenes to deactivate - example: "scene.hygge" brightness: - description: Number between 0 and 255 indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness. - example: 120 + name: Brightness + description: Number indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness. + selector: + number: + min: 0 + max: 255 brightness_pct: - description: Number between 0 and 100 indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness. - example: 42 + name: Brightness percentage + description: Number indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' rate: - description: Rate in seconds for scene to transition to new brightness - example: 3.42 + name: Rate + description: Amount of time for scene to transition to new brightness + selector: + number: + min: -1 + max: 3600 + step: 0.01 + unit_of_measurement: seconds link_fade_start: + name: Start link fade description: Start fading a link either up or down from current brightness. + target: + entity: + integration: upb + domain: scene fields: - entity_id: - description: Name(s) of links to start fading - example: "scene.party" brightness: - description: Number between 0 and 255 indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness. - example: 142 + name: Brightness + description: Number indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness. + selector: + number: + min: 0 + max: 255 brightness_pct: - description: Number between 0 and 100 indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness. - example: 42 + name: Brightness percentage + description: Number indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' rate: - description: Rate in seconds for scene to transition to new brightness - example: 3.42 + name: Rate + description: Amount of time for scene to transition to new brightness + selector: + number: + min: -1 + max: 3600 + step: 0.01 + unit_of_measurement: seconds link_fade_stop: + name: Stop link fade description: Stop a link fade. - fields: - entity_id: - description: Name(s) of links to stop fadding - example: "scene.dining, scene.no_tv" + target: + entity: + integration: upb + domain: scene link_blink: + name: Blink link description: Blink a link. + target: + entity: + integration: upb + domain: scene fields: - entity_id: - description: Name(s) of links to start fading - example: "scene.hygge" blink_rate: - description: Number of seconds between 0 and 4.25 that the link flashes on. - example: 1.5 + name: Blink rate + description: Amount of time that the link flashes on. + default: 0.5 + selector: + number: + min: 0 + max: 4.25 + step: 0.01 + unit_of_measurement: seconds diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index fac9dadfa29..b2e2a025c47 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -2,7 +2,7 @@ reset: name: Reset - description: Resets the counter of an utility meter. + description: Resets the counter of a utility meter. target: next_tariff: @@ -12,7 +12,7 @@ next_tariff: select_tariff: name: Select Tariff - description: Selects the current tariff of an utility meter. + description: Selects the current tariff of a utility meter. target: fields: tariff: diff --git a/homeassistant/components/vallox/services.yaml b/homeassistant/components/vallox/services.yaml index 65757b70364..98d7abac249 100644 --- a/homeassistant/components/vallox/services.yaml +++ b/homeassistant/components/vallox/services.yaml @@ -1,27 +1,57 @@ set_profile: + name: Set profile description: Set the ventilation profile. fields: profile: - description: "Set to any of: Home, Away, Boost, Fireplace" - example: Away + name: Profile + description: "Set profile." + required: true + selector: + select: + options: + - 'Away' + - 'Boost' + - 'Fireplace' + - 'Home' set_profile_fan_speed_home: + name: Set profile fan speed hom description: Set the fan speed of the Home profile. fields: fan_speed: - description: Fan speed in %. Integer, between 0 and 100. - example: 50 + name: Fan speed + description: Fan speed. + required: true + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' set_profile_fan_speed_away: + name: Set profile fan speed away description: Set the fan speed of the Away profile. fields: fan_speed: - description: Fan speed in %. Integer, between 0 and 100. - example: 25 + name: Fan speed + description: Fan speed. + required: true + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' set_profile_fan_speed_boost: + name: Set profile fan speed boost description: Set the fan speed of the Boost profile. fields: fan_speed: - description: Fan speed in %. Integer, between 0 and 100. - example: 65 + name: Fan speed + description: Fan speed. + required: true + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' diff --git a/homeassistant/components/velbus/services.yaml b/homeassistant/components/velbus/services.yaml index 490c746fa74..9fed172fad4 100644 --- a/homeassistant/components/velbus/services.yaml +++ b/homeassistant/components/velbus/services.yaml @@ -1,18 +1,29 @@ sync_clock: + name: Sync clock description: Sync the velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink set_memo_text: + name: Set memo text description: > Set the memo text to the display of modules like VMBGPO, VMBGPOD Be sure the page(s) of the module is configured to display the memo text. fields: address: + name: Address description: > The module address in decimal format. The decimal addresses are displayed in front of the modules listed at the integration page. - example: "11" + required: true + selector: + number: + min: 0 + max: 255 memo_text: + name: Memo text description: > The actual text to be displayed. Text is limited to 64 characters. example: "Do not forget trash" + default: '' + selector: + text: diff --git a/homeassistant/components/velux/services.yaml b/homeassistant/components/velux/services.yaml index 2460db0bbb0..46aee795890 100644 --- a/homeassistant/components/velux/services.yaml +++ b/homeassistant/components/velux/services.yaml @@ -1,4 +1,5 @@ # Velux Integration services reboot_gateway: + name: Reboot gateway description: Reboots the KLF200 Gateway. diff --git a/homeassistant/components/vesync/services.yaml b/homeassistant/components/vesync/services.yaml index dec19740aef..da264ea3b5d 100644 --- a/homeassistant/components/vesync/services.yaml +++ b/homeassistant/components/vesync/services.yaml @@ -1,2 +1,3 @@ update_devices: + name: Update devices description: Add new VeSync devices to Home Assistant diff --git a/homeassistant/components/vicare/services.yaml b/homeassistant/components/vicare/services.yaml index 2efaf530a9c..94146c4250e 100644 --- a/homeassistant/components/vicare/services.yaml +++ b/homeassistant/components/vicare/services.yaml @@ -1,9 +1,22 @@ set_vicare_mode: + name: Set vicare mode description: Set a ViCare mode. + target: + entity: + integration: vicare + domain: climate fields: - entity_id: - description: Name(s) of vicare climate entities. - example: "climate.vicare_heating" vicare_mode: - description: ViCare mode. One of "dhw", "dhwAndHeating", "heating", "dhwAndHeatingCooling", "forcedReduced", "forcedNormal" or "standby" - example: "dhw" + name: Vicare Mode + description: ViCare mode. + required: true + selector: + select: + options: + - 'dhw' + - 'dhwAndHeating' + - 'dhwAndHeatingCooling' + - 'forcedNormal' + - 'forcedReduced' + - 'heating' + - 'standby' diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml index 8aee796b9cb..3cbd9446d38 100644 --- a/homeassistant/components/water_heater/services.yaml +++ b/homeassistant/components/water_heater/services.yaml @@ -1,31 +1,48 @@ # Describes the format for available water_heater services set_away_mode: + name: Set away mode description: Turn away mode on/off for water_heater device. + target: fields: - entity_id: - description: Name(s) of entities to change. - example: "water_heater.water_heater" away_mode: + name: Away mode description: New value of away mode. - example: true + required: true + selector: + boolean: set_temperature: + name: Set temperature description: Set target temperature of water_heater device. + target: fields: - entity_id: - description: Name(s) of entities to change. - example: "water_heater.water_heater" temperature: + name: Temperature description: New target temperature for water heater. - example: 25 - -set_operation_mode: - description: Set operation mode for water_heater device. - fields: - entity_id: - description: Name(s) of entities to change. - example: "water_heater.water_heater" + required: true + selector: + number: + min: 0 + max: 100 + step: 0.5 + unit_of_measurement: '°' operation_mode: + name: Operation mode description: New value of operation mode. example: eco + selector: + text: + +set_operation_mode: + name: Set operation mode + description: Set operation mode for water_heater device. + target: + fields: + operation_mode: + name: Operation mode + description: New value of operation mode. + required: true + example: eco + selector: + text: diff --git a/homeassistant/components/wemo/services.yaml b/homeassistant/components/wemo/services.yaml index c47d666f5c1..e86366b6a5c 100644 --- a/homeassistant/components/wemo/services.yaml +++ b/homeassistant/components/wemo/services.yaml @@ -1,16 +1,26 @@ set_humidity: + name: Set humidity description: Set the target humidity of WeMo humidifier devices. + target: + entity: + integration: wemo + domain: fan fields: - entity_id: - description: Names of the WeMo humidifier entities (1 or more entity_ids are required). - example: "fan.wemo_humidifier" target_humidity: - description: Target humidity. This is a float value between 0 and 100, but will be mapped to the humidity levels that WeMo humidifiers support (45, 50, 55, 60, and 100/Max) by rounding the value down to the nearest supported value. - example: 56.5 + name: Target humidity + description: Target humidity. + required: true + selector: + number: + min: 0 + max: 100 + step: 5 + unit_of_measurement: '%' reset_filter_life: + name: Reset filter life description: Reset the WeMo Humidifier's filter life to 100%. - fields: - entity_id: - description: Names of the WeMo humidifier entities (1 or more entity_ids are required). - example: "fan.wemo_humidifier" + target: + entity: + integration: wemo + domain: fan diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml index ac050fd0087..f7f21125f27 100644 --- a/homeassistant/components/wink/services.yaml +++ b/homeassistant/components/wink/services.yaml @@ -1,227 +1,432 @@ # Describes the format for available Wink services pair_new_device: + name: Pair new device description: Pair a new device to a Wink Hub. fields: hub_name: + name: Hub name description: The name of the hub to pair a new device to. + required: true example: "My hub" + selector: + text: pairing_mode: - description: One of ["zigbee", "zwave", "zwave_exclusion", "zwave_network_rediscovery", "lutron", "bluetooth", "kidde"]. - example: "zigbee" + name: Pairing mode + description: Mode. + required: true + selector: + select: + options: + - 'bluetooth' + - 'kidde' + - 'lutron' + - 'zigbee' + - 'zwave' + - 'zwave_exclusion' + - 'zwave_network_rediscovery' kidde_radio_code: + name: Kidde radio code description: "A string of 8 1s and 0s one for each dip switch on the kidde device left --> right = 1 --> 8. Down = 1 and Up = 0" example: "10101010" + selector: + text: rename_wink_device: + name: Rename wink device description: Rename the provided device. + target: + entity: + integration: wink fields: - entity_id: - description: The entity_id of the device to rename. - example: binary_sensor.front_door_opened name: + name: Name description: The name to change it to. + required: true example: back_door + selector: + text: delete_wink_device: + name: Delete wink device description: Remove/unpair device from Wink. - fields: - entity_id: - description: The entity_id of the device to delete. + target: + entity: + integration: wink pull_newly_added_devices_from_wink: + name: Pull newly added devices from wink description: Pull newly paired devices from Wink. refresh_state_from_wink: + name: Refresh state from wink description: Pull the latest states for every device. set_siren_volume: + name: Set siren volume description: Set the volume of the siren for a Dome siren/chime. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" volume: - description: Volume level. One of ["low", "medium", "high"]. - example: "high" + name: Volume + description: Volume level. + required: true + selector: + select: + options: + - 'low' + - 'medium' + - 'high' enable_chime: + name: Enable chime description: Enable the chime of a Dome siren with the provided sound. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" tone: + name: Tone description: >- - The tone to use for the chime. One of ["doorbell", "fur_elise", - "doorbell_extended", "alert", "william_tell", "rondo_alla_turca", - "police_siren", "evacuation", "beep_beep", "beep", "inactive"] - example: "doorbell" + The tone to use for the chime. + required: true + selector: + select: + options: + - 'alert' + - 'beep' + - 'beep_beep' + - 'doorbell' + - 'doorbell_extended' + - 'evacuation' + - 'fur_elise' + - 'inactive' + - 'police_siren' + - 'rondo_alla_turca' + - 'william_tell' set_siren_tone: + name: Set siren tone description: Set the sound to use when the siren is enabled. (This doesn't enable the siren) + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" tone: + name: Tone description: >- - The tone to use for the chime. One of ["doorbell", "fur_elise", - "doorbell_extended", "alert", "william_tell", "rondo_alla_turca", - "police_siren", "evacuation", "beep_beep", "beep", "inactive"] - example: "alert" + The tone to use for the chime. + required: true + selector: + select: + options: + - 'alert' + - 'beep' + - 'beep_beep' + - 'doorbell' + - 'doorbell_extended' + - 'evacuation' + - 'fur_elise' + - 'inactive' + - 'police_siren' + - 'rondo_alla_turca' + - 'william_tell' siren_set_auto_shutoff: + name: Siren set auto shutoff description: How long to sound the siren before turning off. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" auto_shutoff: + name: Auto shutoff description: >- The time in seconds to sound the siren. One of [None, -1, 30, 60, 120] (None and -1 are forever. Use None for gocontrol, and -1 for Dome) - example: 60 + required: true + selector: + select: + options: + - 'None' + - '-1' + - '30' + - '60' + - '120' set_siren_strobe_enabled: + name: Set siren strobe enabled description: Enable or disable the strobe light when the siren is sounding. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" enabled: + name: Enabled description: "True or False" + required: true + selector: + boolean: set_chime_strobe_enabled: + name: Set chime strobe enabled description: Enable or disable the strobe light when the chime is sounding. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" enabled: + name: Enabled description: "True or False" + required: true + selector: + boolean: enable_siren: + name: Enable siren description: Enable/disable the siren. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set - example: "switch.dome_siren" enabled: + name: Enabled description: "true or false" + required: true + selector: + boolean: set_chime_volume: + name: Set chime volume description: Set the volume of the chime for a Dome siren/chime. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" volume: - description: Volume level. One of ["low", "medium", "high"] - example: "low" + name: Volume + description: Volume level. + required: true + selector: + select: + options: + - 'low' + - 'medium' + - 'high' set_nimbus_dial_configuration: + name: Set nimbus dial configuration description: Set the configuration of an individual nimbus dial + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name of the entity to set. - example: "wink.nimbus_dial_3" rotation: - description: Direction dial hand should spin ["cw" or "ccw"] - example: "cw" + name: Rotation + description: Direction dial hand should spin. + selector: + select: + options: + - 'cw' + - 'ccw' ticks: + name: Ticks description: Number of times the hand should move - example: 12 + selector: + number: + min: 0 + max: 3600 scale: - description: How the dial should move in response to higher values ["log" or "linear"] - example: "linear" + name: Scale + description: How the dial should move in response to higher values. + selector: + select: + options: + - 'linear' + - 'log' min_value: + name: minimum value description: The minimum value allowed to be set example: 0 + selector: + text: max_value: + name: Maximum value description: The maximum value allowed to be set example: 500 + selector: + text: min_position: - description: The minimum position the dial hand can rotate to generally [0-360] - example: 0 + name: Minimum position + description: The minimum position the dial hand can rotate to generally. + selector: + number: + min: 0 + max: 360 max_position: - description: The maximum position the dial hand can rotate to generally [0-360] - example: 360 + name: Maximum position + description: The maximum position the dial hand can rotate to generally. + selector: + number: + min: 0 + max: 360 set_nimbus_dial_state: + name: Set nimbus dial state description: Set the value and labels of an individual nimbus dial + target: + entity: + integration: wink fields: - entity_id: - description: Name of the entity to set. - example: "wink.nimbus_dial_3" value: + name: Value description: The value that should be set (Should be between min_value and max_value) + required: true example: 250 + selector: + text: labels: + name: Labels description: >- The values shown on the dial labels ["Dial 1", "test"] the first value is what is shown by default the second value is shown when the nimbus is pressed. example: ["example", "test"] + selector: + object: set_lock_vacation_mode: + name: Set lock vacation mode description: Set vacation mode for all or specified locks. Disables all user codes. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock enabled: + name: Enabled description: enable or disable. true or false. - example: true + required: true + selector: + boolean: set_lock_alarm_mode: + name: Set lock alarm mode description: Set alarm mode for all or specified locks. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock mode: - description: One of tamper, activity, or forced_entry. - example: tamper + name: Mode + description: Select mode. + required: true + selector: + select: + options: + - 'activity' + - 'forced_entry' + - 'tamper' set_lock_alarm_sensitivity: + name: Set lock alarm sensitivity description: Set alarm sensitivity for all or specified locks. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock sensitivity: - description: One of low, medium_low, medium, medium_high, high. - example: medium + name: Sensitivity + description: Choose the sensitivity. + required: true + selector: + select: + options: + - 'low' + - 'medium_low' + - 'medium' + - 'medium_high' + - 'high' set_lock_alarm_state: + name: Set lok alarm state description: Set alarm state. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock enabled: + name: Enabled description: enable or disable. true or false. - example: true + required: true + selector: + boolean: set_lock_beeper_state: + name: Set lock beeper state description: Set beeper state. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock enabled: + name: Enabled description: enable or disable. true or false. - example: true + required: true + selector: + boolean: add_new_lock_key_code: + name: Add new lock key code description: Add a new user key code. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock name: + name: Name description: name of the new key code. + required: true example: Bob + selector: + text: code: + name: Code description: new key code, length must match length of other codes. Default length is 4. + required: true example: 1234 + selector: + text: From a9660d5788d81259728f397d33b1c7fb05103392 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 15 May 2021 05:06:34 -0400 Subject: [PATCH 431/852] Add targets and selectors for services (L-M) (#50543) --- homeassistant/components/lcn/services.yaml | 464 +++++++++++++++++- homeassistant/components/lifx/services.yaml | 149 ++++-- homeassistant/components/light/services.yaml | 4 +- .../components/local_file/services.yaml | 10 + homeassistant/components/lock/services.yaml | 33 ++ .../components/logbook/services.yaml | 11 +- homeassistant/components/logger/services.yaml | 56 ++- .../components/logi_circle/services.yaml | 48 +- .../components/lovelace/services.yaml | 1 + homeassistant/components/matrix/services.yaml | 12 + .../components/media_extractor/services.yaml | 22 +- .../components/media_player/services.yaml | 7 +- .../components/melcloud/services.yaml | 24 +- .../components/microsoft_face/services.yaml | 46 ++ homeassistant/components/mill/services.yaml | 23 + .../components/min_max/services.yaml | 1 + homeassistant/components/minio/services.yaml | 35 ++ homeassistant/components/modbus/services.yaml | 46 +- .../components/monoprice/services.yaml | 18 +- .../components/motion_blinds/services.yaml | 21 +- .../components/mysensors/services.yaml | 12 +- 21 files changed, 945 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml index 89fe884fa9a..8755551c46a 100644 --- a/homeassistant/components/lcn/services.yaml +++ b/homeassistant/components/lcn/services.yaml @@ -1,200 +1,612 @@ # Describes the format for available LCN services output_abs: + name: Output absolute brightness description: Set absolute brightness of output port in percent. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: output: + name: Output description: Output port + required: true example: "output1" + selector: + select: + options: + - 'OUTPUT1' + - 'OUTPUT2' + - 'OUTPUT3' + - 'OUTPUT4' brightness: - description: Absolute brightness in percent (0..100) + name: Brightness + description: Absolute brightness in percent. + required: true example: 50 + selector: + number: + min: 0 + max: 100 transition: - description: Transition time in seconds + name: Transition + description: Transition time. example: 5 + default: 0 + selector: + number: + min: 0 + max: 486 + step: 0.1 + unit_of_measurement: seconds output_rel: + name: Output relative brightness description: Set relative brightness of output port in percent. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: output: + name: Output description: Output port + required: true example: "output1" + selector: + select: + options: + - 'OUTPUT1' + - 'OUTPUT2' + - 'OUTPUT3' + - 'OUTPUT4' brightness: - description: Relative brightness in percent (-100..100) + name: Brightness + description: Relative brightness in percent. + required: true example: 50 - transition: - description: Transition time in seconds - example: 5 + selector: + number: + min: -100 + max: 100 + unit_of_measurement: '%' output_toggle: + name: Toggle output description: Toggle output port. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: output: + name: Output description: Output port + required: true example: "output1" + selector: + select: + options: + - 'OUTPUT1' + - 'OUTPUT2' + - 'OUTPUT3' + - 'OUTPUT4' transition: - description: Transition time in seconds + name: Transition + description: Transition time. example: 5 + default: 0 + selector: + number: + min: 0 + max: 486 + step: 0.1 + unit_of_measurement: seconds relays: + name: Relays description: Set the relays status. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: state: + name: State description: Relays states as string (1=on, 2=off, t=toggle, -=nochange) + required: true example: "t---001-" + selector: + text: led: + name: LED description: Set the led state. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: led: + name: LED description: Led + required: true example: "led6" + selector: + select: + options: + - 'LED1' + - 'LED2' + - 'LED3' + - 'LED4' + - 'LED5' + - 'LED6' + - 'LED7' + - 'LED8' + - 'LED9' + - 'LED10' + - 'LED11' + - 'LED12' state: + name: State description: Led state + required: true example: "blink" - values: - - "on" - - "off" - - blink - - flicker + selector: + select: + options: + - 'blink' + - 'flicker' + - 'off' + - 'on' var_abs: + name: Set absolute variable description: Set absolute value of a variable or setpoint. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: variable: + name: Variable description: Variable or setpoint name + required: true example: "var1" + default: NATIVE + selector: + select: + options: + - 'R1VAR' + - 'R2VAR' + - 'R1VARSETPOINT' + - 'R2VARSETPOINT' + - 'TVAR' + - 'VAR1ORTVAR' + - 'VAR2ORR1VAR' + - 'VAR3ORR2VAR' + - 'VAR1' + - 'VAR2' + - 'VAR3' + - 'VAR4' + - 'VAR5' + - 'VAR6' + - 'VAR7' + - 'VAR8' + - 'VAR9' + - 'VAR10' + - 'VAR11' + - 'VAR12' value: + name: Value description: Value to set example: "50" + default: 0 + selector: + number: + min: 0 + max: 100000 unit_of_measurement: + name: Unit of measurement description: Unit of value example: "celsius" + selector: + select: + options: + - '' + - '%' + - '°' + - '°C' + - '°F' + - 'AMPERE' + - 'AMP' + - 'A' + - 'DEGREE' + - 'NATIVE' + - 'K' + - 'LCN' + - 'LUX_T' + - 'LX_T' + - 'LUX_I' + - 'LUX' + - 'LX' + - 'M/S' + - 'METERPERSECOND' + - 'PERCENT' + - 'PPM' + - 'V' + - 'VOLT' var_reset: + name: Reset variable description: Reset value of variable or setpoint. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" variable: + name: Variable description: Variable or setpoint name + required: true example: "var1" + selector: + select: + options: + - 'R1VAR' + - 'R2VAR' + - 'R1VARSETPOINT' + - 'R2VARSETPOINT' + - 'TVAR' + - 'VAR1ORTVAR' + - 'VAR2ORR1VAR' + - 'VAR3ORR2VAR' + - 'VAR1' + - 'VAR2' + - 'VAR3' + - 'VAR4' + - 'VAR5' + - 'VAR6' + - 'VAR7' + - 'VAR8' + - 'VAR9' + - 'VAR10' + - 'VAR11' + - 'VAR12' var_rel: + name: Shift variable description: Shift value of a variable, setpoint or threshold. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: variable: + name: Variable description: Variable or setpoint name + required: true example: "var1" + selector: + select: + options: + - 'R1VAR' + - 'R2VAR' + - 'R1VARSETPOINT' + - 'R2VARSETPOINT' + - 'THRS1' + - 'THRS2' + - 'THRS3' + - 'THRS4' + - 'THRS5' + - 'THRS2_1' + - 'THRS2_2' + - 'THRS2_3' + - 'THRS2_4' + - 'THRS3_1' + - 'THRS3_2' + - 'THRS3_3' + - 'THRS3_4' + - 'THRS4_1' + - 'THRS4_2' + - 'THRS4_3' + - 'THRS4_4' + - 'TVAR' + - 'VAR1ORTVAR' + - 'VAR2ORR1VAR' + - 'VAR3ORR2VAR' + - 'VAR1' + - 'VAR2' + - 'VAR3' + - 'VAR4' + - 'VAR5' + - 'VAR6' + - 'VAR7' + - 'VAR8' + - 'VAR9' + - 'VAR10' + - 'VAR11' + - 'VAR12' value: + name: Value description: Shift value example: "50" + default: 0 + selector: + number: + min: 0 + max: 100000 unit_of_measurement: + name: Unit of measurement description: Unit of value example: "celsius" + default: NATIVE + selector: + select: + options: + - '' + - '%' + - '°' + - '°C' + - '°F' + - 'AMPERE' + - 'AMP' + - 'A' + - 'DEGREE' + - 'NATIVE' + - 'K' + - 'LCN' + - 'LUX_T' + - 'LX_T' + - 'LUX_I' + - 'LUX' + - 'LX' + - 'M/S' + - 'METERPERSECOND' + - 'PERCENT' + - 'PPM' + - 'V' + - 'VOLT' value_reference: - description: Reference value (current or programmed) for setpoint and threshold + name: Reference value + description: Reference value for setpoint and threshold example: "current" - values: - - current - - prog + default: CURRENT + selector: + select: + options: + - 'CURRENT' + - 'PROG' lock_regulator: + name: Lock regulator description: Lock a regulator setpoint. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: setpoint: + name: Setpoint description: Setpoint name + required: true example: "r1varsetpoint" + selector: + select: + options: + - 'THRS1' + - 'THRS2' + - 'THRS3' + - 'THRS4' + - 'THRS5' + - 'THRS2_1' + - 'THRS2_2' + - 'THRS2_3' + - 'THRS2_4' + - 'THRS3_1' + - 'THRS3_2' + - 'THRS3_3' + - 'THRS3_4' + - 'THRS4_1' + - 'THRS4_2' + - 'THRS4_3' + - 'THRS4_4' state: + name: State description: New setpoint state example: true + default: false + selector: + boolean: send_keys: + name: Send keys description: Send keys (which executes bound commands). fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: keys: + name: Keys description: Keys to send + required: true example: "a1a5d8" + selector: + text: state: + name: State description: "Key state upon sending (optional, must be hit for deferred)" example: "hit" - values: - - hit - - make - - break + default: HIT + selector: + select: + options: + - 'HIT' + - 'MAKE' + - 'BREAK' + - 'DONTSEND' time: - description: Send delay (optional) + name: Time + description: Send delay. example: 10 + default: 0 + selector: + number: + min: 0 + max: 60 time_unit: - description: Time unit of send delay (optional) + name: Time unit + description: Time unit of send delay. example: "s" + default: S + selector: + select: + options: + - 'D' + - 'DAY' + - 'DAYS' + - 'H' + - 'HOUR' + - 'HOURS' + - 'M' + - 'MIN' + - 'MINUTE' + - 'MINUTES' + - 'S' + - 'SEC' + - 'SECOND' + - 'SECONDS' lock_keys: + name: Lock keys description: Lock keys. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: table: - description: "Table with keys to lock (optional, must be A for interval)." + name: Table + description: "Table with keys to lock (must be A for interval)." example: "A5" + default: A + selector: + text: state: + name: State description: Key lock states as string (1=on, 2=off, T=toggle, -=nochange) + required: true example: "1---t0--" + selector: + text: time: - description: Lock interval (optional) + name: Time + description: Lock interval. example: 10 + default: 0 + selector: + number: + min: 0 + max: 60 time_unit: - description: Time unit of lock interval (optional) + name: Time unit + description: Time unit of lock interval. example: "s" + default: S + selector: + select: + options: + - 'D' + - 'DAY' + - 'DAYS' + - 'H' + - 'HOUR' + - 'HOURS' + - 'M' + - 'MIN' + - 'MINUTE' + - 'MINUTES' + - 'S' + - 'SEC' + - 'SECOND' + - 'SECONDS' dyn_text: + name: Dynamic text description: Send dynamic text to LCN-GTxD displays. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: row: - description: Text row 1..4 (support of 4 independent text rows) + name: Row + description: Text row. + required: true example: 1 + selector: + number: + min: 1 + max: 4 text: + name: Text description: Text to send (up to 60 characters encoded as UTF-8) + required: true example: "text up to 60 characters" + selector: + text: pck: + name: PCK description: Send arbitrary PCK command. fields: address: + name: Address description: Module address + required: true example: "myhome.s0.m7" + selector: + text: pck: + name: PCK description: PCK command (without address header) + required: true example: "PIN4" + selector: + text: diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index 0ea0cb7a696..5f4784630aa 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -1,77 +1,168 @@ set_state: - description: Set a color/brightness and possibliy turn the light on/off. + name: Set State + description: Set a color/brightness and possibly turn the light on/off. + target: + entity: + integration: lifx + domain: light fields: - entity_id: - description: Name(s) of entities to set a state on. - example: "light.garage" - "...": - description: All turn_on parameters can be used to specify a color. infrared: - description: Automatic infrared level (0..255) when light brightness is low. + name: infrared + description: Automatic infrared level when light brightness is low. example: 255 + selector: + number: + min: 0 + max: 255 zones: + name: Zones description: List of zone numbers to affect (8 per LIFX Z, starts at 0). example: "[0,5]" + selector: + object: transition: - description: Duration in seconds it takes to get to the final state. + name: Transition + description: Duration it takes to get to the final state. example: 10 + selector: + number: + min: 0 + max: 3600 + unit_of_measurement: seconds power: - description: Turn the light on (True) or off (False). Leave out to keep the power as it is. + name: Power + description: Turn the light on or off. Leave out to keep the power as it is. example: false + selector: + boolean: effect_pulse: + name: Pulse effect description: Run a flash effect by changing to a color and back. + target: + entity: + integration: lifx + domain: light fields: - entity_id: - description: Name(s) of entities to run the effect on. - example: "light.kitchen" mode: - description: "Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid." + name: Mode + description: "Decides how colors are changed." example: strobe + selector: + select: + options: + - 'blink' + - 'breathe' + - 'ping' + - 'strobe' + - 'solid' brightness: - description: Number between 0..255 indicating brightness of the temporary color. + name: Brightness + description: Number indicating brightness of the temporary color. example: 120 + selector: + number: + min: 0 + max: 255 color_name: + name: Color name description: A human readable color name. example: "red" + selector: + text: rgb_color: + name: RGB color description: The temporary color in RGB-format. example: "[255, 100, 100]" + selector: + object: period: - description: Duration of the effect in seconds (default 1.0). + name: Period + description: Duration of the effect. example: 3 + default: 1.0 + selector: + number: + min: 0.05 + max: 60.00 + step: 0.05 + unit_of_measurement: seconds cycles: - description: Number of times the effect should run (default 1.0). + name: Cycles + description: Number of times the effect should run. example: 2 + default: 1 + selector: + number: + min: 1 + max: 10000 power_on: - description: Powered off lights are temporarily turned on during the effect (default True). + name: Power on + description: Powered off lights are temporarily turned on during the effect. example: false + default: true + selector: + boolean: effect_colorloop: + name: Color loop effect description: Run an effect with looping colors. + target: + entity: + integration: lifx + domain: light fields: - entity_id: - description: Name(s) of entities to run the effect on. - example: "light.disco1, light.disco2, light.disco3" brightness: - description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. + name: Brightness + description: Number indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. example: 120 + selector: + number: + min: 0 + max: 255 period: - description: Duration (in seconds) between color changes (default 60). + name: Period + description: Duration between color changes. example: 180 + default: 60 + selector: + number: + min: 0.05 + max: 3600.00 + step: 0.05 + unit_of_measurement: seconds change: - description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20). + name: Change + description: Hue movement per period, in degrees on a color wheel. example: 45 + default: 20 + selector: + number: + min: 0 + max: 360 + unit_of_measurement: '°' spread: - description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30). + name: Spread + description: Maximum hue difference between participating lights, in degrees on a color wheel. example: 0 + default: 30 + selector: + number: + min: 0 + max: 360 + unit_of_measurement: '°' power_on: - description: Powered off lights are temporarily turned on during the effect (default True). + name: Power on + description: Powered off lights are temporarily turned on during the effect. example: false + default: true + selector: + boolean: effect_stop: + name: Stop effect description: Stop a running effect. - fields: - entity_id: - description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. - example: "light.bedroom" + target: + entity: + integration: lifx + domain: light diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index dc12a72215d..34663df0288 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -569,7 +569,7 @@ toggle: mode: slider white_value: name: White level - description: Number between 0..255 indicating level of white. + description: Number indicating level of white. advanced: true example: "250" selector: @@ -581,7 +581,7 @@ toggle: brightness: name: Brightness value description: - Number between 0..255 indicating brightness, where 0 turns the light + Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. advanced: true diff --git a/homeassistant/components/local_file/services.yaml b/homeassistant/components/local_file/services.yaml index 9430c02c0e6..08a954594a9 100644 --- a/homeassistant/components/local_file/services.yaml +++ b/homeassistant/components/local_file/services.yaml @@ -1,9 +1,19 @@ update_file_path: + name: Update file path description: Use this service to change the file displayed by the camera. fields: entity_id: + name: Entity description: Name of the entity_id of the camera to update. + required: true example: "camera.local_file" + selector: + entity: + domain: camera file_path: + name: file path description: The full path to the new image file to be displayed. + required: true example: "/config/www/images/image.jpg" + selector: + text: diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index f5f6077ddc1..f852c82e4e1 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -1,24 +1,46 @@ # Describes the format for available lock services clear_usercode: + name: Clear usercode description: Clear a usercode from lock. fields: node_id: + name: Node ID description: Node id of the lock. example: 18 + selector: + number: + min: 1 + max: 255 code_slot: + name: Code slot description: Code slot to clear code from. example: 1 + selector: + number: + min: 1 + max: 255 get_usercode: + name: Get usercode description: Retrieve a usercode from lock. fields: node_id: + name: Node ID description: Node id of the lock. example: 18 + selector: + number: + min: 1 + max: 255 code_slot: + name: Code slot description: Code slot to retrieve a code from. example: 1 + selector: + number: + min: 1 + max: 255 lock: name: Lock @@ -51,12 +73,23 @@ set_usercode: node_id: description: Node id of the lock. example: 18 + selector: + number: + min: 1 + max: 255 code_slot: description: Code slot to set the code. example: 1 + selector: + number: + min: 1 + max: 255 usercode: description: Code to set. + required: true example: 1234 + selector: + text: unlock: name: Unlock diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml index 252b0b6a39b..e72d4c0a01f 100644 --- a/homeassistant/components/logbook/services.yaml +++ b/homeassistant/components/logbook/services.yaml @@ -1,29 +1,30 @@ log: - description: Create a custom entry in your logbook + name: Log + description: Create a custom entry in your logbook. fields: name: name: Name - description: Custom name for an entity, can be referenced with entity_id + description: Custom name for an entity, can be referenced with entity_id. required: true example: "Kitchen" selector: text: message: name: Message - description: Message of the custom logbook entry + description: Message of the custom logbook entry. required: true example: "is being used" selector: text: entity_id: name: Entity ID - description: Entity to reference in custom logbook entry [Optional] + description: Entity to reference in custom logbook entry. example: "light.kitchen" selector: entity: domain: name: Domain - description: Icon of domain to display in custom logbook entry [Optional] + description: Icon of domain to display in custom logbook entry. example: "light" selector: text: diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml index 39c0bcfdfe1..51fe5fe331c 100644 --- a/homeassistant/components/logger/services.yaml +++ b/homeassistant/components/logger/services.yaml @@ -21,23 +21,63 @@ set_level: description: Set log level for integrations. fields: homeassistant.core: + name: Home Assistant Core description: "Example on how to change the logging level for a Home Assistant Core - integrations. Possible values are debug, info, warn, warning, error, - fatal, critical." + integrations." example: debug + selector: + select: + options: + - 'debug' + - 'critical' + - 'error' + - 'fatal' + - 'info' + - 'warn' + - 'warning' homeassistant.components.mqtt: + name: Home Assistant components mqtt description: - "Example on how to change the logging level for an Integration. Possible - values are debug, info, warn, warning, error, fatal, critical." + "Example on how to change the logging level for an Integration." example: warning + selector: + select: + options: + - 'debug' + - 'critical' + - 'error' + - 'fatal' + - 'info' + - 'warn' + - 'warning' custom_components.my_integration: + name: Custom components "my_integation" description: - "Example on how to change the logging level for a Custom Integration. - Possible values are debug, info, warn, warning, error, fatal, critical." + "Example on how to change the logging level for a Custom Integration." example: debug + selector: + select: + options: + - 'debug' + - 'critical' + - 'error' + - 'fatal' + - 'info' + - 'warn' + - 'warning' aiohttp: + name: aioHttp description: - "Example on how to change the logging level for a Python module. - Possible values are debug, info, warn, warning, error, fatal, critical." + "Example on how to change the logging level for a Python module." example: error + selector: + select: + options: + - 'debug' + - 'critical' + - 'error' + - 'fatal' + - 'info' + - 'warn' + - 'warning' diff --git a/homeassistant/components/logi_circle/services.yaml b/homeassistant/components/logi_circle/services.yaml index 8d1c7ca1485..00f2a7090fe 100644 --- a/homeassistant/components/logi_circle/services.yaml +++ b/homeassistant/components/logi_circle/services.yaml @@ -1,37 +1,81 @@ # Describes the format for available Logi Circle services set_config: + name: Set config description: Set a configuration property. fields: entity_id: + name: Entity description: Name(s) of entities to apply the operation mode to. example: "camera.living_room_camera" + selector: + entity: + integration: logi_circle + domain: camera mode: + name: Mode description: "Operation mode. Allowed values: LED, RECORDING_MODE." + required: true example: "RECORDING_MODE" + selector: + select: + options: + - 'LED' + - 'RECORDING_MODE' value: - description: "Operation value. Allowed values: true, false" + name: Value + description: "Operation value." + required: true example: true + selector: + boolean: livestream_snapshot: + name: Livestream snapshot description: Take a snapshot from the camera's livestream. Will wake the camera from sleep if required. fields: entity_id: + name: Entity description: Name(s) of entities to create snapshots from. example: "camera.living_room_camera" + selector: + entity: + integration: logi_circle + domain: camera filename: + name: File name description: Template of a Filename. Variable is entity_id. + required: true example: "/tmp/snapshot_{{ entity_id }}.jpg" + selector: + text: livestream_record: + name: Livestream record description: Take a video recording from the camera's livestream. fields: entity_id: + name: Entity description: Name(s) of entities to create recordings from. example: "camera.living_room_camera" + selector: + entity: + integration: logi_circle + domain: camera filename: + name: File name description: Template of a Filename. Variable is entity_id. + required: true example: "/tmp/snapshot_{{ entity_id }}.mp4" + selector: + text: duration: - description: Recording duration in seconds. + name: Duration + description: Recording duration. + required: true example: 60 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds diff --git a/homeassistant/components/lovelace/services.yaml b/homeassistant/components/lovelace/services.yaml index b324b551e94..f9fc5999da6 100644 --- a/homeassistant/components/lovelace/services.yaml +++ b/homeassistant/components/lovelace/services.yaml @@ -1,4 +1,5 @@ # Describes the format for available lovelace services reload_resources: + name: Reload resources description: Reload Lovelace resources from YAML configuration diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index fe99bf6365a..66988def22d 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -1,12 +1,24 @@ send_message: + name: Send message description: Send message to target room(s) fields: message: + name: Message description: The message to be sent. + required: true example: This is a message I am sending to matrix + selector: + text: target: + name: Target description: A list of room(s) to send the message to. + required: true example: "#hasstest:matrix.org" + selector: + text: data: + name: Data description: Extended information of notification. Supports list of images. Optional. example: "{'images': ['/tmp/test.jpg']}" + selector: + text: diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml index 1e58c19baf1..b3ef02c74e3 100644 --- a/homeassistant/components/media_extractor/services.yaml +++ b/homeassistant/components/media_extractor/services.yaml @@ -1,12 +1,28 @@ play_media: + name: Play media description: Downloads file from given URL. + target: + entity: + domain: media_player fields: - entity_id: - description: Name(s) of entities to play media on. - example: "media_player.living_room_chromecast" media_content_id: + name: Media content ID description: The ID of the content to play. Platform dependent. + required: true example: "https://soundcloud.com/bruttoband/brutto-11" + selector: + text: media_content_type: + name: Media content type description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC. + required: true example: "music" + selector: + select: + options: + - 'CHANNEL' + - 'EPISODE' + - 'PLAYLIST MUSIC' + - 'MUSIC' + - 'TVSHOW' + - 'VIDEO' diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index bec89ed44fb..9699fa5f8bb 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -176,7 +176,7 @@ repeat_set: fields: repeat: name: Repeat mode - description: Repeat mode to set (off, all, one). + description: Repeat mode to set. required: true example: "off" selector: @@ -187,18 +187,21 @@ repeat_set: - "one" join: + name: Join description: Group players together. Only works on platforms with support for player groups. - name: Join target: fields: group_members: + name: Group members description: The players which will be synced with the target player. example: - "media_player.multiroom_player2" - "media_player.multiroom_player3" + selector: + object: unjoin: description: diff --git a/homeassistant/components/melcloud/services.yaml b/homeassistant/components/melcloud/services.yaml index 40faa097d9b..f470076ee7f 100644 --- a/homeassistant/components/melcloud/services.yaml +++ b/homeassistant/components/melcloud/services.yaml @@ -1,23 +1,35 @@ set_vane_horizontal: + name: Set vane horizontal description: Sets horizontal vane position. + target: + entity: + integration: melcloud + domain: climate fields: - entity_id: - description: Name of the target entity - example: "climate.ac_1" position: + name: Position description: > Horizontal vane position. Possible options can be found in the vane_horizontal_positions state attribute. + required: true example: "auto" + selector: + text: set_vane_vertical: + name: Set vane vertical description: Sets vertical vane position. + target: + entity: + integration: melcloud + domain: climate fields: - entity_id: - description: Name of the target entity - example: "climate.ac_1" position: + name: Position description: > Vertical vane position. Possible options can be found in the vane_vertical_positions state attribute. + required: true example: "auto" + selector: + text: diff --git a/homeassistant/components/microsoft_face/services.yaml b/homeassistant/components/microsoft_face/services.yaml index 4b273286d83..e27e29dfc6f 100644 --- a/homeassistant/components/microsoft_face/services.yaml +++ b/homeassistant/components/microsoft_face/services.yaml @@ -1,48 +1,94 @@ create_group: + name: Create group description: Create a new person group. fields: name: + name: Name description: Name of the group. + required: true example: family + selector: + text: create_person: + name: Create person description: Create a new person in the group. fields: group: + name: Group description: Name of the group + required: true example: family + selector: + text: name: + name: Name description: Name of the person + required: true example: Hans + selector: + text: delete_group: + name: Delete group description: Delete a new person group. fields: name: + name: Name description: Name of the group. + required: true example: family + selector: + text: delete_person: + name: Delete person description: Delete a person in the group. fields: group: + name: Group description: Name of the group. + required: true example: family + selector: + text: name: + name: Name description: Name of the person. + required: true example: Hans + selector: + text: face_person: + name: Face person description: Add a new picture to a person. fields: camera_entity: + name: Camera entity description: Camera to take a picture. + required: true example: camera.door + selector: + text: group: + name: Group description: Name of the group. + required: true example: family + selector: + text: person: + name: Person description: Name of the person. + required: true example: Hans + selector: + text: train_group: + name: Train group description: Train a person group. fields: group: + name: Group description: Name of the group + required: true example: family + selector: + text: diff --git a/homeassistant/components/mill/services.yaml b/homeassistant/components/mill/services.yaml index 5223bf9c890..37b878580c9 100644 --- a/homeassistant/components/mill/services.yaml +++ b/homeassistant/components/mill/services.yaml @@ -1,15 +1,38 @@ set_room_temperature: + name: Set room temperature description: Set Mill room temperatures. fields: room_name: + name: Room name description: Name of room to change. + required: true example: "kitchen" + selector: + text: away_temp: + name: Away temperature description: Away temp. example: 12 + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '°' comfort_temp: + name: Comfort temperature description: Comfort temp. example: 22 + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '°' sleep_temp: + name: Sleep temperature description: Sleep temp. example: 17 + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '°' diff --git a/homeassistant/components/min_max/services.yaml b/homeassistant/components/min_max/services.yaml index c91b36249ac..cca67d92144 100644 --- a/homeassistant/components/min_max/services.yaml +++ b/homeassistant/components/min_max/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all min_max entities. diff --git a/homeassistant/components/minio/services.yaml b/homeassistant/components/minio/services.yaml index 8fb8a267c3b..39e430ab165 100644 --- a/homeassistant/components/minio/services.yaml +++ b/homeassistant/components/minio/services.yaml @@ -1,35 +1,70 @@ get: + name: Get description: Download file from Minio. fields: bucket: + name: Bucket description: Bucket to use. + required: true example: camera-files + selector: + text: key: + name: Kay description: Object key of the file. + required: true example: front_camera/2018/01/02/snapshot_12512514.jpg + selector: + text: file_path: + name: File path description: File path on local filesystem. + required: true example: /data/camera_files/snapshot.jpg + selector: + text: put: + name: Put description: Upload file to Minio. fields: bucket: + name: Bucket description: Bucket to use. + required: true example: camera-files + selector: + text: key: + name: Key description: Object key of the file. + required: true example: front_camera/2018/01/02/snapshot_12512514.jpg + selector: + text: file_path: + name: File path description: File path on local filesystem. + required: true example: /data/camera_files/snapshot.jpg + selector: + text: remove: + name: Remove description: Delete file from Minio. fields: bucket: + name: Bucket description: Bucket to use. + required: true example: camera-files + selector: + text: key: + name: Key description: Object key of the file. + required: true example: front_camera/2018/01/02/snapshot_12512514.jpg + selector: + text: diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index ba3113db5e0..a3aa26a1a41 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -1,30 +1,72 @@ write_coil: + name: Write coil description: Write to a modbus coil. fields: address: + name: Address description: Address of the register to write to. + required: true example: 0 + selector: + number: + min: 1 + max: 255 state: + name: State description: State to write. + required: true example: false + selector: + object: unit: + name: Unit description: Address of the modbus unit. + required: true example: 21 + selector: + number: + min: 1 + max: 255 hub: - description: Optional Modbus hub name. A hub with the name 'default' is used if not specified. + name: Hub + description: Modbus hub name. example: "hub1" + default: "modbus_hub" + selector: + text: write_register: + name: Write register description: Write to a modbus holding register. fields: address: + name: Address description: Address of the holding register to write to. + required: true example: 0 + selector: + number: + min: 1 + max: 255 unit: + name: Unit description: Address of the modbus unit. + required: true example: 21 + selector: + number: + min: 1 + max: 255 value: + name: Value description: Value (single value or array) to write. + required: true example: "0 or [4,0]" + selector: + object: hub: - description: Optional Modbus hub name. A hub with the name 'default' is used if not specified. + name: Hub + description: Modbus hub name. example: "hub1" + default: "modbus_hub" + selector: + text: diff --git a/homeassistant/components/monoprice/services.yaml b/homeassistant/components/monoprice/services.yaml index a271d704768..93275fd2a1d 100644 --- a/homeassistant/components/monoprice/services.yaml +++ b/homeassistant/components/monoprice/services.yaml @@ -1,13 +1,15 @@ snapshot: + name: Snapshot description: Take a snapshot of the media player zone. - fields: - entity_id: - description: Name(s) of entities that will be snapshot. Platform dependent. - example: "media_player.living_room" + target: + entity: + integration: monoprice + domain: media_player restore: + name: Restore description: Restore a snapshot of the media player zone. - fields: - entity_id: - description: Name(s) of entities that will be restored. Platform dependent. - example: "media_player.living_room" + target: + entity: + integration: monoprice + domain: media_player diff --git a/homeassistant/components/motion_blinds/services.yaml b/homeassistant/components/motion_blinds/services.yaml index f46cc94bd43..08ee4098e27 100644 --- a/homeassistant/components/motion_blinds/services.yaml +++ b/homeassistant/components/motion_blinds/services.yaml @@ -1,14 +1,27 @@ # Describes the format for available motion blinds services set_absolute_position: + name: Set absolute position description: "Set the absolute position of the cover." + target: + entity: + integration: motion_blinds + domain: cover fields: - entity_id: - description: Name of the motion blind cover entity to control. - example: "cover.TopDownBottomUp-Bottom-0001" absolute_position: + name: Absolute position description: Absolute position to move to. + required: true example: 70 + selector: + number: + min: 1 + max: 100 width: - description: Optionally specify the width that is covered, only for TDBU Combined entities. + name: Width + description: Specify the width that is covered, only for TDBU Combined entities. example: 30 + selector: + number: + min: 1 + max: 100 diff --git a/homeassistant/components/mysensors/services.yaml b/homeassistant/components/mysensors/services.yaml index a93429550cd..e0fa5bf8e89 100644 --- a/homeassistant/components/mysensors/services.yaml +++ b/homeassistant/components/mysensors/services.yaml @@ -1,9 +1,19 @@ send_ir_code: + name: Send IR code description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on. fields: entity_id: - description: Name(s) of entities that should have the IR code set and be turned on. Platform dependent. + name: Entity + description: Name of entity that should have the IR code set and be turned on. Platform dependent. example: "switch.living_room_1_1" + selector: + entity: + integration: mysensors + domain: switch V_IR_SEND: + name: IR send description: IR code to send. + required: true example: "0xC284" + selector: + text: From 77bed66a4d2ad4cfcb83d3ddc185c10f9084f1a4 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 15 May 2021 05:41:43 -0400 Subject: [PATCH 432/852] Fix roon services.yaml (#50638) --- homeassistant/components/roon/services.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roon/services.yaml b/homeassistant/components/roon/services.yaml index 0697911c07c..d3a33ec2fe7 100644 --- a/homeassistant/components/roon/services.yaml +++ b/homeassistant/components/roon/services.yaml @@ -10,6 +10,8 @@ transfer: name: Transfer ID description: id of the destination player. required: true - example: "media_player.study" selector: - text: + entity: + integration: roon + domain: media_player + From f2f64348e75512ee5d269afdf16ef20ac905db3c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 13:09:18 +0200 Subject: [PATCH 433/852] Deprecate MELCloud YAML configuration (#50645) --- homeassistant/components/melcloud/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index fcff9ab3304..2380d0ea8d7 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -29,14 +29,17 @@ PLATFORMS = ["climate", "sensor", "water_heater"] CONF_LANGUAGE = "language" CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_TOKEN): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_TOKEN): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From e7392609e3bf81371edff16821a0a11f01849448 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 13:09:45 +0200 Subject: [PATCH 434/852] Deprecate Jandy iAqualink YAML configuration (#50644) --- .../components/iaqualink/__init__.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 895733c5f35..a95ec804890 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -54,19 +54,22 @@ PLATFORMS = [ ] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> None: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Aqualink component.""" conf = config.get(DOMAIN) From 704a9969569b409351169ee2a8c0d90b191a2dce Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 13:17:10 +0200 Subject: [PATCH 435/852] Deprecate Tibber YAML configuration (#50646) --- homeassistant/components/tibber/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 81c3fd406a2..d575a520cb2 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -21,7 +21,10 @@ PLATFORMS = [ ] CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, + vol.All( + cv.deprecated(DOMAIN), + {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, + ), extra=vol.ALLOW_EXTRA, ) From 8d551e3f7b4622dc1d80830bc0775953f4affca6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 13:25:00 +0200 Subject: [PATCH 436/852] Deprecate Transmission YAML configuration (#50648) --- homeassistant/components/transmission/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index b50f228ddad..e0ced70f15e 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -91,7 +91,8 @@ TRANS_SCHEMA = vol.All( ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [TRANS_SCHEMA])}, extra=vol.ALLOW_EXTRA + vol.All(cv.deprecated(DOMAIN), {DOMAIN: vol.All(cv.ensure_list, [TRANS_SCHEMA])}), + extra=vol.ALLOW_EXTRA, ) PLATFORMS = ["sensor", "switch"] From 117860f13b028c43345fa6891e6c5fde2edc77b0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 13:30:23 +0200 Subject: [PATCH 437/852] Update Hue IoT Class to Local Push (#50637) --- homeassistant/components/hue/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 896c9c7a048..2a46da9c52b 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -23,5 +23,5 @@ }, "codeowners": ["@balloob", "@frenck"], "quality_scale": "platinum", - "iot_class": "local_polling" + "iot_class": "local_push" } From c9b25fe2a2f9560872632df4edad5e2a28bf7a3c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 13:45:10 +0200 Subject: [PATCH 438/852] Remove YAML configuration from Local IP (#50642) --- homeassistant/components/local_ip/__init__.py | 28 ++----------------- .../components/local_ip/config_flow.py | 4 --- tests/components/local_ip/test_init.py | 17 +++++------ 3 files changed, 9 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index 1e8376b6b6f..c4e8c541e4a 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -1,35 +1,11 @@ """Get the local IP address of the Home Assistant instance.""" -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from .const import DOMAIN, PLATFORMS -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.deprecated(CONF_NAME), - vol.Schema({vol.Optional(CONF_NAME, default=DOMAIN): cv.string}), - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up local_ip from configuration.yaml.""" - conf = config.get(DOMAIN) - if conf: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, data=conf, context={"source": SOURCE_IMPORT} - ) - ) - - return True +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/local_ip/config_flow.py b/homeassistant/components/local_ip/config_flow.py index 3ce1bbfb4dc..2bc994c4dca 100644 --- a/homeassistant/components/local_ip/config_flow.py +++ b/homeassistant/components/local_ip/config_flow.py @@ -20,7 +20,3 @@ class SimpleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user") return self.async_create_entry(title=DOMAIN, data=user_input) - - async def async_step_import(self, import_info): - """Handle import from config file.""" - return await self.async_step_user(import_info) diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py index 3f5c4395f2d..a7ebfba28e2 100644 --- a/tests/components/local_ip/test_init.py +++ b/tests/components/local_ip/test_init.py @@ -1,21 +1,18 @@ """Tests for the local_ip component.""" -import pytest - from homeassistant.components.local_ip import DOMAIN -from homeassistant.setup import async_setup_component from homeassistant.util import get_local_ip - -@pytest.fixture(name="config") -def config_fixture(): - """Create hass config fixture.""" - return {DOMAIN: {}} +from tests.common import MockConfigEntry -async def test_basic_setup(hass, config): +async def test_basic_setup(hass): """Test component setup creates entry from config.""" - assert await async_setup_component(hass, DOMAIN, config) + entry = MockConfigEntry(domain=DOMAIN, data={}) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + local_ip = await hass.async_add_executor_job(get_local_ip) state = hass.states.get(f"sensor.{DOMAIN}") assert state From 599db742a3e78423b5f7cb7e5c19f408e6ce0fbc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 13:45:25 +0200 Subject: [PATCH 439/852] Deprecate Mikrotik YAML configuration (#50649) --- homeassistant/components/mikrotik/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index cd96cba327c..b3813dc6a6e 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -45,7 +45,10 @@ MIKROTIK_SCHEMA = vol.All( CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [MIKROTIK_SCHEMA])}, extra=vol.ALLOW_EXTRA + vol.All( + cv.deprecated(DOMAIN), {DOMAIN: vol.All(cv.ensure_list, [MIKROTIK_SCHEMA])} + ), + extra=vol.ALLOW_EXTRA, ) From d72a10a5e97f23165440231c4a9f4404627231da Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 13:45:54 +0200 Subject: [PATCH 440/852] Deprecate Plum Lightpad YAML configuration (#50650) --- .../components/plum_lightpad/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index ecc1dacfb2f..ab370f53731 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -17,14 +17,17 @@ from .utils import load_plum _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From 625e6ceff3c876e1f3d044897a599c322cc5ca36 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 13:46:16 +0200 Subject: [PATCH 441/852] Deprecate Soma Connect YAML configuration (#50651) --- homeassistant/components/soma/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 62cdeb11f8b..e90fba62824 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -15,11 +15,14 @@ from .const import API, DOMAIN, HOST, PORT DEVICES = "devices" CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.string} - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.string} + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From b2c0bebbf0c2a5303b6adacc50de7a48bbdf0c5f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 13:56:23 +0200 Subject: [PATCH 442/852] Deprecate VeSync YAML configuration (#50652) --- homeassistant/components/vesync/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 9a79ca1fbb4..3a17af55c93 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -26,14 +26,17 @@ PLATFORMS = ["switch", "fan", "light"] _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From 00e90736bdd5ef0eb552dc23867c9f9526b23729 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 14:12:40 +0200 Subject: [PATCH 443/852] Deprecate Islamic Prayer Times YAML configuration (#50654) --- .../islamic_prayer_times/__init__.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 8fa2d1b04cb..8375b9d4c12 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later, async_track_point_in_time import homeassistant.util.dt as dt_util @@ -25,13 +26,16 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: { - vol.Optional(CONF_CALC_METHOD, default=DEFAULT_CALC_METHOD): vol.In( - CALC_METHODS - ), - } - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: { + vol.Optional(CONF_CALC_METHOD, default=DEFAULT_CALC_METHOD): vol.In( + CALC_METHODS + ), + } + }, + ), extra=vol.ALLOW_EXTRA, ) From c6860dc999fd65039a665a914e2602e8f760112a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 14:13:20 +0200 Subject: [PATCH 444/852] Deprecate JuiceNet YAML configuration (#50655) --- homeassistant/components/juicenet/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index f892babd9cf..28789849944 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -22,7 +22,10 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor", "switch"] CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, + vol.All( + cv.deprecated(DOMAIN), + {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, + ), extra=vol.ALLOW_EXTRA, ) From 8a135ce0f688d8793c3bc5237e37717b7dd1345f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 14:56:28 +0200 Subject: [PATCH 445/852] Deprecate Meteo-France YAML configuration (#50658) --- homeassistant/components/meteo_france/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 4ec03e4f5a5..15a9aa7d3cd 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -33,7 +33,10 @@ SCAN_INTERVAL = timedelta(minutes=15) CITY_SCHEMA = vol.Schema({vol.Required(CONF_CITY): cv.string}) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CITY_SCHEMA]))}, + vol.All( + cv.deprecated(DOMAIN), + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CITY_SCHEMA]))}, + ), extra=vol.ALLOW_EXTRA, ) From 7ae050c5acb0c17e9b8e247fa037f63c7b411deb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 14:56:49 +0200 Subject: [PATCH 446/852] Upgrade watchdog to 2.1.1 (#50659) --- homeassistant/components/folder_watcher/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 828d925ddbd..8907d37b472 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==2.1.0"], + "requirements": ["watchdog==2.1.1"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 092f1ece333..853d4a30557 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2329,7 +2329,7 @@ wakeonlan==2.0.1 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.1.0 +watchdog==2.1.1 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba1b5c40889..0dbb679019b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1250,7 +1250,7 @@ vultr==0.1.2 wakeonlan==2.0.1 # homeassistant.components.folder_watcher -watchdog==2.1.0 +watchdog==2.1.1 # homeassistant.components.wiffi wiffi==1.0.1 From f84ceee7b740654a5968e3a20b1d86f0b27c662e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 15 May 2021 15:00:03 +0200 Subject: [PATCH 447/852] Bump OpenCV 4.4.0.42 (#50640) --- homeassistant/components/opencv/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index b2fecaf8144..7a64332c840 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.20.2", "opencv-python-headless==4.3.0.36"], + "requirements": ["numpy==1.20.2", "opencv-python-headless==4.4.0.42"], "codeowners": [], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 853d4a30557..fe368af45cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1064,7 +1064,7 @@ onvif-zeep-async==1.0.0 open-garage==0.1.4 # homeassistant.components.opencv -# opencv-python-headless==4.3.0.36 +# opencv-python-headless==4.4.0.42 # homeassistant.components.openerz openerz-api==0.1.0 From 4025443b67c7ecf7b45ae0e128dee0e9b7052370 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 15:39:45 +0200 Subject: [PATCH 448/852] Upgrade black to 21.5b1 (#50661) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 15e723feb26..a261686a8bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 21.5b0 + rev: 21.5b1 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 4b08322b43b..f874bb288f7 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.5b0 +black==21.5b1 codespell==2.0.0 flake8-comprehensions==3.4.0 flake8-docstrings==1.6.0 From bdc1ab2b88cc64cc4bfb7d0c0edbeaa4d41df48f Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Sat, 15 May 2021 15:55:07 +0200 Subject: [PATCH 449/852] Flic bump lib to 2.0.3 (#50483) --- homeassistant/components/flic/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flic/manifest.json b/homeassistant/components/flic/manifest.json index c7018199d91..7480257fcaa 100644 --- a/homeassistant/components/flic/manifest.json +++ b/homeassistant/components/flic/manifest.json @@ -2,7 +2,7 @@ "domain": "flic", "name": "Flic", "documentation": "https://www.home-assistant.io/integrations/flic", - "requirements": ["pyflic-homeassistant==0.4.dev0"], + "requirements": ["pyflic==2.0.3"], "codeowners": [], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index fe368af45cd..3f2941c3210 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1409,7 +1409,7 @@ pyfireservicerota==0.0.40 pyflexit==0.3 # homeassistant.components.flic -pyflic-homeassistant==0.4.dev0 +pyflic==2.0.3 # homeassistant.components.flume pyflume==0.5.5 From 5ce07e689a09fc4524bd27508d590bf4acebccfa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 15:55:45 +0200 Subject: [PATCH 450/852] Upgrade flake8 to 3.9.2 (#50664) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a261686a8bd..e4bf6908e69 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: exclude_types: [csv, json] exclude: ^tests/fixtures/ - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.1 + rev: 3.9.2 hooks: - id: flake8 additional_dependencies: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index f874bb288f7..1d6af7f315a 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -6,7 +6,7 @@ codespell==2.0.0 flake8-comprehensions==3.4.0 flake8-docstrings==1.6.0 flake8-noqa==1.1.0 -flake8==3.9.1 +flake8==3.9.2 isort==5.8.0 mccabe==0.6.1 pycodestyle==2.7.0 From 71c21693ef998f25eb181559e1cacfdc6d9aa2e4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 16:02:10 +0200 Subject: [PATCH 451/852] Upgrade flake8-comprehensions to 3.5.0 (#50665) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4bf6908e69..09ce50b8dd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - pyflakes==2.3.1 - flake8-docstrings==1.6.0 - pydocstyle==6.0.0 - - flake8-comprehensions==3.4.0 + - flake8-comprehensions==3.5.0 - flake8-noqa==1.1.0 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 1d6af7f315a..55a4bbf6491 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -3,7 +3,7 @@ bandit==1.7.0 black==21.5b1 codespell==2.0.0 -flake8-comprehensions==3.4.0 +flake8-comprehensions==3.5.0 flake8-docstrings==1.6.0 flake8-noqa==1.1.0 flake8==3.9.2 From 64a6a75330a0c7d9c5ff04f514eea5f810518264 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 16:04:08 +0200 Subject: [PATCH 452/852] Upgrade pyupgrade to v2.15.0 (#50666) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09ce50b8dd4..57bc4a85ad1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.14.0 + rev: v2.15.0 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 55a4bbf6491..8422dbc338c 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 -pyupgrade==2.14.0 +pyupgrade==2.15.0 yamllint==1.26.1 From bdeeb54d2dabd577e10b2c33292890f879db5c4c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 16:09:44 +0200 Subject: [PATCH 453/852] Deprecate PVPC YAML configuration (#50656) --- homeassistant/components/pvpc_hourly_pricing/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index dfb7282aae9..2ab8f387bda 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -15,7 +15,8 @@ UI_CONFIG_SCHEMA = vol.Schema( } ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.ensure_list(UI_CONFIG_SCHEMA)}, extra=vol.ALLOW_EXTRA + vol.All(cv.deprecated(DOMAIN), {DOMAIN: cv.ensure_list(UI_CONFIG_SCHEMA)}), + extra=vol.ALLOW_EXTRA, ) From 8e38f26978073f18cc0bc59d9c8b6b917cb5c6e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 May 2021 10:24:36 -0400 Subject: [PATCH 454/852] Add support for asair brand to nexia (#50504) --- homeassistant/components/nexia/__init__.py | 5 ++- homeassistant/components/nexia/config_flow.py | 14 ++++++- homeassistant/components/nexia/const.py | 5 +++ homeassistant/components/nexia/manifest.json | 4 +- homeassistant/components/nexia/strings.json | 2 +- .../components/nexia/translations/en.json | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nexia/test_config_flow.py | 40 +++++++++++++++---- tests/components/nexia/util.py | 7 ++-- 10 files changed, 64 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index da3e00b2d6a..65be22b57f1 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -3,6 +3,7 @@ from datetime import timedelta from functools import partial import logging +from nexia.const import BRAND_NEXIA from nexia.home import NexiaHome from requests.exceptions import ConnectTimeout, HTTPError @@ -13,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR +from .const import CONF_BRAND, DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) @@ -29,6 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): conf = entry.data username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] + brand = conf.get(CONF_BRAND, BRAND_NEXIA) state_file = hass.config.path(f"nexia_config_{username}.conf") @@ -40,6 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): password=password, device_name=hass.config.location_name, state_file=state_file, + brand=brand, ) ) except ConnectTimeout as ex: diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index d9e69fd75b6..18c20a8f92a 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Nexia integration.""" import logging +from nexia.const import BRAND_ASAIR, BRAND_NEXIA from nexia.home import NexiaHome from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -8,12 +9,20 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN +from .const import BRAND_ASAIR_NAME, BRAND_NEXIA_NAME, CONF_BRAND, DOMAIN from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str}) +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_BRAND, default=BRAND_NEXIA): vol.In( + {BRAND_NEXIA: BRAND_NEXIA_NAME, BRAND_ASAIR: BRAND_ASAIR_NAME} + ), + } +) async def validate_input(hass: core.HomeAssistant, data): @@ -27,6 +36,7 @@ async def validate_input(hass: core.HomeAssistant, data): nexia_home = NexiaHome( username=data[CONF_USERNAME], password=data[CONF_PASSWORD], + brand=data[CONF_BRAND], auto_login=False, auto_update=False, device_name=hass.config.location_name, diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index dbe7b71705c..d6e3e5f8008 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -7,6 +7,8 @@ ATTRIBUTION = "Data provided by mynexia.com" NOTIFICATION_ID = "nexia_notification" NOTIFICATION_TITLE = "Nexia Setup" +CONF_BRAND = "brand" + NEXIA_DEVICE = "device" NEXIA_SCAN_INTERVAL = "scan_interval" @@ -29,3 +31,6 @@ MANUFACTURER = "Trane" SIGNAL_ZONE_UPDATE = "NEXIA_CLIMATE_ZONE_UPDATE" SIGNAL_THERMOSTAT_UPDATE = "NEXIA_CLIMATE_THERMOSTAT_UPDATE" + +BRAND_NEXIA_NAME = "Nexia" +BRAND_ASAIR_NAME = "American Standard" diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 5411723d2e2..ed1247ee9e3 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", - "name": "Nexia", - "requirements": ["nexia==0.9.6"], + "name": "Nexia/American Standard", + "requirements": ["nexia==0.9.7"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/strings.json b/homeassistant/components/nexia/strings.json index 876ea2d656f..c9bc84243da 100644 --- a/homeassistant/components/nexia/strings.json +++ b/homeassistant/components/nexia/strings.json @@ -2,8 +2,8 @@ "config": { "step": { "user": { - "title": "Connect to mynexia.com", "data": { + "brand": "Brand", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/nexia/translations/en.json b/homeassistant/components/nexia/translations/en.json index fad0b8e542a..050db24e0cf 100644 --- a/homeassistant/components/nexia/translations/en.json +++ b/homeassistant/components/nexia/translations/en.json @@ -11,10 +11,10 @@ "step": { "user": { "data": { + "brand": "Brand", "password": "Password", "username": "Username" - }, - "title": "Connect to mynexia.com" + } } } } diff --git a/requirements_all.txt b/requirements_all.txt index 3f2941c3210..4ab0c5b0b3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1000,7 +1000,7 @@ nettigo-air-monitor==0.2.5 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.6 +nexia==0.9.7 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0dbb679019b..5039df65ab2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -551,7 +551,7 @@ netdisco==2.8.3 nettigo-air-monitor==0.2.5 # homeassistant.components.nexia -nexia==0.9.6 +nexia==0.9.7 # homeassistant.components.notify_events notify-events==1.0.4 diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index b9726fdd974..2dd5c270c07 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,14 +1,17 @@ """Test the nexia config flow.""" from unittest.mock import MagicMock, patch +from nexia.const import BRAND_ASAIR, BRAND_NEXIA +import pytest from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import config_entries, setup -from homeassistant.components.nexia.const import DOMAIN +from homeassistant.components.nexia.const import CONF_BRAND, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -async def test_form(hass): +@pytest.mark.parametrize("brand", [BRAND_ASAIR, BRAND_NEXIA]) +async def test_form(hass, brand): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -29,13 +32,14 @@ async def test_form(hass): ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + {CONF_BRAND: brand, CONF_USERNAME: "username", CONF_PASSWORD: "password"}, ) await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "myhouse" assert result2["data"] == { + CONF_BRAND: brand, CONF_USERNAME: "username", CONF_PASSWORD: "password", } @@ -51,7 +55,11 @@ async def test_form_invalid_auth(hass): with patch("homeassistant.components.nexia.config_flow.NexiaHome.login"): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + { + CONF_BRAND: BRAND_NEXIA, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, ) assert result2["type"] == "form" @@ -70,7 +78,11 @@ async def test_form_cannot_connect(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + { + CONF_BRAND: BRAND_NEXIA, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, ) assert result2["type"] == "form" @@ -91,7 +103,11 @@ async def test_form_invalid_auth_http_401(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + { + CONF_BRAND: BRAND_NEXIA, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, ) assert result2["type"] == "form" @@ -112,7 +128,11 @@ async def test_form_cannot_connect_not_found(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + { + CONF_BRAND: BRAND_NEXIA, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, ) assert result2["type"] == "form" @@ -131,7 +151,11 @@ async def test_form_broad_exception(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + { + CONF_BRAND: BRAND_NEXIA, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, ) assert result2["type"] == "form" diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index 8e132941994..b6d5c697a18 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -21,17 +21,18 @@ async def async_init_integration( house_fixture = "nexia/mobile_houses_123456.json" session_fixture = "nexia/session_123456.json" sign_in_fixture = "nexia/sign_in.json" + nexia = NexiaHome(auto_login=False) with requests_mock.mock() as m, patch( "nexia.home.load_or_create_uuid", return_value=uuid.uuid4() ): - m.post(NexiaHome.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture)) + m.post(nexia.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture)) m.get( - NexiaHome.API_MOBILE_HOUSES_URL.format(house_id=123456), + nexia.API_MOBILE_HOUSES_URL.format(house_id=123456), text=load_fixture(house_fixture), ) m.post( - NexiaHome.API_MOBILE_ACCOUNTS_SIGN_IN_URL, + nexia.API_MOBILE_ACCOUNTS_SIGN_IN_URL, text=load_fixture(sign_in_fixture), ) entry = MockConfigEntry( From d84962bada1494ec3abb2be09d626a89082e97b0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 15 May 2021 18:24:34 +0200 Subject: [PATCH 455/852] Fix smhi retry (#50673) --- homeassistant/components/smhi/weather.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 5458abb1786..0fd808d1401 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -134,7 +134,9 @@ class SmhiWeather(WeatherEntity): async def retry_update(self, _): """Retry refresh weather forecast.""" - await self.async_update() + await self.async_update( # pylint: disable=unexpected-keyword-arg + no_throttle=True + ) async def get_weather_forecast(self) -> []: """Return the current forecasts from SMHI API.""" From 7b5fff357edc53ef6e63183b753b60c532c4d232 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 15 May 2021 12:24:52 -0400 Subject: [PATCH 456/852] Add targets and selectors for services (X-Z) (#50639) * Add targets and selectors for services (X-Z) * Adjustments --- .../components/xiaomi_aqara/services.yaml | 34 +- .../components/xiaomi_miio/services.yaml | 454 ++++++++++++++---- homeassistant/components/yamaha/services.yaml | 31 +- .../components/yeelight/services.yaml | 180 +++++-- homeassistant/components/zone/services.yaml | 1 + .../components/zoneminder/services.yaml | 5 + homeassistant/components/zwave/services.yaml | 278 +++++++++-- 7 files changed, 802 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/services.yaml b/homeassistant/components/xiaomi_aqara/services.yaml index 9d8c87e5863..75a9b9156c1 100644 --- a/homeassistant/components/xiaomi_aqara/services.yaml +++ b/homeassistant/components/xiaomi_aqara/services.yaml @@ -1,39 +1,71 @@ add_device: + name: Add device description: Enables the join permission of the Xiaomi Aqara Gateway for 30 seconds. A new device can be added afterwards by pressing the pairing button once. fields: gw_mac: + name: Gateway MAC description: MAC address of the Xiaomi Aqara Gateway. + required: true example: 34ce00880088 + selector: + text: play_ringtone: + name: play ringtone description: Play a specific ringtone. The version of the gateway firmware must be 1.4.1_145 at least. fields: gw_mac: + name: Gateway MAC description: MAC address of the Xiaomi Aqara Gateway. + required: true example: 34ce00880088 + selector: + text: ringtone_id: + name: Ringtone ID description: One of the allowed ringtone ids. + required: true example: 8 + selector: + text: ringtone_vol: + name: Ringtone volume description: The volume in percent. - example: 30 + selector: + number: + min: 0 + max: 100 remove_device: + name: Remove device description: Removes a specific device. The removal is required if a device shall be paired with another gateway. fields: device_id: + name: Device ID description: Hardware address of the device to remove. + required: true example: 158d0000000000 + selector: + text: gw_mac: + name: Gateway MAC description: MAC address of the Xiaomi Aqara Gateway. + required: true example: 34ce00880088 + selector: + text: stop_ringtone: + name: Stop ringtone description: Stops a playing ringtone immediately. fields: gw_mac: + name: Gateway MAC description: MAC address of the Xiaomi Aqara Gateway. + required: true example: 34ce00880088 + selector: + text: diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index f0312f01991..90d31765307 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -1,365 +1,621 @@ fan_set_buzzer_on: + name: Fan set buzzer on description: Turn the buzzer on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_buzzer_off: + name: Fan set buzzer off description: Turn the buzzer off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_led_on: + name: Fan set LED on description: Turn the led on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_led_off: + name: Fan set LED off description: Turn the led off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_child_lock_on: + name: Fan set child lock on description: Turn the child lock on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_child_lock_off: + name: Fan set child lock off description: Turn the child lock off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_favorite_level: + name: Fan set favorite level description: Set the favorite level. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan level: - description: Level, between 0 and 16. - example: 1 + name: Level + description: Level. + required: true + selector: + number: + min: 0 + max: 17 fan_set_fan_level: + name: Fan set level description: Set the fan level. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan level: - description: Level, between 1 and 3. - example: 1 + name: Level + description: Level. + selector: + number: + min: 1 + max: 3 fan_set_led_brightness: + name: Fan set LED brightness description: Set the led brightness. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan brightness: description: Brightness (0 = Bright, 1 = Dim, 2 = Off) - example: 1 + required: true + selector: + number: + min: 0 + max: 2 fan_set_auto_detect_on: + name: Fan set auto detect on description: Turn the auto detect on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_auto_detect_off: + name: Fan set auto detect off description: Turn the auto detect off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_learn_mode_on: + name: Fan set learn mode on description: Turn the learn mode on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_learn_mode_off: + name: Fan set learn mode off description: Turn the learn mode off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_volume: + name: Fan set volume description: Set the sound volume. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan volume: - description: Volume, between 0 and 100. - example: 50 + description: Volume. + required: true + selector: + number: + min: 0 + max: 100 fan_reset_filter: + name: Fan reset filter description: Reset the filter lifetime and usage. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_extra_features: + name: Fan set extra features description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan features: + name: Features description: Integer, known values are 0 (default) and 1 (turbo mode). - example: 1 + required: true + selector: + number: + min: 0 + max: 1 fan_set_target_humidity: + name: Fan set target humidity description: Set the target humidity. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan humidity: - description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. - example: 50 + name: Humidity + description: Target humidity. + required: true + selector: + number: + min: 30 + max: 80 + step: 10 + unit_of_measurement: '%' fan_set_dry_on: + name: Fan set dry on description: Turn the dry mode on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_dry_off: + name: Fan set dry off description: Turn the dry mode off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_motor_speed: + name: Fan set motor speed description: Set the target motor speed. fields: entity_id: description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' + selector: + entity: + integration: xiaomi_miio + domain: fan motor_speed: - description: Set RPM of motor speed, between 200 and 2000. - example: 1100 + name: Motor speed + description: Set motor speed. + required: true + selector: + number: + min: 200 + max: 2000 + unit_of_measurement: 'RPM' light_set_scene: + name: Light set scene description: Set a fixed scene. fields: entity_id: description: Name of the light entity. - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light scene: - description: Number of the fixed scene, between 1 and 4. - example: 1 + name: Scene + description: Number of the fixed scene. + required: true + selector: + number: + min: 1 + max: 6 light_set_delayed_turn_off: + name: Light set delayed turn off description: Delayed turn off. fields: entity_id: description: Name of the light entity. - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light time_period: + name: Time period description: Time period for the delayed turn off. + required: true example: "5, '0:05', {'minutes': 5}" + selector: + object: light_reminder_on: + name: Light reminder on description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light light_reminder_off: + name: Light reminder off description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light light_night_light_mode_on: + name: Night light mode on description: Turn the eyecare mode on (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light light_night_light_mode_off: + name: Night light mode off description: Turn the eyecare mode fan_set_dry_off (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light light_eyecare_mode_on: + name: Light eyecare mode on description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light light_eyecare_mode_off: + name: Light eyecare mode off description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light remote_learn_command: + name: Remote learn command description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' + target: + entity: + integration: xiaomi_miio + domain: remote fields: - entity_id: - description: "Name of the entity to learn command from." - example: "remote.xiaomi_miio" slot: - description: "Define the slot used to save the IR command (Value from 1 to 1000000)" - example: "1" + name: Slot + description: "Define the slot used to save the IR command." + default: 1 + selector: + number: + min: 1 + max: 1000000 timeout: - description: "Define the timeout in seconds, before which the command must be learned." - example: "30" + name: Timeout + description: "Define the timeout, before which the command must be learned." + default: 10 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds remote_set_led_on: + name: Remote set LED on description: 'Turn on blue LED.' - fields: - entity_id: - description: "Name of the entity to turn LED on." - example: "remote.xiaomi_miio" + target: + entity: + integration: xiaomi_miio + domain: remote remote_set_led_off: + name: Remote set LED off description: 'Turn off blue LED.' - fields: - entity_id: - description: "Name of the entity to turn LED off." - example: "remote.xiaomi_miio" + target: + entity: + integration: xiaomi_miio + domain: remote switch_set_wifi_led_on: + name: Switch set Wi-fi LED on description: Turn the wifi led on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "switch.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: switch switch_set_wifi_led_off: + name: Switch set Wi-fi LED off description: Turn the wifi led off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "switch.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: switch switch_set_power_price: + name: Switch set power price description: Set the power price. fields: entity_id: description: Name of the xiaomi miio entity. - example: "switch.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: switch mode: - description: Power price, between 0 and 999. - example: 31 + name: Mode + description: Power price. + required: true + selector: + number: + min: 0 + max: 999 switch_set_power_mode: + name: Switch set power mode description: Set the power mode. fields: entity_id: description: Name of the xiaomi miio entity. - example: "switch.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: switch mode: - description: Power mode, valid values are 'normal' and 'green'. - example: "green" + name: Mode + description: Power mode. + required: true + selector: + select: + options: + - 'green' + - 'normal' vacuum_remote_control_start: + name: Vacuum remote control start description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + target: + entity: + integration: xiaomi_miio + domain: vacuum vacuum_remote_control_stop: + name: Vacuum remote control stop description: Stop remote control mode of the vacuum cleaner. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + target: + entity: + integration: xiaomi_miio + domain: vacuum vacuum_remote_control_move: + name: Vacuum remote control move description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`. + target: + entity: + integration: xiaomi_miio + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" velocity: - description: Speed, between -0.29 and 0.29. - example: "0.2" + name: Velocity + description: Speed. + selector: + number: + min: -0.29 + max: 0.29 + step: 0.01 rotation: + name: Rotation description: Rotation, between -179 degrees and 179 degrees. - example: "90" + selector: + number: + min: -179 + max: 179 + unit_of_measurement: '°' duration: + name: Duration description: Duration of the movement. - example: "1500" + selector: + number: + min: 1 + max: 86400 + unit_of_measurement: seconds vacuum_remote_control_move_step: + name: Vacuum remote control move step description: Remote control the vacuum cleaner, only makes one move and then stops. + target: + entity: + integration: xiaomi_miio + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" velocity: - description: Speed, between -0.29 and 0.29. - example: "0.2" + name: Velocity + description: Speed. + selector: + number: + min: -0.29 + max: 0.29 + step: 0.01 rotation: - description: Rotation, between -179 degrees and 179 degrees. - example: "90" + name: Rotation + description: Rotation. + selector: + number: + min: -179 + max: 179 + unit_of_measurement: '°' duration: + name: Duration description: Duration of the movement. - example: "1500" + selector: + number: + min: 1 + max: 86400 + unit_of_measurement: seconds vacuum_clean_zone: + name: Vacuum clean zone description: Start the cleaning operation in the selected areas for the number of repeats indicated. + target: + entity: + integration: xiaomi_miio + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" zone: + name: Zone description: Array of zones. Each zone is an array of 4 integer values. example: "[[23510,25311,25110,26362]]" + selector: + object: repeats: - description: Number of cleaning repeats for each zone between 1 and 3. - example: "1" + name: Repeats + description: Number of cleaning repeats for each zone. + selector: + number: + min: 1 + max: 3 vacuum_goto: + name: Vacuum go to description: Go to the specified coordinates. + target: + entity: + integration: xiaomi_miio + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" x_coord: + name: X coordinate description: x-coordinate. example: 27500 + selector: + text: y_coord: + name: Y coordinate description: y-coordinate. example: 32000 + selector: + text: vacuum_clean_segment: + name: Vacuum clean segment description: Start cleaning of the specified segment(s). + target: + entity: + integration: xiaomi_miio + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" segments: + name: Segments description: Segments. example: "[1,2]" + selector: + object: diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml index e4d85885d54..fe2b2c66384 100644 --- a/homeassistant/components/yamaha/services.yaml +++ b/homeassistant/components/yamaha/services.yaml @@ -1,21 +1,36 @@ enable_output: + name: Enable output description: Enable or disable an output port + target: + entity: + integration: yamaha + domain: media_player fields: - entity_id: - description: Name(s) of entities to enable/disable port on. - example: "media_player.yamaha" port: + name: Port description: Name of port to enable/disable. + required: true example: "hdmi1" + selector: + text: enabled: - description: Boolean indicating if port should be enabled or not. - example: true + name: Enabled + description: Indicate if port should be enabled or not. + required: true + selector: + boolean: select_scene: + name: Select scene description: "Select a scene on the receiver" + target: + entity: + integration: yamaha + domain: media_player fields: - entity_id: - description: Name(s) of entities to enable/disable port on. - example: "media_player.yamaha" scene: + name: Scene description: Name of the scene. Standard for RX-V437 is 'BD/DVD Movie Viewing', 'TV Viewing', 'NET Audio Listening' or 'Radio Listening' + required: true example: "TV Viewing" + selector: + text: diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml index b519d0c91d9..92b184d0497 100644 --- a/homeassistant/components/yeelight/services.yaml +++ b/homeassistant/components/yeelight/services.yaml @@ -1,96 +1,194 @@ set_mode: + name: Set mode description: Set a operation mode. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" mode: - description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'. - example: "moonlight" + name: Mode + description: Operation mode. + required: true + selector: + select: + options: + - 'color_flow' + - 'hsv' + - 'last' + - 'moonlight' + - 'normal' + - 'rgb' + set_color_scene: + name: Set color scene description: Changes the light to the specified RGB color and brightness. If the light is off, it will be turned on. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" rgb_color: + name: RGB color description: Color for the light in RGB-format. example: "[255, 100, 100]" + selector: + object: brightness: - description: The brightness value to set (1-100). - example: 50 + name: Brightness + description: The brightness value to set. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" set_hsv_scene: + name: Set HSV scene description: Changes the light to the specified HSV color and brightness. If the light is off, it will be turned on. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" hs_color: + name: Hue/sat color description: Color for the light in hue/sat format. Hue is 0-359 and Sat is 0-100. example: "[300, 70]" + selector: + object: brightness: - description: The brightness value to set (1-100). - example: 50 + name: Brightness + description: The brightness value to set. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" set_color_temp_scene: + name: Set color temperature scene description: Changes the light to the specified color temperature. If the light is off, it will be turned on. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" kelvin: + name: Kelvin description: Color temperature for the light in Kelvin. example: 4000 + selector: + number: + min: 1700 + max: 6500 + step: 100 + unit_of_measurement: K brightness: - description: The brightness value to set (1-100). - example: 50 + name: Brightness + description: The brightness value to set. + selector: + number: + min: 0 + max: 100 set_color_flow_scene: + name: Set color flow scene description: starts a color flow. If the light is off, it will be turned on. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" count: + name: Count description: The number of times to run this flow (0 to run forever). - example: 0 + default: 0 + selector: + number: + min: 0 + max: 100 action: - description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover') + name: Action + description: The action to take after the flow stops. example: "stay" + default: 'recover' + selector: + select: + options: + - 'off' + - 'recover' + - 'stay' transitions: + name: Transitions description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' + selector: + object: set_auto_delay_off_scene: + name: Set auto delay off scene description: Turns the light on to the specified brightness and sets a timer to turn it back off after the given number of minutes. If the light is off, Set a color scene, if light is off, it will be turned on. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" minutes: - description: The minutes to wait before automatically turning the light off. + name: Minutes + description: The time to wait before automatically turning the light off. example: 5 + selector: + number: + min: 1 + max: 60 + unit_of_measurement: minutes brightness: - description: The brightness value to set (1-100). - example: 50 + name: Brightness + description: The brightness value to set. + selector: + number: + min: 0 + max: 100 start_flow: + name: Start flow description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" count: + name: Count description: The number of times to run this flow (0 to run forever). - example: 0 + default: 0 + selector: + number: + min: 0 + max: 100 action: - description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover') - example: "stay" + name: Action + description: The action to take after the flow stops. + default: 'recover' + selector: + select: + options: + - 'off' + - 'recover' + - 'stay' transitions: + name: Transitions description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' + selector: + object: set_music_mode: + name: Set music mode description: Enable or disable music_mode + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" music_mode: + name: Music mode description: Use true or false to enable / disable music_mode - example: true + required: true + selector: + boolean: diff --git a/homeassistant/components/zone/services.yaml b/homeassistant/components/zone/services.yaml index 550eee24fab..2ce77132a53 100644 --- a/homeassistant/components/zone/services.yaml +++ b/homeassistant/components/zone/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload the YAML-based zone configuration. diff --git a/homeassistant/components/zoneminder/services.yaml b/homeassistant/components/zoneminder/services.yaml index a6fb85b641d..74ab0cf5945 100644 --- a/homeassistant/components/zoneminder/services.yaml +++ b/homeassistant/components/zoneminder/services.yaml @@ -1,6 +1,11 @@ set_run_state: + name: Set run state description: Set the ZoneMinder run state fields: name: + name: Name description: The string name of the ZoneMinder run state to set as active. + required: true example: "Home" + selector: + text: diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index b4a5db58986..db74292ff8a 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -1,196 +1,410 @@ # Describes the format for available Z-Wave services change_association: + name: Change association description: Change an association in the Z-Wave network. fields: association: + name: Association description: Specify add or remove association + required: true example: add + selector: + text: node_id: + name: Node ID description: Node id of the node to set association for. + required: true example: 10 + selector: + number: + min: 1 + max: 255 target_node_id: + name: Target node ID description: Node id of the node to associate to. + required: true example: 42 + selector: + number: + min: 1 + max: 255 group: + name: Group description: Group number to set association for. + required: true + selector: + number: + min: 1 + max: 5 instance: - description: (Optional) Instance of multichannel association. Defaults to 0. + name: Instance + description: Instance of multichannel association. + default: 0 + selector: + number: + min: 0 + max: 255 add_node: + name: Add node description: Add a new (unsecure) node to the Z-Wave network. Refer to OZW_Log.txt for progress. add_node_secure: + name: Add node secure description: Add a new node to the Z-Wave network with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. Refer to OZW_Log.txt for progress. cancel_command: + name: Cancel command description: Cancel a running Z-Wave controller command. Use this to exit add_node, if you weren't going to use it but activated it. heal_network: + name: Heal network description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW_Log.txt for progress. fields: return_routes: - description: Whether or not to update the return routes from the nodes to the controller. Defaults to False. - example: true + name: Return routes + description: Whether or not to update the return routes from the nodes to the controller. + default: false + selector: + boolean: heal_node: + name: Heal node description: Start a Z-Wave node heal. Refer to OZW_Log.txt for progress. fields: return_routes: - description: Whether or not to update the return routes from the node to the controller. Defaults to False. - example: true + name: Return routes + description: Whether or not to update the return routes from the node to the controller. + default: false + selector: + boolean: remove_node: + name: Remove node description: Remove a node from the Z-Wave network. Refer to OZW_Log.txt for progress. remove_failed_node: + name: Remove failed node description: This command will remove a failed node from the network. The node should be on the controller's failed nodes list, otherwise this command will fail. Refer to OZW_Log.txt for progress. fields: node_id: - description: Node id of the device to remove (integer). - example: 10 + name: Node ID + description: Node id of the device to remove. + required: true + selector: + number: + min: 1 + max: 255 replace_failed_node: + name: Replace failed node description: Replace a failed node with another. If the node is not in the controller's failed nodes list, or the node responds, this command will fail. Refer to OZW_Log.txt for progress. fields: node_id: - description: Node id of the device to replace (integer). - example: 10 + name: Node ID + description: Node id of the device to replace. + required: true + selector: + number: + min: 1 + max: 255 set_config_parameter: + name: Set config parameter description: Set a config parameter to a node on the Z-Wave network. fields: node_id: - description: Node id of the device to set config parameter to (integer). + name: Node ID + description: Node id of the device to set config parameter to. + required: true + selector: + number: + min: 1 + max: 255 parameter: - description: Parameter number to set (integer). + name: Parameter + description: Parameter number to set. + required: true + selector: + number: + min: 1 + max: 255 value: + name: Value description: Value to set for parameter. (String value for list and bool parameters, integer for others). + required: true + selector: + text: size: - description: (Optional) Set the size of the parameter value. Only needed if no parameters are available. + name: Size + description: Set the size of the parameter value. Only needed if no parameters are available. + default: 2 + selector: + number: + min: 1 + max: 255 set_node_value: + name: Set node value description: Set the value for a given value_id on a Z-Wave device. fields: node_id: - description: Node id of the device to set the value on (integer). + name: Node ID + description: Node id of the device to set the value on. + required: true + selector: + number: + min: 1 + max: 255 value_id: + name: Value ID description: Value id of the value to set (integer or string). + required: true + selector: + text: value: + name: Value description: Value to set (integer or string). + required: true + selector: + text: refresh_node_value: + name: Refresh node value description: Refresh the value for a given value_id on a Z-Wave device. fields: node_id: - description: Node id of the device to refresh value from (integer). + name: Node ID + description: Node id of the device to refresh value from. + required: true + selector: + number: + min: 1 + max: 255 value_id: + name: Value ID description: Value id of the value to refresh. + required: true + selector: + text: set_poll_intensity: + name: Set poll intensity description: Set the polling interval to a nodes value fields: node_id: + name: Node ID description: ID of the node to set polling to. + required: true example: 10 value_id: + name: Value ID description: ID of the value to set polling to. example: 72037594255792737 + required: true + selector: + text: poll_intensity: + name: Poll intensity description: The intensity to poll, 0 = disabled, 1 = Every time through list, 2 = Every second time through list... - example: 2 + required: true + selector: + number: + min: 0 + max: 100 print_config_parameter: + name: Print configuration parameter description: Prints a Z-Wave node config parameter value to log. fields: node_id: - description: Node id of the device to print the parameter from (integer). + name: Node ID + description: Node id of the device to print the parameter from. + required: true + selector: + number: + min: 1 + max: 255 parameter: - description: Parameter number to print (integer). + name: Parameter + description: Parameter number to print. + required: true + selector: + number: + min: 1 + max: 255 print_node: + name: Print node description: Print all information about z-wave node. fields: node_id: + name: Node ID description: Node id of the device to print. + required: true + selector: + number: + min: 1 + max: 255 refresh_entity: + name: Refresh entity description: Refresh zwave entity. fields: entity_id: + name: Entity description: Name of the entity to refresh. - example: "light.leviton_vrmx11lz_multilevel_scene_switch_level_40" + required: true + selector: + entity: + integration: zwave refresh_node: + name: Refresh node description: Refresh zwave node. fields: node_id: + name: Node ID description: ID of the node to refresh. - example: 10 + required: true + selector: + number: + min: 1 + max: 255 set_wakeup: + name: Set wakeup description: Sets wake-up interval of a node. fields: node_id: - description: Node id of the device to set the wake-up interval for. (integer) + name: Node ID + description: Node id of the device to set the wake-up interval for. + required: true + selector: + number: + min: 1 + max: 255 value: - description: Value of the interval to set. (integer) + name: Value + description: Value of the interval to set. + required: true + selector: + text: start_network: + name: Start network description: Start the Z-Wave network. This might take a while, depending on how big your Z-Wave network is. stop_network: + name: Stop network description: Stop the Z-Wave network, all updates into Home Assistant will stop. soft_reset: + name: Soft reset description: This will reset the controller without removing its data. Use carefully because not all controllers support this. Refer to your controller's manual. test_network: + name: Test network description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW_Log.txt for progress. test_node: + name: Test node description: This will send test messages to a node in the Z-Wave network. This could bring back dead nodes. fields: node_id: + name: Node ID description: ID of the node to send test messages to. - example: 10 + required: true + selector: + number: + min: 1 + max: 255 messages: - description: Optional. Amount of test messages to send. - example: 3 + name: Messages + description: Amount of test messages to send. + default: 1 + selector: + number: + min: 1 + max: 100 rename_node: + name: Rename node description: Set the name of a node. This will also affect the IDs of all entities in the node. fields: node_id: + name: Node ID description: ID of the node to rename. - example: 10 + required: true + selector: + number: + min: 1 + max: 255 update_ids: - description: (optional) Rename the entity IDs for entities of this node. - example: true + name: Update IDs + description: Rename the entity IDs for entities of this node. + default: false + selector: + boolean: name: + name: Name description: New Name + required: true example: "kitchen" + selector: + text: rename_value: + name: Rename value description: Set the name of a node value. This will affect the ID of the value entity. Value IDs can be queried from /api/zwave/values/{node_id} fields: node_id: + name: Node ID description: ID of the node to rename. - example: 10 + required: true + selector: + number: + min: 1 + max: 255 value_id: + name: Value ID description: ID of the value to rename. example: 72037594255792737 + required: true + selector: + text: update_ids: - description: (optional) Update the entity ID for this value's entity. - example: true + name: Update IDs + description: Update the entity ID for this value's entity. + default: false + selector: + boolean: name: + name: Name description: New Name example: "Luminosity" + required: true + selector: + text: reset_node_meters: + name: Reset node meters description: Resets the meter counters of a node. fields: node_id: - description: Node id of the device to reset meters for. (integer) + name: Node ID + description: Node id of the device to reset meters for. + required: true + selector: + number: + min: 1 + max: 255 instance: - description: (Optional) Instance of association. Defaults to instance 1. + name: Instance + description: Instance of association. + default: 1 + selector: + number: + min: 1 + max: 100 From c1be4cbd79aacfb114734e6ddf7cd4682e19b69b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 18:28:03 +0200 Subject: [PATCH 457/852] Upgrade numpy to 1.20.3 (#50660) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 9c4cd3449a9..38ee883e559 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.20.2"], + "requirements": ["numpy==1.20.3"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 85131bebded..779e62de4fb 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.20.2", "pyiqvia==0.3.1"], + "requirements": ["numpy==1.20.3", "pyiqvia==0.3.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 7a64332c840..d011c485d42 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.20.2", "opencv-python-headless==4.4.0.42"], + "requirements": ["numpy==1.20.3", "opencv-python-headless==4.4.0.42"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index c4036e3cb3b..74c10af363d 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.3.0", "tf-models-official==2.3.0", "pycocotools==2.0.1", - "numpy==1.20.2", + "numpy==1.20.3", "pillow==8.1.2" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 594a327f266..508ce659a4a 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.20.2"], + "requirements": ["numpy==1.20.3"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 4ab0c5b0b3a..739e20d31fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1034,7 +1034,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.20.2 +numpy==1.20.3 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5039df65ab2..4e9e715884a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -570,7 +570,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.20.2 +numpy==1.20.3 # homeassistant.components.google oauth2client==4.0.0 From f142f292550e93c9c7f1c1a9c1cde32e18c7e1f9 Mon Sep 17 00:00:00 2001 From: Sascha Sander Date: Sat, 15 May 2021 18:54:12 +0200 Subject: [PATCH 458/852] Add PV3 / DC3 sensors to Kostal Plenticore (#50614) Co-authored-by: Franck Nijhof --- .../components/kostal_plenticore/const.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 8342ff74ada..5c223f4f5d6 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -114,6 +114,13 @@ SENSOR_PROCESS_DATA = [ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, "format_round", ), + ( + "devices:local:pv3", + "P", + "DC3 Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), ( "devices:local", "PV2Bat_P", @@ -445,6 +452,46 @@ SENSOR_PROCESS_DATA = [ }, "format_energy", ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv3:Day", + "Energy PV3 Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv3:Month", + "Energy PV3 Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv3:Year", + "Energy PV3 Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv3:Total", + "Energy PV3 Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), ( "scb:statistic:EnergyFlow", "Statistic:Yield:Day", From 97d7037d12bf59ea709daa3804550be5e1d318d5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 15 May 2021 19:36:08 +0200 Subject: [PATCH 459/852] Bump hatasmota to 0.2.13 (#50662) * Bump hatasmota to 0.2.13 * Process review comment Co-authored-by: Martin Hjelmare * Tweak brightness compensation, improve tests Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- homeassistant/components/tasmota/light.py | 62 +++--- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_common.py | 2 +- tests/components/tasmota/test_light.py | 206 +++++++++++++++++- 6 files changed, 241 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index 53db34a9001..58a1ff1fb23 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -55,6 +55,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) +def clamp(value): + """Clamp value to the range 0..255.""" + return min(max(value, 0), 255) + + class TasmotaLight( TasmotaAvailability, TasmotaDiscoveryUpdate, @@ -136,22 +141,7 @@ class TasmotaLight( percent_bright = brightness / TASMOTA_BRIGHTNESS_MAX self._brightness = percent_bright * 255 if "color" in attributes: - - def clamp(value): - """Clamp value to the range 0..255.""" - return min(max(value, 0), 255) - - rgb = attributes["color"] - # Tasmota's RGB color is adjusted for brightness, compensate - if self._brightness > 0: - red_compensated = clamp(round(rgb[0] / self._brightness * 255)) - green_compensated = clamp(round(rgb[1] / self._brightness * 255)) - blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) - else: - red_compensated = 0 - green_compensated = 0 - blue_compensated = 0 - self._rgb = [red_compensated, green_compensated, blue_compensated] + self._rgb = attributes["color"][0:3] if "color_temp" in attributes: self._color_temp = attributes["color_temp"] if "effect" in attributes: @@ -207,14 +197,38 @@ class TasmotaLight( @property def rgb_color(self): """Return the rgb color value.""" - return self._rgb + if self._rgb is None: + return None + rgb = self._rgb + # Tasmota's RGB color is adjusted for brightness, compensate + if self._brightness > 0: + red_compensated = clamp(round(rgb[0] / self._brightness * 255)) + green_compensated = clamp(round(rgb[1] / self._brightness * 255)) + blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) + else: + red_compensated = 0 + green_compensated = 0 + blue_compensated = 0 + return [red_compensated, green_compensated, blue_compensated] @property def rgbw_color(self): """Return the rgbw color value.""" if self._rgb is None or self._white_value is None: return None - return [*self._rgb, self._white_value] + rgb = self._rgb + # Tasmota's color is adjusted for brightness, compensate + if self._brightness > 0: + red_compensated = clamp(round(rgb[0] / self._brightness * 255)) + green_compensated = clamp(round(rgb[1] / self._brightness * 255)) + blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) + white_compensated = clamp(round(self._white_value / self._brightness * 255)) + else: + red_compensated = 0 + green_compensated = 0 + blue_compensated = 0 + white_compensated = 0 + return [red_compensated, green_compensated, blue_compensated, white_compensated] @property def force_update(self): @@ -250,18 +264,10 @@ class TasmotaLight( if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes: rgbw = kwargs[ATTR_RGBW_COLOR] + attributes["color"] = [rgbw[0], rgbw[1], rgbw[2], rgbw[3]] # Tasmota does not support direct RGBW control, the light must be set to # either white mode or color mode. Set the mode to white if white channel - # is on, and to color otheruse - if rgbw[3] == 0: - attributes["color"] = [rgbw[0], rgbw[1], rgbw[2]] - else: - white_value_normalized = rgbw[3] / DEFAULT_BRIGHTNESS_MAX - device_white_value = min( - round(white_value_normalized * TASMOTA_BRIGHTNESS_MAX), - TASMOTA_BRIGHTNESS_MAX, - ) - attributes["white_value"] = device_white_value + # is on, and to color otherwise if ATTR_TRANSITION in kwargs: attributes["transition"] = kwargs[ATTR_TRANSITION] diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a6e7a1d45a8..15b5501adce 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.12"], + "requirements": ["hatasmota==0.2.13"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 739e20d31fb..97955e8888a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.12 +hatasmota==0.2.13 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e9e715884a..db90816d3f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -411,7 +411,7 @@ hangups==0.4.11 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.12 +hatasmota==0.2.13 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index e535787411f..9174060ef93 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -36,7 +36,7 @@ DEFAULT_CONFIG = { "ofln": "Offline", "onln": "Online", "state": ["OFF", "ON", "TOGGLE", "HOLD"], - "sw": "8.4.0.2", + "sw": "9.4.0.4", "swn": [None, None, None, None, None], "t": "tasmota_49A3BC", "ft": "%topic%/%prefix%/", diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 3a27409e433..b74799d1d12 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -197,7 +197,7 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 2 - config["lt_st"] = 4 # 5 channel light (RGBW) + config["lt_st"] = 4 # 4 channel light (RGBW) mac = config["mac"] async_fire_mqtt_message( @@ -406,6 +406,99 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("color_mode") == "color_temp" +async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 4 # 4 channel light (RGBW) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "rgbw" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert "color_mode" not in state.attributes + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "rgbw" + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Color":"128,64,0","White":0}', + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("rgb_color") == (255, 128, 0) + assert state.attributes.get("rgbw_color") == (255, 128, 0, 0) + assert state.attributes.get("color_mode") == "rgbw" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("rgb_color") == (255, 192, 128) + assert state.attributes.get("rgbw_color") == (255, 128, 0, 255) + assert state.attributes.get("color_mode") == "rgbw" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":0}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 0 + assert state.attributes.get("rgb_color") == (0, 0, 0) + assert state.attributes.get("rgbw_color") == (0, 0, 0, 0) + assert state.attributes.get("color_mode") == "rgbw" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -667,7 +760,17 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Dimmer":0}', + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (0, 0, 0) + assert state.attributes.get("color_mode") == "rgb" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":50}' ) state = hass.states.get("light.test") assert state.state == STATE_ON @@ -799,9 +902,10 @@ async def test_sending_mqtt_commands_rgbww_tuya(hass, mqtt_mock, setup_tasmota): ) -async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): +async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) + config["sw"] = "9.4.0.3" # RGBW support was added in 9.4.0.4 config["rl"][0] = 2 config["lt_st"] = 4 # 4 channel light (RGBW) mac = config["mac"] @@ -895,6 +999,102 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.reset_mock() +async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 4 # 4 channel light (RGBW) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT message is sent + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Tasmota is not optimistic, the state should still be off + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + # Turn the light off and verify MQTT message is sent + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT messages are sent + await common.async_turn_on(hass, "light.test", brightness=192) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer4 75", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set color when setting color + await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set color when setting white is off + await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Color2 128,64,32,0", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set white when white is on + await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Color2 16,64,32,128", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", white_value=128) + # white_value should be ignored + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", effect="Random") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Scheme 4", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) From de77e0be8cb5893adf329e78d908f00195c04ad5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 19:41:17 +0200 Subject: [PATCH 460/852] Upgrade pylint to 2.8.2 (#50669) --- homeassistant/components/sentry/__init__.py | 1 + requirements_test.txt | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 8a87cba84d7..3cd209dfaf3 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -76,6 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), } + # pylint: disable=abstract-class-instantiated sentry_sdk.init( dsn=entry.data[CONF_DSN], environment=entry.options.get(CONF_ENVIRONMENT), diff --git a/requirements_test.txt b/requirements_test.txt index 3386f3561ce..b12842c78dd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,8 +10,7 @@ jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.812 pre-commit==2.12.1 -pylint==2.8.0 -astroid==2.5.5 +pylint==2.8.2 pipdeptree==1.0.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From aa6b26c9ff82870b8d094b304c5a63c14cee5651 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 19:41:34 +0200 Subject: [PATCH 461/852] Upgrade defusedxml to 0.7.1 (#50671) --- homeassistant/components/ihc/manifest.json | 2 +- homeassistant/components/namecheapdns/manifest.json | 2 +- homeassistant/components/ohmconnect/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index 3aaa8f2fb77..b90b0fc022b 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -2,7 +2,7 @@ "domain": "ihc", "name": "IHC Controller", "documentation": "https://www.home-assistant.io/integrations/ihc", - "requirements": ["defusedxml==0.6.0", "ihcsdk==2.7.0"], + "requirements": ["defusedxml==0.7.1", "ihcsdk==2.7.0"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/namecheapdns/manifest.json b/homeassistant/components/namecheapdns/manifest.json index 7b94b09885d..128d7feaccb 100644 --- a/homeassistant/components/namecheapdns/manifest.json +++ b/homeassistant/components/namecheapdns/manifest.json @@ -2,7 +2,7 @@ "domain": "namecheapdns", "name": "Namecheap FreeDNS", "documentation": "https://www.home-assistant.io/integrations/namecheapdns", - "requirements": ["defusedxml==0.6.0"], + "requirements": ["defusedxml==0.7.1"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/homeassistant/components/ohmconnect/manifest.json b/homeassistant/components/ohmconnect/manifest.json index d2ee9bc70cd..08eaba422bb 100644 --- a/homeassistant/components/ohmconnect/manifest.json +++ b/homeassistant/components/ohmconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "ohmconnect", "name": "OhmConnect", "documentation": "https://www.home-assistant.io/integrations/ohmconnect", - "requirements": ["defusedxml==0.6.0"], + "requirements": ["defusedxml==0.7.1"], "codeowners": ["@robbiet480"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 54cec45f1d0..348f8f3a46c 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -3,7 +3,7 @@ "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ - "defusedxml==0.6.0", + "defusedxml==0.7.1", "netdisco==2.8.3", "async-upnp-client==0.17.0" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d485da19142..18421821d2a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 cryptography==3.3.2 -defusedxml==0.6.0 +defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 diff --git a/requirements_all.txt b/requirements_all.txt index 97955e8888a..601a892ea9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -479,7 +479,7 @@ debugpy==1.3.0 # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect # homeassistant.components.ssdp -defusedxml==0.6.0 +defusedxml==0.7.1 # homeassistant.components.deluge deluge-client==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db90816d3f4..fc912c71fb3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ debugpy==1.3.0 # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect # homeassistant.components.ssdp -defusedxml==0.6.0 +defusedxml==0.7.1 # homeassistant.components.denonavr denonavr==0.10.8 From 990b7c371fc089951c10dbd99852e26e654449d6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 19:41:48 +0200 Subject: [PATCH 462/852] Upgrade PyTurboJPEG to 1.5.0 (#50670) --- 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 0a23d52f17a..483279d55f3 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -7,7 +7,7 @@ "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", - "PyTurboJPEG==1.4.0" + "PyTurboJPEG==1.5.0" ], "dependencies": ["http", "camera", "ffmpeg"], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 601a892ea9e..76d322ba71c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -58,7 +58,7 @@ PySocks==1.7.1 PyTransportNSW==0.1.1 # homeassistant.components.homekit -PyTurboJPEG==1.4.0 +PyTurboJPEG==1.5.0 # homeassistant.components.vicare PyViCare==0.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc912c71fb3..aa777f4a63e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -27,7 +27,7 @@ PyRMVtransport==0.3.2 PyTransportNSW==0.1.1 # homeassistant.components.homekit -PyTurboJPEG==1.4.0 +PyTurboJPEG==1.5.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 From ad7be91b6a25ed889c0168995c9eaff0d3e55635 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 15 May 2021 19:54:17 +0200 Subject: [PATCH 463/852] Fix modbus blocking threads (#50619) Co-authored-by: Martin Hjelmare --- homeassistant/components/modbus/__init__.py | 6 +- .../components/modbus/binary_sensor.py | 19 +- homeassistant/components/modbus/climate.py | 31 +- homeassistant/components/modbus/cover.py | 53 ++-- homeassistant/components/modbus/modbus.py | 289 ++++++++---------- homeassistant/components/modbus/sensor.py | 17 +- homeassistant/components/modbus/switch.py | 53 ++-- tests/components/modbus/test_init.py | 143 ++++----- 8 files changed, 284 insertions(+), 327 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8b9e37aa1e4..ff70ccd6ad5 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -101,7 +101,7 @@ from .const import ( MODBUS_DOMAIN as DOMAIN, PLATFORMS, ) -from .modbus import modbus_setup +from .modbus import async_modbus_setup _LOGGER = logging.getLogger(__name__) @@ -350,8 +350,8 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass, config): """Set up Modbus component.""" - return modbus_setup( + return await async_modbus_setup( hass, config, SERVICE_WRITE_REGISTER_SCHEMA, SERVICE_WRITE_COIL_SCHEMA ) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 82b1db6dd1a..14d01535c5b 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -36,6 +36,7 @@ from .const import ( MODBUS_DOMAIN, ) +PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) @@ -114,9 +115,7 @@ class ModbusBinarySensor(BinarySensorEntity): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_track_time_interval( - self._hass, lambda arg: self.update(), self._scan_interval - ) + async_track_time_interval(self._hass, self.async_update, self._scan_interval) @property def name(self): @@ -148,17 +147,21 @@ class ModbusBinarySensor(BinarySensorEntity): """Return True if entity is available.""" return self._available - def update(self): + async def async_update(self, now=None): """Update the state of the sensor.""" + # remark "now" is a dummy parameter to avoid problems with + # async_track_time_interval if self._input_type == CALL_TYPE_COIL: - result = self._hub.read_coils(self._slave, self._address, 1) + result = await self._hub.async_read_coils(self._slave, self._address, 1) else: - result = self._hub.read_discrete_inputs(self._slave, self._address, 1) + result = await self._hub.async_read_discrete_inputs( + self._slave, self._address, 1 + ) if result is None: self._available = False - self.schedule_update_ha_state() + self.async_write_ha_state() return self._value = result.bits[0] & 1 self._available = True - self.schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index cc8f74577c7..43c0f0d05db 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -46,6 +46,7 @@ from .const import ( ) from .modbus import ModbusHub +PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) @@ -132,9 +133,7 @@ class ModbusThermostat(ClimateEntity): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_track_time_interval( - self.hass, lambda arg: self.update(), self._scan_interval - ) + async_track_time_interval(self.hass, self.async_update, self._scan_interval) @property def should_poll(self): @@ -160,7 +159,7 @@ class ModbusThermostat(ClimateEntity): """Return the possible HVAC modes.""" return [HVAC_MODE_AUTO] - def set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" # Home Assistant expects this method. # We'll keep it here to avoid getting exceptions. @@ -200,7 +199,7 @@ class ModbusThermostat(ClimateEntity): """Return the supported step of target temperature.""" return self._temp_step - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" if ATTR_TEMPERATURE not in kwargs: return @@ -209,35 +208,39 @@ class ModbusThermostat(ClimateEntity): ) byte_string = struct.pack(self._structure, target_temperature) register_value = struct.unpack(">h", byte_string[0:2])[0] - self._available = self._hub.write_registers( + self._available = await self._hub.async_write_registers( self._slave, self._target_temperature_register, register_value, ) - self.update() + self.async_update() @property def available(self) -> bool: """Return True if entity is available.""" return self._available - def update(self): + async def async_update(self, now=None): """Update Target & Current Temperature.""" - self._target_temperature = self._read_register( + # remark "now" is a dummy parameter to avoid problems with + # async_track_time_interval + self._target_temperature = await self._async_read_register( CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register ) - self._current_temperature = self._read_register( + self._current_temperature = await self._async_read_register( self._current_temperature_register_type, self._current_temperature_register ) - self.schedule_update_ha_state() + self.async_write_ha_state() - def _read_register(self, register_type, register) -> float | None: + async def _async_read_register(self, register_type, register) -> float | None: """Read register using the Modbus hub slave.""" if register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers(self._slave, register, self._count) + result = await self._hub.async_read_input_registers( + self._slave, register, self._count + ) else: - result = self._hub.read_holding_registers( + result = await self._hub.async_read_holding_registers( self._slave, register, self._count ) if result is None: diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 7d9bf9e2e45..edb81ae7eb3 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -33,6 +33,7 @@ from .const import ( ) from .modbus import ModbusHub +PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) @@ -106,9 +107,7 @@ class ModbusCover(CoverEntity, RestoreEntity): if state: self._value = state.state - async_track_time_interval( - self.hass, lambda arg: self.update(), self._scan_interval - ) + async_track_time_interval(self.hass, self.async_update, self._scan_interval) @property def device_class(self) -> str | None: @@ -154,41 +153,43 @@ class ModbusCover(CoverEntity, RestoreEntity): # Handle polling directly in this entity return False - def open_cover(self, **kwargs: Any) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" if self._coil is not None: - self._write_coil(True) + await self._async_write_coil(True) else: - self._write_register(self._state_open) + await self._async_write_register(self._state_open) - self.update() + self.async_update() - def close_cover(self, **kwargs: Any) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" if self._coil is not None: - self._write_coil(False) + await self._async_write_coil(False) else: - self._write_register(self._state_closed) + await self._async_write_register(self._state_closed) - self.update() + self.async_update() - def update(self): + async def async_update(self, now=None): """Update the state of the cover.""" + # remark "now" is a dummy parameter to avoid problems with + # async_track_time_interval if self._coil is not None and self._status_register is None: - self._value = self._read_coil() + self._value = await self._async_read_coil() else: - self._value = self._read_status_register() + self._value = await self._async_read_status_register() - self.schedule_update_ha_state() + self.async_write_ha_state() - def _read_status_register(self) -> int | None: + async def _async_read_status_register(self) -> int | None: """Read status register using the Modbus hub slave.""" if self._status_register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( + result = await self._hub.async_read_input_registers( self._slave, self._status_register, 1 ) else: - result = self._hub.read_holding_registers( + result = await self._hub.async_read_holding_registers( self._slave, self._status_register, 1 ) if result is None: @@ -200,13 +201,15 @@ class ModbusCover(CoverEntity, RestoreEntity): return value - def _write_register(self, value): + async def _async_write_register(self, value): """Write holding register using the Modbus hub slave.""" - self._available = self._hub.write_register(self._slave, self._register, value) + self._available = await self._hub.async_write_register( + self._slave, self._register, value + ) - def _read_coil(self) -> bool | None: + async def _async_read_coil(self) -> bool | None: """Read coil using the Modbus hub slave.""" - result = self._hub.read_coils(self._slave, self._coil, 1) + result = await self._hub.async_read_coils(self._slave, self._coil, 1) if result is None: self._available = False return None @@ -214,6 +217,8 @@ class ModbusCover(CoverEntity, RestoreEntity): value = bool(result.bits[0] & 1) return value - def _write_coil(self, value): + async def _async_write_coil(self, value): """Write coil using the Modbus hub slave.""" - self._available = self._hub.write_coil(self._slave, self._coil, value) + self._available = await self._hub.async_write_coil( + self._slave, self._coil, value + ) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 8610c6a855c..0f4266654a7 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -1,6 +1,6 @@ """Support for Modbus.""" +import asyncio import logging -import threading from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient from pymodbus.constants import Defaults @@ -17,8 +17,9 @@ from homeassistant.const import ( CONF_TYPE, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.event import call_later +from homeassistant.core import callback +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.event import async_call_later from .const import ( ATTR_ADDRESS, @@ -41,32 +42,37 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def modbus_setup( +async def async_modbus_setup( hass, config, service_write_register_schema, service_write_coil_schema ): """Set up Modbus component.""" hass.data[DOMAIN] = hub_collect = {} for conf_hub in config[DOMAIN]: - hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub) + my_hub = ModbusHub(hass, conf_hub) + hub_collect[conf_hub[CONF_NAME]] = my_hub # modbus needs to be activated before components are loaded # to avoid a racing problem - hub_collect[conf_hub[CONF_NAME]].setup(hass) + await my_hub.async_setup() # load platforms for component, conf_key in PLATFORMS: if conf_key in conf_hub: - load_platform(hass, component, DOMAIN, conf_hub, config) + hass.async_create_task( + async_load_platform(hass, component, DOMAIN, conf_hub, config) + ) - def stop_modbus(event): + async def async_stop_modbus(event): """Stop Modbus service.""" for client in hub_collect.values(): - client.close() + await client.async_close() del client - def write_register(service): + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_modbus) + + async def async_write_register(service): """Write Modbus registers.""" unit = int(float(service.data[ATTR_UNIT])) address = int(float(service.data[ATTR_ADDRESS])) @@ -75,13 +81,22 @@ def modbus_setup( service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB ) if isinstance(value, list): - hub_collect[client_name].write_registers( + await hub_collect[client_name].async_write_registers( unit, address, [int(float(i)) for i in value] ) else: - hub_collect[client_name].write_register(unit, address, int(float(value))) + await hub_collect[client_name].async_write_register( + unit, address, int(float(value)) + ) - def write_coil(service): + hass.services.async_register( + DOMAIN, + SERVICE_WRITE_REGISTER, + async_write_register, + schema=service_write_register_schema, + ) + + async def async_write_coil(service): """Write Modbus coil.""" unit = service.data[ATTR_UNIT] address = service.data[ATTR_ADDRESS] @@ -90,22 +105,12 @@ def modbus_setup( service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB ) if isinstance(state, list): - hub_collect[client_name].write_coils(unit, address, state) + await hub_collect[client_name].async_write_coils(unit, address, state) else: - hub_collect[client_name].write_coil(unit, address, state) + await hub_collect[client_name].async_write_coil(unit, address, state) - # register function to gracefully stop modbus - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) - - # Register services for modbus - hass.services.register( - DOMAIN, - SERVICE_WRITE_REGISTER, - write_register, - schema=service_write_register_schema, - ) - hass.services.register( - DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=service_write_coil_schema + hass.services.async_register( + DOMAIN, SERVICE_WRITE_COIL, async_write_coil, schema=service_write_coil_schema ) return True @@ -113,14 +118,15 @@ def modbus_setup( class ModbusHub: """Thread safe wrapper class for pymodbus.""" - def __init__(self, client_config): + def __init__(self, hass, client_config): """Initialize the Modbus hub.""" # generic configuration self._client = None - self._cancel_listener = None + self._async_cancel_listener = None self._in_error = False - self._lock = threading.Lock() + self._lock = asyncio.Lock() + self.hass = hass self._config_name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] self._config_port = client_config[CONF_PORT] @@ -152,7 +158,7 @@ class ModbusHub: _LOGGER.error(log_text) self._in_error = error_state - def setup(self, hass): + async def async_setup(self): """Set up pymodbus client.""" try: if self._config_type == "serial": @@ -193,166 +199,113 @@ class ModbusHub: self._log_error(exception_error, error_state=False) return - # Connect device - self.connect() + async with self._lock: + await self.hass.async_add_executor_job(self._pymodbus_connect) # Start counting down to allow modbus requests. if self._config_delay: - self._cancel_listener = call_later(hass, self._config_delay, self.end_delay) + self._async_cancel_listener = async_call_later( + self.hass, self._config_delay, self.async_end_delay + ) - def end_delay(self, args): + @callback + def async_end_delay(self, args): """End startup delay.""" - self._cancel_listener = None + self._async_cancel_listener = None self._config_delay = 0 - def close(self): + def _pymodbus_close(self): + """Close sync. pymodbus.""" + if self._client: + try: + self._client.close() + except ModbusException as exception_error: + self._log_error(exception_error) + self._client = None + + async def async_close(self): """Disconnect client.""" - if self._cancel_listener: - self._cancel_listener() - self._cancel_listener = None - with self._lock: - try: - if self._client: - self._client.close() - self._client = None - except ModbusException as exception_error: - self._log_error(exception_error) - return + if self._async_cancel_listener: + self._async_cancel_listener() + self._async_cancel_listener = None - def connect(self): + async with self._lock: + return await self.hass.async_add_executor_job(self._pymodbus_close) + + def _pymodbus_connect(self): """Connect client.""" - with self._lock: - try: - self._client.connect() - except ModbusException as exception_error: - self._log_error(exception_error, error_state=False) - return + try: + self._client.connect() + except ModbusException as exception_error: + self._log_error(exception_error, error_state=False) - def read_coils(self, unit, address, count): + def _pymodbus_call(self, unit, address, value, check_attr, func): + """Call sync. pymodbus.""" + kwargs = {"unit": unit} if unit else {} + try: + result = func(address, value, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + result = exception_error + if not hasattr(result, check_attr): + self._log_error(result) + return None + self._in_error = False + return result + + async def async_pymodbus_call(self, unit, address, value, check_attr, func): + """Convert async to sync pymodbus call.""" + if self._config_delay: + return None + async with self._lock: + return await self.hass.async_add_executor_job( + self._pymodbus_call, unit, address, value, check_attr, func + ) + + async def async_read_coils(self, unit, address, count): """Read coils.""" - if self._config_delay: - return None - with self._lock: - kwargs = {"unit": unit} if unit else {} - try: - result = self._client.read_coils(address, count, **kwargs) - except ModbusException as exception_error: - self._log_error(exception_error) - result = exception_error - if not hasattr(result, "bits"): - self._log_error(result) - return None - self._in_error = False - return result + return await self.async_pymodbus_call( + unit, address, count, "bits", self._client.read_coils + ) - def read_discrete_inputs(self, unit, address, count): + async def async_read_discrete_inputs(self, unit, address, count): """Read discrete inputs.""" - if self._config_delay: - return None - with self._lock: - kwargs = {"unit": unit} if unit else {} - try: - result = self._client.read_discrete_inputs(address, count, **kwargs) - except ModbusException as exception_error: - result = exception_error - if not hasattr(result, "bits"): - self._log_error(result) - return None - self._in_error = False - return result + return await self.async_pymodbus_call( + unit, address, count, "bits", self._client.read_discrete_inputs + ) - def read_input_registers(self, unit, address, count): + async def async_read_input_registers(self, unit, address, count): """Read input registers.""" - if self._config_delay: - return None - with self._lock: - kwargs = {"unit": unit} if unit else {} - try: - result = self._client.read_input_registers(address, count, **kwargs) - except ModbusException as exception_error: - result = exception_error - if not hasattr(result, "registers"): - self._log_error(result) - return None - self._in_error = False - return result + return await self.async_pymodbus_call( + unit, address, count, "registers", self._client.read_input_registers + ) - def read_holding_registers(self, unit, address, count): + async def async_read_holding_registers(self, unit, address, count): """Read holding registers.""" - if self._config_delay: - return None - with self._lock: - kwargs = {"unit": unit} if unit else {} - try: - result = self._client.read_holding_registers(address, count, **kwargs) - except ModbusException as exception_error: - result = exception_error - if not hasattr(result, "registers"): - self._log_error(result) - return None - self._in_error = False - return result + return await self.async_pymodbus_call( + unit, address, count, "registers", self._client.read_holding_registers + ) - def write_coil(self, unit, address, value) -> bool: + async def async_write_coil(self, unit, address, value) -> bool: """Write coil.""" - if self._config_delay: - return False - with self._lock: - kwargs = {"unit": unit} if unit else {} - try: - result = self._client.write_coil(address, value, **kwargs) - except ModbusException as exception_error: - result = exception_error - if not hasattr(result, "value"): - self._log_error(result) - return False - self._in_error = False - return True + return await self.async_pymodbus_call( + unit, address, value, "value", self._client.write_coil + ) - def write_coils(self, unit, address, values) -> bool: + async def async_write_coils(self, unit, address, values) -> bool: """Write coil.""" - if self._config_delay: - return False - with self._lock: - kwargs = {"unit": unit} if unit else {} - try: - result = self._client.write_coils(address, values, **kwargs) - except ModbusException as exception_error: - result = exception_error - if not hasattr(result, "count"): - self._log_error(result) - return False - self._in_error = False - return True + return await self.async_pymodbus_call( + unit, address, values, "count", self._client.write_coils + ) - def write_register(self, unit, address, value) -> bool: + async def async_write_register(self, unit, address, value) -> bool: """Write register.""" - if self._config_delay: - return False - with self._lock: - kwargs = {"unit": unit} if unit else {} - try: - result = self._client.write_register(address, value, **kwargs) - except ModbusException as exception_error: - result = exception_error - if not hasattr(result, "value"): - self._log_error(result) - return False - self._in_error = False - return True + return await self.async_pymodbus_call( + unit, address, value, "value", self._client.write_register + ) - def write_registers(self, unit, address, values) -> bool: + async def async_write_registers(self, unit, address, values) -> bool: """Write registers.""" - if self._config_delay: - return False - with self._lock: - kwargs = {"unit": unit} if unit else {} - try: - result = self._client.write_registers(address, values, **kwargs) - except ModbusException as exception_error: - result = exception_error - if not hasattr(result, "count"): - self._log_error(result) - return False - self._in_error = False - return True + return await self.async_pymodbus_call( + unit, address, values, "count", self._client.write_registers + ) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 91f80864f73..7aeb142d1e2 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -59,6 +59,7 @@ from .const import ( MODBUS_DOMAIN, ) +PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) @@ -226,9 +227,7 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): if state: self._value = state.state - async_track_time_interval( - self.hass, lambda arg: self.update(), self._scan_interval - ) + async_track_time_interval(self.hass, self.async_update, self._scan_interval) @property def state(self): @@ -280,19 +279,21 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): registers.reverse() return registers - def update(self): + async def async_update(self, now=None): """Update the state of the sensor.""" + # remark "now" is a dummy parameter to avoid problems with + # async_track_time_interval if self._register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( + result = await self._hub.async_read_input_registers( self._slave, self._register, self._count ) else: - result = self._hub.read_holding_registers( + result = await self._hub.async_read_holding_registers( self._slave, self._register, self._count ) if result is None: self._available = False - self.schedule_update_ha_state() + self.async_write_ha_state() return registers = self._swap_registers(result.registers) @@ -332,4 +333,4 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): self._value = f"{float(val):.{self._precision}f}" self._available = True - self.schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index c449c25bb22..4495a72c63a 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -34,6 +34,7 @@ from .const import ( ) from .modbus import ModbusHub +PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) @@ -62,11 +63,11 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) self._address = config[CONF_ADDRESS] if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: - self._write_func = self._hub.write_coil + self._async_write_func = self._hub.async_write_coil self._command_on = 0x01 self._command_off = 0x00 else: - self._write_func = self._hub.write_register + self._async_write_func = self._hub.async_write_register self._command_on = config[CONF_COMMAND_ON] self._command_off = config[CONF_COMMAND_OFF] if CONF_VERIFY in config: @@ -83,13 +84,13 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) if self._verify_type == CALL_TYPE_REGISTER_HOLDING: - self._read_func = self._hub.read_holding_registers + self._async_read_func = self._hub.async_read_holding_registers elif self._verify_type == CALL_TYPE_DISCRETE: - self._read_func = self._hub.read_discrete_inputs + self._async_read_func = self._hub.async_read_discrete_inputs elif self._verify_type == CALL_TYPE_REGISTER_INPUT: - self._read_func = self._hub.read_input_registers + self._async_read_func = self._hub.async_read_input_registers else: # self._verify_type == CALL_TYPE_COIL: - self._read_func = self._hub.read_coils + self._async_read_func = self._hub.async_read_coils else: self._verify_active = False @@ -99,9 +100,7 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): if state: self._is_on = state.state == STATE_ON - async_track_time_interval( - self.hass, lambda arg: self.update(), self._scan_interval - ) + async_track_time_interval(self.hass, self.async_update, self._scan_interval) @property def is_on(self): @@ -123,46 +122,52 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): """Return True if entity is available.""" return self._available - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Set switch on.""" - result = self._write_func(self._slave, self._address, self._command_on) + result = await self._async_write_func( + self._slave, self._address, self._command_on + ) if result is False: self._available = False - self.schedule_update_ha_state() + self.async_write_ha_state() else: self._available = True if self._verify_active: - self.update() + self.async_update() else: self._is_on = True - self.schedule_update_ha_state() + self.async_write_ha_state() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Set switch off.""" - result = self._write_func(self._slave, self._address, self._command_off) + result = await self._async_write_func( + self._slave, self._address, self._command_off + ) if result is False: self._available = False - self.schedule_update_ha_state() + self.async_write_ha_state() else: self._available = True if self._verify_active: - self.update() + self.async_update() else: self._is_on = False - self.schedule_update_ha_state() + self.async_write_ha_state() - def update(self): + async def async_update(self, now=None): """Update the entity state.""" + # remark "now" is a dummy parameter to avoid problems with + # async_track_time_interval if not self._verify_active: self._available = True - self.schedule_update_ha_state() + self.async_write_ha_state() return - result = self._read_func(self._slave, self._verify_address, 1) + result = await self._async_read_func(self._slave, self._verify_address, 1) if result is None: self._available = False - self.schedule_update_ha_state() + self.async_write_ha_state() return self._available = True @@ -182,4 +187,4 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): self._verify_address, value, ) - self.schedule_update_ha_state() + self.async_write_ha_state() diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 7c0c7453abb..8959fe82319 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -480,11 +480,13 @@ async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus): async def test_delay(hass, mock_pymodbus): - """Run test for different read.""" + """Run test for startup delay.""" # the purpose of this test is to test startup delay - # We "hijiack" binary_sensor and sensor in order - # to make a proper blackbox test. + # We "hijiack" a binary_sensor to make a proper blackbox test. + test_delay = 15 + test_scan_interval = 5 + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" config = { DOMAIN: [ { @@ -492,101 +494,86 @@ async def test_delay(hass, mock_pymodbus): CONF_HOST: "modbusTestHost", CONF_PORT: 5501, CONF_NAME: TEST_MODBUS_NAME, - CONF_DELAY: 15, + CONF_DELAY: test_delay, CONF_BINARY_SENSORS: [ { CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_NAME: f"{TEST_SENSOR_NAME}_2", + CONF_NAME: f"{TEST_SENSOR_NAME}", CONF_ADDRESS: 52, - CONF_SCAN_INTERVAL: 5, - }, - { - CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, - CONF_NAME: f"{TEST_SENSOR_NAME}_1", - CONF_ADDRESS: 51, - CONF_SCAN_INTERVAL: 5, - }, - ], - CONF_SENSORS: [ - { - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_NAME: f"{TEST_SENSOR_NAME}_3", - CONF_ADDRESS: 53, - CONF_SCAN_INTERVAL: 5, - }, - { - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_NAME: f"{TEST_SENSOR_NAME}_4", - CONF_ADDRESS: 54, - CONF_SCAN_INTERVAL: 5, + CONF_SCAN_INTERVAL: test_scan_interval, }, ], } ] } mock_pymodbus.read_coils.return_value = ReadResult([0x01]) - mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) - mock_pymodbus.read_holding_registers.return_value = ReadResult([7]) - mock_pymodbus.read_input_registers.return_value = ReadResult([7]) now = dt_util.utcnow() with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): assert await async_setup_component(hass, DOMAIN, config) is True await hass.async_block_till_done() - now = now + timedelta(seconds=10) + # pass first scan_interval + start_time = now + now = now + timedelta(seconds=(test_scan_interval + 1)) with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - # Check states - entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_1" - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_2" - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_3" - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_4" - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + stop_time = start_time + timedelta(seconds=(test_delay + 1)) + step_timedelta = timedelta(seconds=1) + while now < stop_time: + now = now + step_timedelta + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + now = now + step_timedelta + timedelta(seconds=2) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ON - mock_pymodbus.reset_mock() - data = { - ATTR_HUB: TEST_MODBUS_NAME, - ATTR_UNIT: 17, - ATTR_ADDRESS: 16, - ATTR_STATE: False, + +async def test_thread_lock(hass, mock_pymodbus): + """Run test for block of threads.""" + + # the purpose of this test is to test the threads are not being blocked + # We "hijiack" a binary_sensor to make a proper blackbox test. + test_scan_interval = 5 + sensors = [] + for i in range(200): + sensors.append( + { + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_NAME: f"{TEST_SENSOR_NAME}_{i}", + CONF_ADDRESS: 52 + i, + CONF_SCAN_INTERVAL: test_scan_interval, + } + ) + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_NAME: TEST_MODBUS_NAME, + CONF_BINARY_SENSORS: sensors, + } + ] } - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert not mock_pymodbus.write_coil.called - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert not mock_pymodbus.write_coil.called - data[ATTR_STATE] = [True, False, True] - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert not mock_pymodbus.write_coils.called - - del data[ATTR_STATE] - data[ATTR_VALUE] = 15 - await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) - assert not mock_pymodbus.write_register.called - data[ATTR_VALUE] = [1, 2, 3] - await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) - assert not mock_pymodbus.write_registers.called - - # 2 times fire_changed is needed to secure "normal" update is called. - now = now + timedelta(seconds=6) + mock_pymodbus.read_coils.return_value = ReadResult([0x01]) + now = dt_util.utcnow() with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) + assert await async_setup_component(hass, DOMAIN, config) is True await hass.async_block_till_done() - now = now + timedelta(seconds=10) - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - - # Check states - entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_1" - assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE - entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_2" - assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE - entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_3" - assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE - entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_4" - assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE + stop_time = now + timedelta(seconds=10) + step_timedelta = timedelta(seconds=1) + while now < stop_time: + now = now + step_timedelta + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + for i in range(200): + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_{i}" + assert hass.states.get(entity_id).state == STATE_ON From 562e0d785d23b39e33609467f43a376ef46a4895 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 15 May 2021 19:55:28 +0200 Subject: [PATCH 464/852] Add strict type annotations to acer_projector (#50657) --- .coveragerc | 2 +- .strict-typing | 1 + .../components/acer_projector/const.py | 34 +++++++ .../components/acer_projector/switch.py | 98 ++++++++++--------- mypy.ini | 11 +++ 5 files changed, 98 insertions(+), 48 deletions(-) create mode 100644 homeassistant/components/acer_projector/const.py diff --git a/.coveragerc b/.coveragerc index 1b44cb3013b..fa77898ca6b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,7 +8,7 @@ omit = homeassistant/scripts/*.py # omit pieces of code that rely on external devices being present - homeassistant/components/acer_projector/switch.py + homeassistant/components/acer_projector/* homeassistant/components/actiontec/device_tracker.py homeassistant/components/acmeda/__init__.py homeassistant/components/acmeda/base.py diff --git a/.strict-typing b/.strict-typing index e6f0f96368b..92a0bcd55e8 100644 --- a/.strict-typing +++ b/.strict-typing @@ -3,6 +3,7 @@ # to enable strict mypy checks. homeassistant.components +homeassistant.components.acer_projector.* homeassistant.components.airly.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* diff --git a/homeassistant/components/acer_projector/const.py b/homeassistant/components/acer_projector/const.py new file mode 100644 index 00000000000..98864ab957f --- /dev/null +++ b/homeassistant/components/acer_projector/const.py @@ -0,0 +1,34 @@ +"""Use serial protocol of Acer projector to obtain state of the projector.""" +from __future__ import annotations + +from typing import Final + +from homeassistant.const import STATE_OFF, STATE_ON + +CONF_WRITE_TIMEOUT: Final = "write_timeout" + +DEFAULT_NAME: Final = "Acer Projector" +DEFAULT_TIMEOUT: Final = 1 +DEFAULT_WRITE_TIMEOUT: Final = 1 + +ECO_MODE: Final = "ECO Mode" + +ICON: Final = "mdi:projector" + +INPUT_SOURCE: Final = "Input Source" + +LAMP: Final = "Lamp" +LAMP_HOURS: Final = "Lamp Hours" + +MODEL: Final = "Model" + +# Commands known to the projector +CMD_DICT: Final[dict[str, str]] = { + LAMP: "* 0 Lamp ?\r", + LAMP_HOURS: "* 0 Lamp\r", + INPUT_SOURCE: "* 0 Src ?\r", + ECO_MODE: "* 0 IR 052\r", + MODEL: "* 0 IR 035\r", + STATE_ON: "* 0 IR 001\r", + STATE_OFF: "* 0 IR 002\r", +} diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 4a61ec793db..69aba415589 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -1,6 +1,9 @@ """Use serial protocol of Acer projector to obtain state of the projector.""" +from __future__ import annotations + import logging import re +from typing import Any import serial import voluptuous as vol @@ -14,39 +17,26 @@ from homeassistant.const import ( STATE_ON, STATE_UNKNOWN, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import ( + CMD_DICT, + CONF_WRITE_TIMEOUT, + DEFAULT_NAME, + DEFAULT_TIMEOUT, + DEFAULT_WRITE_TIMEOUT, + ECO_MODE, + ICON, + INPUT_SOURCE, + LAMP, + LAMP_HOURS, +) _LOGGER = logging.getLogger(__name__) -CONF_WRITE_TIMEOUT = "write_timeout" - -DEFAULT_NAME = "Acer Projector" -DEFAULT_TIMEOUT = 1 -DEFAULT_WRITE_TIMEOUT = 1 - -ECO_MODE = "ECO Mode" - -ICON = "mdi:projector" - -INPUT_SOURCE = "Input Source" - -LAMP = "Lamp" -LAMP_HOURS = "Lamp Hours" - -MODEL = "Model" - -# Commands known to the projector -CMD_DICT = { - LAMP: "* 0 Lamp ?\r", - LAMP_HOURS: "* 0 Lamp\r", - INPUT_SOURCE: "* 0 Src ?\r", - ECO_MODE: "* 0 IR 052\r", - MODEL: "* 0 IR 035\r", - STATE_ON: "* 0 IR 001\r", - STATE_OFF: "* 0 IR 002\r", -} - - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_FILENAME): cv.isdevice, @@ -59,7 +49,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType, +) -> None: """Connect with serial port and return Acer Projector.""" serial_port = config[CONF_FILENAME] name = config[CONF_NAME] @@ -72,10 +67,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AcerSwitch(SwitchEntity): """Represents an Acer Projector as a switch.""" - def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): + def __init__( + self, + serial_port: str, + name: str, + timeout: int, + write_timeout: int, + ) -> None: """Init of the Acer projector.""" self.ser = serial.Serial( - port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs + port=serial_port, timeout=timeout, write_timeout=write_timeout ) self._serial_port = serial_port self._name = name @@ -87,7 +88,7 @@ class AcerSwitch(SwitchEntity): ECO_MODE: STATE_UNKNOWN, } - def _write_read(self, msg): + def _write_read(self, msg: str) -> str: """Write to the projector and read the return.""" ret = "" # Sometimes the projector won't answer for no reason or the projector @@ -96,8 +97,7 @@ class AcerSwitch(SwitchEntity): try: if not self.ser.is_open: self.ser.open() - msg = msg.encode("utf-8") - self.ser.write(msg) + self.ser.write(msg.encode("utf-8")) # Size is an experience value there is no real limit. # AFAIK there is no limit and no end character so we will usually # need to wait for timeout @@ -107,7 +107,7 @@ class AcerSwitch(SwitchEntity): self.ser.close() return ret - def _write_read_format(self, msg): + def _write_read_format(self, msg: str) -> str: """Write msg, obtain answer and format output.""" # answers are formatted as ***\answer\r*** awns = self._write_read(msg) @@ -117,29 +117,33 @@ class AcerSwitch(SwitchEntity): return STATE_UNKNOWN @property - def available(self): + def available(self) -> bool: """Return if projector is available.""" return self._available @property - def name(self): + def name(self) -> str: """Return name of the projector.""" return self._name @property - def is_on(self): + def icon(self) -> str: + """Return the icon.""" + return ICON + + @property + def is_on(self) -> bool: """Return if the projector is turned on.""" return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return state attributes.""" return self._attributes - def update(self): + def update(self) -> None: """Get the latest state from the projector.""" - msg = CMD_DICT[LAMP] - awns = self._write_read_format(msg) + awns = self._write_read_format(CMD_DICT[LAMP]) if awns == "Lamp 1": self._state = True self._available = True @@ -155,14 +159,14 @@ class AcerSwitch(SwitchEntity): awns = self._write_read_format(msg) self._attributes[key] = awns - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the projector on.""" msg = CMD_DICT[STATE_ON] self._write_read(msg) - self._state = STATE_ON + self._state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the projector off.""" msg = CMD_DICT[STATE_OFF] self._write_read(msg) - self._state = STATE_OFF + self._state = False diff --git a/mypy.ini b/mypy.ini index 3fc323dab44..9020f007113 100644 --- a/mypy.ini +++ b/mypy.ini @@ -44,6 +44,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.acer_projector.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.airly.*] check_untyped_defs = true disallow_incomplete_defs = true From dab66a58ce56e44b6c036295eacdb115941727f2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 15 May 2021 20:22:32 +0200 Subject: [PATCH 465/852] Clean smhi tests (#50681) --- tests/components/smhi/__init__.py | 2 + tests/components/smhi/conftest.py | 10 + tests/components/smhi/test_init.py | 56 +++-- tests/components/smhi/test_weather.py | 316 +++++++++++++------------- 4 files changed, 210 insertions(+), 174 deletions(-) create mode 100644 tests/components/smhi/conftest.py diff --git a/tests/components/smhi/__init__.py b/tests/components/smhi/__init__.py index 100b1f1bbb1..d815aafc8f5 100644 --- a/tests/components/smhi/__init__.py +++ b/tests/components/smhi/__init__.py @@ -1 +1,3 @@ """Tests for the SMHI component.""" +ENTITY_ID = "weather.smhi_test" +TEST_CONFIG = {"name": "test", "longitude": "17.84197", "latitude": "59.32624"} diff --git a/tests/components/smhi/conftest.py b/tests/components/smhi/conftest.py new file mode 100644 index 00000000000..6ededa6d975 --- /dev/null +++ b/tests/components/smhi/conftest.py @@ -0,0 +1,10 @@ +"""Provide common smhi fixtures.""" +import pytest + +from tests.common import load_fixture + + +@pytest.fixture(scope="session") +def api_response(): + """Return an API response.""" + return load_fixture("smhi.json") diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index ac4177dca7d..ab937d266a4 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,30 +1,44 @@ """Test SMHI component setup process.""" -from unittest.mock import Mock +from smhi.smhi_lib import APIURL_TEMPLATE -from homeassistant.components import smhi +from homeassistant.components.smhi.const import DOMAIN +from homeassistant.core import HomeAssistant -from .common import AsyncMock +from . import ENTITY_ID, TEST_CONFIG -TEST_CONFIG = { - "config": { - "name": "0123456789ABCDEF", - "longitude": "62.0022", - "latitude": "17.0022", - } -} +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker -async def test_forward_async_setup_entry() -> None: - """Test that it will forward setup entry.""" - hass = Mock() +async def test_setup_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str +) -> None: + """Test setup entry.""" + uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + aioclient_mock.get(uri, text=api_response) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + entry.add_to_hass(hass) - assert await smhi.async_setup_entry(hass, {}) is True - assert len(hass.config_entries.async_setup_platforms.mock_calls) == 1 + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state -async def test_forward_async_unload_entry() -> None: - """Test that it will forward unload entry.""" - hass = AsyncMock() - hass.config_entries.async_unload_platforms = AsyncMock(return_value=True) - assert await smhi.async_unload_entry(hass, {}) is True - assert len(hass.config_entries.async_unload_platforms.mock_calls) == 1 +async def test_remove_entry(hass: HomeAssistant) -> None: + """Test remove entry.""" + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert not state diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 10a21f74099..33214be0ae3 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -1,18 +1,19 @@ """Test for the smhi weather entity.""" import asyncio -from datetime import datetime -import logging -from unittest.mock import AsyncMock, Mock, patch +from datetime import datetime, timedelta +from unittest.mock import patch -from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecastException +import pytest +from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecast, SmhiForecastException -from homeassistant.components.smhi import weather as weather_smhi from homeassistant.components.smhi.const import ( ATTR_SMHI_CLOUDINESS, ATTR_SMHI_THUNDER_PROBABILITY, ATTR_SMHI_WIND_GUST_SPEED, ) +from homeassistant.components.smhi.weather import CONDITION_CLASSES, RETRY_TIMEOUT from homeassistant.components.weather import ( + ATTR_FORECAST, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, @@ -25,40 +26,36 @@ from homeassistant.components.weather import ( ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - DOMAIN as WEATHER_DOMAIN, ) -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry, load_fixture +from . import ENTITY_ID, TEST_CONFIG -_LOGGER = logging.getLogger(__name__) - -TEST_CONFIG = {"name": "test", "longitude": "17.84197", "latitude": "59.32624"} +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.test_util.aiohttp import AiohttpClientMocker -async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: - """Test for successfully setting up the smhi platform. - - This test are deeper integrated with the core. Since only - config_flow is used the component are setup with - "async_forward_entry_setup". The actual result are tested - with the entity state rather than "per function" unity tests - """ +async def test_setup_hass( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str +) -> None: + """Test for successfully setting up the smhi integration.""" uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) - api_response = load_fixture("smhi.json") aioclient_mock.get(uri, text=api_response) entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 # Testing the actual entity state for # deeper testing than normal unity test - state = hass.states.get("weather.smhi_test") + state = hass.states.get(ENTITY_ID) + assert state assert state.state == "sunny" assert state.attributes[ATTR_SMHI_CLOUDINESS] == 50 assert state.attributes[ATTR_SMHI_THUNDER_PROBABILITY] == 33 @@ -70,7 +67,6 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: assert state.attributes[ATTR_WEATHER_VISIBILITY] == 50 assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 7 assert state.attributes[ATTR_WEATHER_WIND_BEARING] == 134 - _LOGGER.error(state.attributes) assert len(state.attributes["forecast"]) == 4 forecast = state.attributes["forecast"][1] @@ -81,157 +77,171 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: assert forecast[ATTR_FORECAST_CONDITION] == "partlycloudy" -def test_properties_no_data(hass: HomeAssistant) -> None: +async def test_properties_no_data(hass: HomeAssistant) -> None: """Test properties when no API data available.""" - weather = weather_smhi.SmhiWeather("name", "10", "10") - weather.hass = hass + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) - assert weather.name == "name" - assert weather.should_poll is True - assert weather.temperature is None - assert weather.humidity is None - assert weather.wind_speed is None - assert weather.wind_gust_speed is None - assert weather.wind_bearing is None - assert weather.visibility is None - assert weather.pressure is None - assert weather.cloudiness is None - assert weather.thunder_probability is None - assert weather.condition is None - assert weather.forecast is None - assert weather.temperature_unit == TEMP_CELSIUS + with patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + side_effect=SmhiForecastException("boom"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + + assert state + assert state.name == "test" + assert state.state == STATE_UNKNOWN + assert ( + state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Swedish weather institute (SMHI)" + ) + assert ATTR_WEATHER_HUMIDITY not in state.attributes + assert ATTR_WEATHER_PRESSURE not in state.attributes + assert ATTR_WEATHER_TEMPERATURE not in state.attributes + assert ATTR_WEATHER_VISIBILITY not in state.attributes + assert ATTR_WEATHER_WIND_SPEED not in state.attributes + assert ATTR_WEATHER_WIND_BEARING not in state.attributes + assert ATTR_FORECAST not in state.attributes + assert ATTR_SMHI_CLOUDINESS not in state.attributes + assert ATTR_SMHI_THUNDER_PROBABILITY not in state.attributes + assert ATTR_SMHI_WIND_GUST_SPEED not in state.attributes -# pylint: disable=protected-access -def test_properties_unknown_symbol() -> None: +async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: """Test behaviour when unknown symbol from API.""" - hass = Mock() - data = Mock() - data.temperature = 5 - data.mean_precipitation = 0.5 - data.total_precipitation = 1 - data.humidity = 5 - data.wind_speed = 10 - data.wind_gust_speed = 17 - data.wind_direction = 180 - data.horizontal_visibility = 6 - data.pressure = 1008 - data.cloudiness = 52 - data.thunder_probability = 41 - data.symbol = 100 # Faulty symbol - data.valid_time = datetime(2018, 1, 1, 0, 1, 2) + data = SmhiForecast( + temperature=5, + temperature_max=10, + temperature_min=0, + humidity=5, + pressure=1008, + thunder=0, + cloudiness=52, + precipitation=1, + wind_direction=180, + wind_speed=10, + horizontal_visibility=6, + wind_gust=1.5, + mean_precipitation=0.5, + total_precipitation=1, + symbol=100, # Faulty symbol + valid_time=datetime(2018, 1, 1, 0, 1, 2), + ) - data2 = Mock() - data2.temperature = 5 - data2.mean_precipitation = 0.5 - data2.total_precipitation = 1 - data2.humidity = 5 - data2.wind_speed = 10 - data2.wind_gust_speed = 17 - data2.wind_direction = 180 - data2.horizontal_visibility = 6 - data2.pressure = 1008 - data2.cloudiness = 52 - data2.thunder_probability = 41 - data2.symbol = 100 # Faulty symbol - data2.valid_time = datetime(2018, 1, 1, 12, 1, 2) + data2 = SmhiForecast( + temperature=5, + temperature_max=10, + temperature_min=0, + humidity=5, + pressure=1008, + thunder=0, + cloudiness=52, + precipitation=1, + wind_direction=180, + wind_speed=10, + horizontal_visibility=6, + wind_gust=1.5, + mean_precipitation=0.5, + total_precipitation=1, + symbol=100, # Faulty symbol + valid_time=datetime(2018, 1, 1, 12, 1, 2), + ) - data3 = Mock() - data3.temperature = 5 - data3.mean_precipitation = 0.5 - data3.total_precipitation = 1 - data3.humidity = 5 - data3.wind_speed = 10 - data3.wind_gust_speed = 17 - data3.wind_direction = 180 - data3.horizontal_visibility = 6 - data3.pressure = 1008 - data3.cloudiness = 52 - data3.thunder_probability = 41 - data3.symbol = 100 # Faulty symbol - data3.valid_time = datetime(2018, 1, 2, 12, 1, 2) + data3 = SmhiForecast( + temperature=5, + temperature_max=10, + temperature_min=0, + humidity=5, + pressure=1008, + thunder=0, + cloudiness=52, + precipitation=1, + wind_direction=180, + wind_speed=10, + horizontal_visibility=6, + wind_gust=1.5, + mean_precipitation=0.5, + total_precipitation=1, + symbol=100, # Faulty symbol + valid_time=datetime(2018, 1, 2, 12, 1, 2), + ) testdata = [data, data2, data3] - weather = weather_smhi.SmhiWeather("name", "10", "10") - weather.hass = hass - weather._forecasts = testdata - assert weather.condition is None - forecast = weather.forecast[0] - assert forecast[ATTR_FORECAST_CONDITION] is None + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + return_value=testdata, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + + assert state + assert state.name == "test" + assert state.state == STATE_UNKNOWN + assert ATTR_FORECAST in state.attributes + assert all( + forecast[ATTR_FORECAST_CONDITION] is None + for forecast in state.attributes[ATTR_FORECAST] + ) -# pylint: disable=protected-access -async def test_refresh_weather_forecast_exceeds_retries(hass) -> None: +@pytest.mark.parametrize("error", [SmhiForecastException(), asyncio.TimeoutError()]) +async def test_refresh_weather_forecast_retry( + hass: HomeAssistant, error: Exception +) -> None: """Test the refresh weather forecast function.""" + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) + now = utcnow() - with patch.object( - hass.helpers.event, "async_call_later" - ) as call_later, patch.object( - weather_smhi.SmhiWeather, - "get_weather_forecast", - side_effect=SmhiForecastException(), - ): + with patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + side_effect=error, + ) as mock_get_forecast: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass - weather._fail_count = 2 + state = hass.states.get(ENTITY_ID) - await weather.async_update() - assert weather._forecasts is None - assert not call_later.mock_calls + assert state + assert state.name == "test" + assert state.state == STATE_UNKNOWN + assert mock_get_forecast.call_count == 1 + future = now + timedelta(seconds=RETRY_TIMEOUT + 1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() -async def test_refresh_weather_forecast_timeout(hass) -> None: - """Test timeout exception.""" - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + assert mock_get_forecast.call_count == 2 - with patch.object( - hass.helpers.event, "async_call_later" - ) as call_later, patch.object( - weather_smhi.SmhiWeather, "retry_update" - ), patch.object( - weather_smhi.SmhiWeather, - "get_weather_forecast", - side_effect=asyncio.TimeoutError, - ): + future = future + timedelta(seconds=RETRY_TIMEOUT + 1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - await weather.async_update() - assert len(call_later.mock_calls) == 1 - # Assert we are going to wait RETRY_TIMEOUT seconds - assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + assert mock_get_forecast.call_count == 3 + future = future + timedelta(seconds=RETRY_TIMEOUT + 1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() -async def test_refresh_weather_forecast_exception() -> None: - """Test any exception.""" - - hass = Mock() - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass - - with patch.object( - hass.helpers.event, "async_call_later" - ) as call_later, patch.object( - weather, - "get_weather_forecast", - side_effect=SmhiForecastException(), - ): - await weather.async_update() - assert len(call_later.mock_calls) == 1 - # Assert we are going to wait RETRY_TIMEOUT seconds - assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT - - -async def test_retry_update(): - """Test retry function of refresh forecast.""" - hass = Mock() - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass - - with patch.object(weather, "async_update", AsyncMock()) as update: - await weather.retry_update(None) - assert len(update.mock_calls) == 1 + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + # after three failed retries we stop retrying and go back to normal interval + assert mock_get_forecast.call_count == 3 def test_condition_class(): @@ -239,7 +249,7 @@ def test_condition_class(): def get_condition(index: int) -> str: """Return condition given index.""" - return [k for k, v in weather_smhi.CONDITION_CLASSES.items() if index in v][0] + return [k for k, v in CONDITION_CLASSES.items() if index in v][0] # SMHI definitions as follows, see # http://opendata.smhi.se/apidocs/metfcst/parameters.html From 0c37effc72040c55160a71501b8539ce64a4827f Mon Sep 17 00:00:00 2001 From: Filipe Pina <636320+fopina@users.noreply.github.com> Date: Sat, 15 May 2021 19:29:11 +0100 Subject: [PATCH 466/852] Add SSL support to TCP integration (#48060) Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- homeassistant/components/tcp/sensor.py | 21 ++++++++ tests/components/tcp/test_sensor.py | 75 ++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index 54cf4d120f1..ff436f8ecaf 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -2,6 +2,7 @@ import logging import select import socket +import ssl import voluptuous as vol @@ -11,9 +12,11 @@ from homeassistant.const import ( CONF_NAME, CONF_PAYLOAD, CONF_PORT, + CONF_SSL, CONF_TIMEOUT, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, + CONF_VERIFY_SSL, ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -26,6 +29,8 @@ CONF_VALUE_ON = "value_on" DEFAULT_BUFFER_SIZE = 1024 DEFAULT_NAME = "TCP Sensor" DEFAULT_TIMEOUT = 10 +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -38,6 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_VALUE_ON): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } ) @@ -71,6 +78,15 @@ class TcpSensor(SensorEntity): CONF_VALUE_ON: config.get(CONF_VALUE_ON), CONF_BUFFER_SIZE: config.get(CONF_BUFFER_SIZE), } + + if config[CONF_SSL]: + self._ssl_context = ssl.create_default_context() + if not config[CONF_VERIFY_SSL]: + self._ssl_context.check_hostname = False + self._ssl_context.verify_mode = ssl.CERT_NONE + else: + self._ssl_context = None + self._state = None self.update() @@ -104,6 +120,11 @@ class TcpSensor(SensorEntity): ) return + if self._ssl_context is not None: + sock = self._ssl_context.wrap_socket( + sock, server_hostname=self._config[CONF_HOST] + ) + try: sock.send(self._config[CONF_PAYLOAD].encode()) except OSError as err: diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index b1efef305bf..48b5703c204 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -57,6 +57,18 @@ def mock_select_fixture(): yield mock_select +@pytest.fixture(name="mock_ssl_context") +def mock_ssl_context_fixture(): + """Mock select.""" + with patch( + "homeassistant.components.tcp.sensor.ssl.create_default_context", + ) as mock_ssl_context: + mock_ssl_context.return_value.wrap_socket.return_value.recv.return_value = ( + socket_test_value + "_ssl" + ).encode() + yield mock_ssl_context + + async def test_setup_platform_valid_config(hass, mock_socket): """Check a valid configuration and call add_entities with sensor.""" with assert_setup_component(1, "sensor"): @@ -159,3 +171,66 @@ async def test_update_returns_if_template_render_fails(hass, mock_socket): assert state assert state.state == "unknown" + + +async def test_ssl_state(hass, mock_socket, mock_select, mock_ssl_context): + """Return the contents of _state, updated over SSL.""" + config = copy(SENSOR_TEST_CONFIG) + config[tcp.CONF_SSL] = "on" + + assert await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY) + + assert state + assert state.state == "test_value_ssl" + assert mock_socket.connect.called + assert mock_socket.connect.call_args == call( + (SENSOR_TEST_CONFIG["host"], SENSOR_TEST_CONFIG["port"]) + ) + assert not mock_socket.send.called + assert mock_ssl_context.called + assert mock_ssl_context.return_value.check_hostname + mock_ssl_socket = mock_ssl_context.return_value.wrap_socket.return_value + assert mock_ssl_socket.send.called + assert mock_ssl_socket.send.call_args == call( + SENSOR_TEST_CONFIG["payload"].encode() + ) + assert mock_select.call_args == call( + [mock_ssl_socket], [], [], SENSOR_TEST_CONFIG[tcp.CONF_TIMEOUT] + ) + assert mock_ssl_socket.recv.called + assert mock_ssl_socket.recv.call_args == call(SENSOR_TEST_CONFIG["buffer_size"]) + + +async def test_ssl_state_verify_off(hass, mock_socket, mock_select, mock_ssl_context): + """Return the contents of _state, updated over SSL (verify_ssl disabled).""" + config = copy(SENSOR_TEST_CONFIG) + config[tcp.CONF_SSL] = "on" + config[tcp.CONF_VERIFY_SSL] = "off" + + assert await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY) + + assert state + assert state.state == "test_value_ssl" + assert mock_socket.connect.called + assert mock_socket.connect.call_args == call( + (SENSOR_TEST_CONFIG["host"], SENSOR_TEST_CONFIG["port"]) + ) + assert not mock_socket.send.called + assert mock_ssl_context.called + assert not mock_ssl_context.return_value.check_hostname + mock_ssl_socket = mock_ssl_context.return_value.wrap_socket.return_value + assert mock_ssl_socket.send.called + assert mock_ssl_socket.send.call_args == call( + SENSOR_TEST_CONFIG["payload"].encode() + ) + assert mock_select.call_args == call( + [mock_ssl_socket], [], [], SENSOR_TEST_CONFIG[tcp.CONF_TIMEOUT] + ) + assert mock_ssl_socket.recv.called + assert mock_ssl_socket.recv.call_args == call(SENSOR_TEST_CONFIG["buffer_size"]) From e1dd479e15420d7181a854657df372e94c5f7baa Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 15 May 2021 20:43:12 +0200 Subject: [PATCH 467/852] Add Garages Amsterdam integration (#43157) Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .coveragerc | 3 + CODEOWNERS | 1 + .../components/garages_amsterdam/__init__.py | 60 ++++++++++++ .../garages_amsterdam/binary_sensor.py | 81 ++++++++++++++++ .../garages_amsterdam/config_flow.py | 58 +++++++++++ .../components/garages_amsterdam/const.py | 4 + .../garages_amsterdam/manifest.json | 9 ++ .../components/garages_amsterdam/sensor.py | 96 +++++++++++++++++++ .../components/garages_amsterdam/strings.json | 16 ++++ .../garages_amsterdam/translations/en.json | 18 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../components/garages_amsterdam/__init__.py | 1 + .../components/garages_amsterdam/conftest.py | 32 +++++++ .../garages_amsterdam/test_config_flow.py | 65 +++++++++++++ 16 files changed, 451 insertions(+) create mode 100644 homeassistant/components/garages_amsterdam/__init__.py create mode 100644 homeassistant/components/garages_amsterdam/binary_sensor.py create mode 100644 homeassistant/components/garages_amsterdam/config_flow.py create mode 100644 homeassistant/components/garages_amsterdam/const.py create mode 100644 homeassistant/components/garages_amsterdam/manifest.json create mode 100644 homeassistant/components/garages_amsterdam/sensor.py create mode 100644 homeassistant/components/garages_amsterdam/strings.json create mode 100644 homeassistant/components/garages_amsterdam/translations/en.json create mode 100644 tests/components/garages_amsterdam/__init__.py create mode 100644 tests/components/garages_amsterdam/conftest.py create mode 100644 tests/components/garages_amsterdam/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index fa77898ca6b..b0da1981f87 100644 --- a/.coveragerc +++ b/.coveragerc @@ -348,6 +348,9 @@ omit = homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py homeassistant/components/garadget/cover.py + homeassistant/components/garages_amsterdam/__init__.py + homeassistant/components/garages_amsterdam/binary_sensor.py + homeassistant/components/garages_amsterdam/sensor.py homeassistant/components/garmin_connect/__init__.py homeassistant/components/garmin_connect/const.py homeassistant/components/garmin_connect/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 00446a4f087..a6e9461544c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -170,6 +170,7 @@ homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 homeassistant/components/fritzbox/* @mib1185 homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend +homeassistant/components/garages_amsterdam/* @klaasnicolaas homeassistant/components/garmin_connect/* @cyberjunky homeassistant/components/gdacs/* @exxamalte homeassistant/components/geniushub/* @zxdavb diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py new file mode 100644 index 00000000000..be228e2f3a0 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -0,0 +1,60 @@ +"""The Garages Amsterdam integration.""" +from datetime import timedelta +import logging + +import async_timeout +import garages_amsterdam + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +PLATFORMS = ["binary_sensor", "sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Garages Amsterdam from a config entry.""" + await get_coordinator(hass) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Garages Amsterdam config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if len(hass.config_entries.async_entries(DOMAIN)) == 1: + hass.data.pop(DOMAIN) + + return unload_ok + + +async def get_coordinator( + hass: HomeAssistant, +) -> DataUpdateCoordinator: + """Get the data update coordinator.""" + if DOMAIN in hass.data: + return hass.data[DOMAIN] + + async def async_get_garages(): + with async_timeout.timeout(10): + return { + garage.garage_name: garage + for garage in await garages_amsterdam.get_garages( + aiohttp_client.async_get_clientsession(hass) + ) + } + + coordinator = DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name=DOMAIN, + update_method=async_get_garages, + update_interval=timedelta(minutes=10), + ) + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN] = coordinator + return coordinator diff --git a/homeassistant/components/garages_amsterdam/binary_sensor.py b/homeassistant/components/garages_amsterdam/binary_sensor.py new file mode 100644 index 00000000000..cb2ba8906bc --- /dev/null +++ b/homeassistant/components/garages_amsterdam/binary_sensor.py @@ -0,0 +1,81 @@ +"""Binary Sensor platform for Garages Amsterdam.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import get_coordinator +from .const import ATTRIBUTION + +BINARY_SENSORS = { + "state", +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Defer sensor setup to the shared sensor module.""" + coordinator = await get_coordinator(hass) + + async_add_entities( + GaragesamsterdamBinarySensor( + coordinator, config_entry.data["garage_name"], info_type + ) + for info_type in BINARY_SENSORS + ) + + +class GaragesamsterdamBinarySensor(CoordinatorEntity, BinarySensorEntity): + """Binary Sensor representing garages amsterdam data.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, garage_name: str, info_type: str + ) -> None: + """Initialize garages amsterdam binary sensor.""" + super().__init__(coordinator) + self._unique_id = f"{garage_name}-{info_type}" + self._garage_name = garage_name + self._info_type = info_type + self._name = garage_name + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self._name + + @property + def unique_id(self) -> str: + """Return the unique id of the device.""" + return self._unique_id + + @property + def is_on(self) -> bool: + """If the binary sensor is currently on or off.""" + return ( + getattr(self.coordinator.data[self._garage_name], self._info_type) != "ok" + ) + + @property + def device_class(self) -> str: + """Return the class of the binary sensor.""" + return DEVICE_CLASS_PROBLEM + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return device attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/garages_amsterdam/config_flow.py b/homeassistant/components/garages_amsterdam/config_flow.py new file mode 100644 index 00000000000..a043f7c2b00 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow for Garages Amsterdam integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aiohttp import ClientResponseError +import garages_amsterdam +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Garages Amsterdam.""" + + VERSION = 1 + _options: list[str] | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if self._options is None: + self._options = [] + try: + api_data = await garages_amsterdam.get_garages( + aiohttp_client.async_get_clientsession(self.hass) + ) + except ClientResponseError: + _LOGGER.error("Unexpected response from server") + return self.async_abort(reason="cannot_connect") + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + for garage in sorted(api_data, key=lambda garage: garage.garage_name): + self._options.append(garage.garage_name) + + if user_input is not None: + await self.async_set_unique_id(user_input["garage_name"]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input["garage_name"], data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required("garage_name"): vol.In(self._options)} + ), + ) diff --git a/homeassistant/components/garages_amsterdam/const.py b/homeassistant/components/garages_amsterdam/const.py new file mode 100644 index 00000000000..ae7801a9abd --- /dev/null +++ b/homeassistant/components/garages_amsterdam/const.py @@ -0,0 +1,4 @@ +"""Constants for the Garages Amsterdam integration.""" + +DOMAIN = "garages_amsterdam" +ATTRIBUTION = f'{"Data provided by municipality of Amsterdam"}' diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json new file mode 100644 index 00000000000..f0456c5afef --- /dev/null +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "garages_amsterdam", + "name": "Garages Amsterdam", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", + "requirements": ["garages-amsterdam==2.0.4"], + "codeowners": ["@klaasnicolaas"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/garages_amsterdam/sensor.py b/homeassistant/components/garages_amsterdam/sensor.py new file mode 100644 index 00000000000..ed01862aba4 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/sensor.py @@ -0,0 +1,96 @@ +"""Sensor platform for Garages Amsterdam.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import get_coordinator +from .const import ATTRIBUTION + +SENSORS = { + "free_space_short": "mdi:car", + "free_space_long": "mdi:car", + "short_capacity": "mdi:car", + "long_capacity": "mdi:car", +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Defer sensor setup to the shared sensor module.""" + coordinator = await get_coordinator(hass) + + entities: list[GaragesamsterdamSensor] = [] + + for info_type in SENSORS: + if getattr(coordinator.data[config_entry.data["garage_name"]], info_type) != "": + entities.append( + GaragesamsterdamSensor( + coordinator, config_entry.data["garage_name"], info_type + ) + ) + + async_add_entities(entities) + + +class GaragesamsterdamSensor(CoordinatorEntity, SensorEntity): + """Sensor representing garages amsterdam data.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, garage_name: str, info_type: str + ) -> None: + """Initialize garages amsterdam sensor.""" + super().__init__(coordinator) + self._unique_id = f"{garage_name}-{info_type}" + self._garage_name = garage_name + self._info_type = info_type + self._name = f"{garage_name} - {info_type}".replace("_", " ") + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self._name + + @property + def unique_id(self) -> str: + """Return the unique id of the device.""" + return self._unique_id + + @property + def available(self) -> bool: + """Return if sensor is available.""" + return self.coordinator.last_update_success and ( + self._garage_name in self.coordinator.data + ) + + @property + def state(self) -> str: + """Return the state of the sensor.""" + return getattr(self.coordinator.data[self._garage_name], self._info_type) + + @property + def icon(self) -> str: + """Return the icon.""" + return SENSORS[self._info_type] + + @property + def unit_of_measurement(self) -> str: + """Return unit of measurement.""" + return "cars" + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return device attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/garages_amsterdam/strings.json b/homeassistant/components/garages_amsterdam/strings.json new file mode 100644 index 00000000000..c8c3968aa59 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/strings.json @@ -0,0 +1,16 @@ +{ + "title": "Garages Amsterdam", + "config": { + "step": { + "user": { + "title": "Pick a garage to monitor", + "data": { "garage_name": "Garage name" } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/garages_amsterdam/translations/en.json b/homeassistant/components/garages_amsterdam/translations/en.json new file mode 100644 index 00000000000..03efd757773 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "garage_name": "Garage name" + }, + "title": "Pick a garage to monitor" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e2a36c3b093..a7d0153d8a1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -81,6 +81,7 @@ FLOWS = [ "fritz", "fritzbox", "fritzbox_callmonitor", + "garages_amsterdam", "garmin_connect", "gdacs", "geofency", diff --git a/requirements_all.txt b/requirements_all.txt index 76d322ba71c..8000472f721 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -634,6 +634,9 @@ fritzconnection==1.4.2 # homeassistant.components.google_translate gTTS==2.2.2 +# homeassistant.components.garages_amsterdam +garages-amsterdam==2.0.4 + # homeassistant.components.garmin_connect garminconnect==0.1.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa777f4a63e..54d6e3853c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -340,6 +340,9 @@ fritzconnection==1.4.2 # homeassistant.components.google_translate gTTS==2.2.2 +# homeassistant.components.garages_amsterdam +garages-amsterdam==2.0.4 + # homeassistant.components.garmin_connect garminconnect==0.1.19 diff --git a/tests/components/garages_amsterdam/__init__.py b/tests/components/garages_amsterdam/__init__.py new file mode 100644 index 00000000000..ff430c0e7b2 --- /dev/null +++ b/tests/components/garages_amsterdam/__init__.py @@ -0,0 +1 @@ +"""Tests for the Garages Amsterdam integration.""" diff --git a/tests/components/garages_amsterdam/conftest.py b/tests/components/garages_amsterdam/conftest.py new file mode 100644 index 00000000000..49d242dabd5 --- /dev/null +++ b/tests/components/garages_amsterdam/conftest.py @@ -0,0 +1,32 @@ +"""Test helpers.""" + +from unittest.mock import Mock, patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_cases(): + """Mock garages_amsterdam garages.""" + with patch( + "garages_amsterdam.get_garages", + return_value=[ + Mock( + garage_name="IJDok", + free_space_short=100, + free_space_long=10, + short_capacity=120, + long_capacity=60, + state="ok", + ), + Mock( + garage_name="Arena", + free_space_short=200, + free_space_long=20, + short_capacity=240, + long_capacity=80, + state="error", + ), + ], + ) as mock_get_garages: + yield mock_get_garages diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py new file mode 100644 index 00000000000..464fcb799ad --- /dev/null +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -0,0 +1,65 @@ +"""Test the Garages Amsterdam config flow.""" +from unittest.mock import patch + +from aiohttp import ClientResponseError +import pytest + +from homeassistant import config_entries, setup +from homeassistant.components.garages_amsterdam.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + + +async def test_full_flow(hass: HomeAssistant) -> None: + """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.get("type") == RESULT_TYPE_FORM + assert "flow_id" in result + + with patch( + "homeassistant.components.garages_amsterdam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"garage_name": "IJDok"}, + ) + await hass.async_block_till_done() + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "IJDok" + assert "result" in result2 + assert result2["result"].unique_id == "IJDok" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,reason", + [ + (RuntimeError, "unknown"), + (ClientResponseError(None, None, status=500), "cannot_connect"), + ], +) +async def test_error_handling( + side_effect: Exception, reason: str, hass: HomeAssistant +) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.garages_amsterdam.config_flow.garages_amsterdam.get_garages", + side_effect=side_effect, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == reason From 1afb0a0841e4b7c5c012231b703b7f1a67075a5d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 15 May 2021 14:19:16 -0500 Subject: [PATCH 468/852] Sonos improve radio metadata handling (#50493) --- homeassistant/components/sonos/speaker.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 786b0113334..0960c200da7 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -8,6 +8,7 @@ import datetime from functools import partial import logging from typing import Any, Callable +import urllib.parse import async_timeout from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo @@ -725,7 +726,7 @@ class SonosSpeaker: if variables and "transport_state" in variables: self.media.play_mode = variables["current_play_mode"] - track_uri = variables["current_track_uri"] + track_uri = variables["enqueued_transport_uri"] music_source = self.soco.music_source_from_uri(track_uri) else: self.media.play_mode = self.soco.play_mode @@ -777,18 +778,25 @@ class SonosSpeaker: except (TypeError, KeyError, AttributeError): pass - # Non-playing radios will not have a current title. Radios without tagging - # can have part of the radio URI as title. In these cases we try to use the - # radio name instead. + if not self.media.artist: + try: + self.media.artist = variables["current_track_meta_data"].creator + except (KeyError, AttributeError): + pass + + # Radios without tagging can have part of the radio URI as title. + # In this case we try to use the radio name instead. try: uri_meta_data = variables["enqueued_transport_uri_meta_data"] if isinstance(uri_meta_data, DidlAudioBroadcast) and ( - self.media.playback_status != SONOS_STATE_PLAYING - or self.soco.music_source_from_uri(self.media.title) == MUSIC_SRC_RADIO + self.soco.music_source_from_uri(self.media.title) == MUSIC_SRC_RADIO or ( isinstance(self.media.title, str) and isinstance(self.media.uri, str) - and self.media.title in self.media.uri + and ( + self.media.title in self.media.uri + or self.media.title in urllib.parse.unquote(self.media.uri) + ) ) ): self.media.title = uri_meta_data.title From cad41cd4ed123234f657d16b6a2fea9bab9a6b79 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 21:27:04 +0200 Subject: [PATCH 469/852] Clean up unused method from SolarEdge tests (#50684) --- tests/components/solaredge/test_config_flow.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 280b1c02ca0..059e10c7662 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -5,7 +5,6 @@ import pytest from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import data_entry_flow -from homeassistant.components.solaredge import config_flow from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_NAME @@ -27,13 +26,6 @@ def mock_controller(): yield api -def init_config_flow(hass: HomeAssistant) -> config_flow.SolarEdgeConfigFlow: - """Init a configuration flow.""" - flow = config_flow.SolarEdgeConfigFlow() - flow.hass = hass - return flow - - async def test_user(hass: HomeAssistant, test_api: Mock) -> None: """Test user config.""" result = await hass.config_entries.flow.async_init( From 5da64d01e2bd468d67e3cd81d02226a650b248b9 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 15 May 2021 21:38:12 +0200 Subject: [PATCH 470/852] Fix smhi typing (#50690) --- homeassistant/components/smhi/config_flow.py | 22 +++++-- homeassistant/components/smhi/const.py | 8 ++- homeassistant/components/smhi/weather.py | 64 +++++++++++--------- mypy.ini | 3 - script/hassfest/mypy_config.py | 1 - 5 files changed, 58 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 5fde538b744..5c3572dd2fd 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -1,10 +1,15 @@ """Config flow to configure SMHI component.""" +from __future__ import annotations + +from typing import Any + from smhi.smhi_lib import Smhi, SmhiForecastException import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify @@ -13,7 +18,7 @@ from .const import DOMAIN, HOME_LOCATION_NAME @callback -def smhi_locations(hass: HomeAssistant): +def smhi_locations(hass: HomeAssistant) -> set[str]: """Return configurations of SMHI component.""" return { (slugify(entry.data[CONF_NAME])) @@ -28,9 +33,11 @@ class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize SMHI forecast configuration flow.""" - self._errors = {} + self._errors: dict[str, str] = {} - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" self._errors = {} @@ -79,8 +86,11 @@ class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return name in smhi_locations(self.hass) async def _show_config_form( - self, name: str = None, latitude: str = None, longitude: str = None - ): + self, + name: str | None = None, + latitude: float | None = None, + longitude: float | None = None, + ) -> FlowResult: """Show the configuration form to edit location data.""" return self.async_show_form( step_id="user", @@ -94,7 +104,7 @@ class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def _check_location(self, longitude: str, latitude: str) -> bool: + async def _check_location(self, longitude: float, latitude: float) -> bool: """Return true if location is ok.""" try: session = aiohttp_client.async_get_clientsession(self.hass) diff --git a/homeassistant/components/smhi/const.py b/homeassistant/components/smhi/const.py index c2074416295..03b583b77ec 100644 --- a/homeassistant/components/smhi/const.py +++ b/homeassistant/components/smhi/const.py @@ -1,9 +1,11 @@ """Constants in smhi component.""" +from typing import Final + from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN -ATTR_SMHI_CLOUDINESS = "cloudiness" -ATTR_SMHI_WIND_GUST_SPEED = "wind_gust_speed" -ATTR_SMHI_THUNDER_PROBABILITY = "thunder_probability" +ATTR_SMHI_CLOUDINESS: Final = "cloudiness" +ATTR_SMHI_WIND_GUST_SPEED: Final = "wind_gust_speed" +ATTR_SMHI_THUNDER_PROBABILITY: Final = "thunder_probability" DOMAIN = "smhi" diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 0fd808d1401..d28cb51870b 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -2,13 +2,14 @@ from __future__ import annotations import asyncio -from datetime import timedelta +from datetime import datetime, timedelta import logging +from typing import Any, Final, TypedDict import aiohttp import async_timeout from smhi import Smhi -from smhi.smhi_lib import SmhiForecastException +from smhi.smhi_lib import SmhiForecast, SmhiForecastException from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, @@ -36,6 +37,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later from homeassistant.util import Throttle, slugify from .const import ( @@ -48,7 +51,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) # Used to map condition from API results -CONDITION_CLASSES = { +CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_CLOUDY: [5, 6], ATTR_CONDITION_FOG: [7], ATTR_CONDITION_HAIL: [], @@ -72,8 +75,10 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=31) async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, config_entries -) -> bool: + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Add a weather entity from map location.""" location = config_entry.data name = slugify(location[CONF_NAME]) @@ -88,8 +93,7 @@ async def async_setup_entry( ) entity.entity_id = ENTITY_ID_SENSOR_FORMAT.format(name) - config_entries([entity], True) - return True + async_add_entities([entity], True) class SmhiWeather(WeatherEntity): @@ -100,14 +104,14 @@ class SmhiWeather(WeatherEntity): name: str, latitude: str, longitude: str, - session: aiohttp.ClientSession = None, + session: aiohttp.ClientSession, ) -> None: """Initialize the SMHI weather entity.""" self._name = name self._latitude = latitude self._longitude = longitude - self._forecasts = None + self._forecasts: list[SmhiForecast] | None = None self._fail_count = 0 self._smhi_api = Smhi(self._longitude, self._latitude, session=session) @@ -128,17 +132,15 @@ class SmhiWeather(WeatherEntity): _LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes") self._fail_count += 1 if self._fail_count < 3: - self.hass.helpers.event.async_call_later( - RETRY_TIMEOUT, self.retry_update - ) + async_call_later(self.hass, RETRY_TIMEOUT, self.retry_update) - async def retry_update(self, _): + async def retry_update(self, _: datetime) -> None: """Retry refresh weather forecast.""" await self.async_update( # pylint: disable=unexpected-keyword-arg no_throttle=True ) - async def get_weather_forecast(self) -> []: + async def get_weather_forecast(self) -> list[SmhiForecast]: """Return the current forecasts from SMHI API.""" return await self._smhi_api.async_get_forecast() @@ -148,7 +150,7 @@ class SmhiWeather(WeatherEntity): return self._name @property - def temperature(self) -> int: + def temperature(self) -> int | None: """Return the temperature.""" if self._forecasts is not None: return self._forecasts[0].temperature @@ -160,14 +162,14 @@ class SmhiWeather(WeatherEntity): return TEMP_CELSIUS @property - def humidity(self) -> int: + def humidity(self) -> int | None: """Return the humidity.""" if self._forecasts is not None: return self._forecasts[0].humidity return None @property - def wind_speed(self) -> float: + def wind_speed(self) -> float | None: """Return the wind speed.""" if self._forecasts is not None: # Convert from m/s to km/h @@ -175,7 +177,7 @@ class SmhiWeather(WeatherEntity): return None @property - def wind_gust_speed(self) -> float: + def wind_gust_speed(self) -> float | None: """Return the wind gust speed.""" if self._forecasts is not None: # Convert from m/s to km/h @@ -183,42 +185,42 @@ class SmhiWeather(WeatherEntity): return None @property - def wind_bearing(self) -> int: + def wind_bearing(self) -> int | None: """Return the wind bearing.""" if self._forecasts is not None: return self._forecasts[0].wind_direction return None @property - def visibility(self) -> float: + def visibility(self) -> float | None: """Return the visibility.""" if self._forecasts is not None: return self._forecasts[0].horizontal_visibility return None @property - def pressure(self) -> int: + def pressure(self) -> int | None: """Return the pressure.""" if self._forecasts is not None: return self._forecasts[0].pressure return None @property - def cloudiness(self) -> int: + def cloudiness(self) -> int | None: """Return the cloudiness.""" if self._forecasts is not None: return self._forecasts[0].cloudiness return None @property - def thunder_probability(self) -> int: + def thunder_probability(self) -> int | None: """Return the chance of thunder, unit Percent.""" if self._forecasts is not None: return self._forecasts[0].thunder return None @property - def condition(self) -> str: + def condition(self) -> str | None: """Return the weather condition.""" if self._forecasts is None: return None @@ -233,7 +235,7 @@ class SmhiWeather(WeatherEntity): return "Swedish weather institute (SMHI)" @property - def forecast(self) -> list: + def forecast(self) -> list[dict[str, Any]] | None: """Return the forecast.""" if self._forecasts is None or len(self._forecasts) < 2: return None @@ -258,9 +260,9 @@ class SmhiWeather(WeatherEntity): return data @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> ExtraAttributes: """Return SMHI specific attributes.""" - extra_attributes = {} + extra_attributes: ExtraAttributes = {} if self.cloudiness is not None: extra_attributes[ATTR_SMHI_CLOUDINESS] = self.cloudiness if self.wind_gust_speed is not None: @@ -268,3 +270,11 @@ class SmhiWeather(WeatherEntity): if self.thunder_probability is not None: extra_attributes[ATTR_SMHI_THUNDER_PROBABILITY] = self.thunder_probability return extra_attributes + + +class ExtraAttributes(TypedDict, total=False): + """Represent the extra state attribute types.""" + + cloudiness: int + thunder_probability: int + wind_gust_speed: float diff --git a/mypy.ini b/mypy.ini index 9020f007113..895878bbb6e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1160,9 +1160,6 @@ ignore_errors = true [mypy-homeassistant.components.smarty.*] ignore_errors = true -[mypy-homeassistant.components.smhi.*] -ignore_errors = true - [mypy-homeassistant.components.solaredge.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 5e412517628..1968d51d7f2 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -188,7 +188,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.smartthings.*", "homeassistant.components.smarttub.*", "homeassistant.components.smarty.*", - "homeassistant.components.smhi.*", "homeassistant.components.solaredge.*", "homeassistant.components.solarlog.*", "homeassistant.components.somfy.*", From ca558545a1ce19a8f643a82a24b77c2c5cb43860 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 15 May 2021 21:39:41 +0200 Subject: [PATCH 471/852] Use mock_restore_state in testing of modbus sensor (#50455) --- tests/components/modbus/test_modbus_sensor.py | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index c83eab0b3d6..cb784ac46b3 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -1,6 +1,5 @@ """The tests for the Modbus sensor component.""" import logging -from unittest import mock import pytest @@ -42,6 +41,8 @@ from homeassistant.core import State from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from tests.common import mock_restore_cache + @pytest.mark.parametrize( "do_discovery, do_config", @@ -573,24 +574,21 @@ async def test_restore_state_sensor(hass): sensor_name = "test_sensor" test_value = "117" config_sensor = {CONF_NAME: sensor_name, CONF_ADDRESS: 17} - with mock.patch( - "homeassistant.components.modbus.sensor.ModbusRegisterSensor.async_get_last_state" - ) as mock_get_last_state: - mock_get_last_state.return_value = State( - f"{SENSOR_DOMAIN}.{sensor_name}", f"{test_value}" - ) - - await base_config_test( - hass, - config_sensor, - sensor_name, - SENSOR_DOMAIN, - CONF_SENSORS, - None, - method_discovery=True, - ) - entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" - assert hass.states.get(entity_id).state == test_value + mock_restore_cache( + hass, + (State(f"{SENSOR_DOMAIN}.{sensor_name}", test_value),), + ) + await base_config_test( + hass, + config_sensor, + sensor_name, + SENSOR_DOMAIN, + CONF_SENSORS, + None, + method_discovery=True, + ) + entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" + assert hass.states.get(entity_id).state == test_value @pytest.mark.parametrize( From 8bc75e91a02f9b0a743c26bed1ee31a1d4c96551 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 15 May 2021 21:43:06 +0200 Subject: [PATCH 472/852] Add color effect to Shelly's color devices (#48052) * Add color effect * Final commit based on updated firmware * Update homeassistant/components/shelly/light.py Co-authored-by: Shay Levy * Update homeassistant/components/shelly/light.py Co-authored-by: Shay Levy * Update homeassistant/components/shelly/light.py * Fix flake Co-authored-by: Shay Levy --- homeassistant/components/shelly/const.py | 17 +++++++ homeassistant/components/shelly/light.py | 58 ++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 964dab31698..119ae478bb7 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -77,6 +77,23 @@ INPUTS_EVENTS_SUBTYPES = { SHBTN_MODELS = ["SHBTN-1", "SHBTN-2"] +STANDARD_RGB_EFFECTS = { + 0: "Off", + 1: "Meteor Shower", + 2: "Gradual Change", + 3: "Flash", +} + +SHBLB_1_RGB_EFFECTS = { + 0: "Off", + 1: "Meteor Shower", + 2: "Gradual Change", + 3: "Flash", + 4: "Breath", + 5: "On/Off Gradual", + 6: "Red/Green Change", +} + # Kelvin value for colorTemp KELVIN_MAX_VALUE = 6500 KELVIN_MIN_VALUE_WHITE = 2700 diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index d6afcd8841e..8314650d548 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -11,6 +11,7 @@ import async_timeout from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, COLOR_MODE_BRIGHTNESS, @@ -18,6 +19,7 @@ from homeassistant.components.light import ( COLOR_MODE_ONOFF, COLOR_MODE_RGB, COLOR_MODE_RGBW, + SUPPORT_EFFECT, LightEntity, brightness_supported, ) @@ -36,6 +38,8 @@ from .const import ( KELVIN_MAX_VALUE, KELVIN_MIN_VALUE_COLOR, KELVIN_MIN_VALUE_WHITE, + SHBLB_1_RGB_EFFECTS, + STANDARD_RGB_EFFECTS, ) from .entity import ShellyBlockEntity from .utils import async_remove_shelly_entity @@ -77,6 +81,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): self.control_result = None self.mode_result = None self._supported_color_modes = set() + self._supported_features = 0 self._min_kelvin = KELVIN_MIN_VALUE_WHITE self._max_kelvin = KELVIN_MAX_VALUE @@ -96,6 +101,14 @@ class ShellyLight(ShellyBlockEntity, LightEntity): else: self._supported_color_modes.add(COLOR_MODE_ONOFF) + if hasattr(block, "effect"): + self._supported_features |= SUPPORT_EFFECT + + @property + def supported_features(self) -> int: + """Supported features.""" + return self._supported_features + @property def is_on(self) -> bool: """If light is on.""" @@ -204,6 +217,33 @@ class ShellyLight(ShellyBlockEntity, LightEntity): """Flag supported color modes.""" return self._supported_color_modes + @property + def effect_list(self) -> list[str] | None: + """Return the list of supported effects.""" + if not self.supported_features & SUPPORT_EFFECT: + return None + + if self.wrapper.model == "SHBLB-1": + return list(SHBLB_1_RGB_EFFECTS.values()) + + return list(STANDARD_RGB_EFFECTS.values()) + + @property + def effect(self) -> str | None: + """Return the current effect.""" + if not self.supported_features & SUPPORT_EFFECT: + return None + + if self.control_result: + effect_index = self.control_result["effect"] + else: + effect_index = self.block.effect + + if self.wrapper.model == "SHBLB-1": + return SHBLB_1_RGB_EFFECTS[effect_index] + + return STANDARD_RGB_EFFECTS[effect_index] + async def async_turn_on(self, **kwargs) -> None: """Turn on light.""" if self.block.type == "relay": @@ -241,6 +281,24 @@ class ShellyLight(ShellyBlockEntity, LightEntity): ATTR_RGBW_COLOR ] + if ATTR_EFFECT in kwargs: + # Color effect change - used only in color mode, switch device mode to color + set_mode = "color" + if self.wrapper.model == "SHBLB-1": + effect_dict = SHBLB_1_RGB_EFFECTS + else: + effect_dict = STANDARD_RGB_EFFECTS + if kwargs[ATTR_EFFECT] in effect_dict.values(): + params["effect"] = [ + k for k, v in effect_dict.items() if v == kwargs[ATTR_EFFECT] + ][0] + else: + _LOGGER.error( + "Effect '%s' not supported by device %s", + kwargs[ATTR_EFFECT], + self.wrapper.model, + ) + if await self.set_light_mode(set_mode): self.control_result = await self.set_state(**params) From e293d35ac9e87a9d8d0d6e62673bd2ded2d64e83 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 May 2021 22:14:56 +0200 Subject: [PATCH 473/852] Clean up WLED tests (#50685) Co-authored-by: Martin Hjelmare --- tests/components/wled/test_config_flow.py | 326 ++++++++++------------ 1 file changed, 148 insertions(+), 178 deletions(-) diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 60f2c59ecba..e828c632451 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -4,11 +4,15 @@ from unittest.mock import MagicMock, patch import aiohttp from wled import WLEDConnectionError -from homeassistant import data_entry_flow -from homeassistant.components.wled import config_flow +from homeassistant.components.wled.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from . import init_integration @@ -16,163 +20,8 @@ from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -async def test_show_user_form(hass: HomeAssistant) -> None: - """Test that the user set up form is served.""" - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_USER}, - ) - - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - -async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None: - """Test that the zeroconf confirmation form is served.""" - flow = config_flow.WLEDFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF, CONF_NAME: "test"} - result = await flow.async_step_zeroconf_confirm() - - assert result["description_placeholders"] == {CONF_NAME: "test"} - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - -async def test_show_zerconf_form( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that the zeroconf confirmation form is served.""" - aioclient_mock.get( - "http://192.168.1.123:80/json/", - text=load_fixture("wled/rgb.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - flow = config_flow.WLEDFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - result = await flow.async_step_zeroconf( - {"host": "192.168.1.123", "hostname": "example.local.", "properties": {}} - ) - - assert flow.context[CONF_HOST] == "192.168.1.123" - assert flow.context[CONF_NAME] == "example" - assert result["description_placeholders"] == {CONF_NAME: "example"} - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) -async def test_connection_error( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we show user form on WLED connection error.""" - aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "example.com"}, - ) - - assert result["errors"] == {"base": "cannot_connect"} - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) -async def test_zeroconf_connection_error( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow on WLED connection error.""" - aioclient_mock.get("http://192.168.1.123/json/", exc=aiohttp.ClientError) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, - ) - - assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - - -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) -async def test_zeroconf_confirm_connection_error( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow on WLED connection error.""" - aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={ - "source": SOURCE_ZEROCONF, - CONF_HOST: "example.com", - CONF_NAME: "test", - }, - data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}}, - ) - - assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - - -async def test_user_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow if WLED device already configured.""" - await init_integration(hass, aioclient_mock) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "192.168.1.123"}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_zeroconf_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow if WLED device already configured.""" - await init_integration(hass, aioclient_mock) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_zeroconf_with_mac_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow if WLED device already configured.""" - await init_integration(hass, aioclient_mock) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data={ - "host": "192.168.1.123", - "hostname": "example.local.", - "properties": {CONF_MAC: "aabbccddeeff"}, - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_full_user_flow_implementation( - hass: HomeAssistant, aioclient_mock + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the full manual user flow from start to finish.""" aioclient_mock.get( @@ -182,21 +31,23 @@ async def test_full_user_flow_implementation( ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": SOURCE_USER}, ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "user" + assert result.get("type") == RESULT_TYPE_FORM + assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) + assert result.get("title") == "192.168.1.123" + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert result["data"][CONF_MAC] == "aabbccddeeff" - assert result["title"] == "192.168.1.123" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY async def test_full_zeroconf_flow_implementation( @@ -209,21 +60,140 @@ async def test_full_zeroconf_flow_implementation( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.WLEDFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - result = await flow.async_step_zeroconf( - {"host": "192.168.1.123", "hostname": "example.local.", "properties": {}} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, ) - assert flow.context[CONF_HOST] == "192.168.1.123" - assert flow.context[CONF_NAME] == "example" - assert result["description_placeholders"] == {CONF_NAME: "example"} - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 - result = await flow.async_step_zeroconf_confirm(user_input={}) - assert result["data"][CONF_HOST] == "192.168.1.123" - assert result["data"][CONF_MAC] == "aabbccddeeff" - assert result["title"] == "example" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("description_placeholders") == {CONF_NAME: "example"} + assert result.get("step_id") == "zeroconf_confirm" + assert result.get("type") == RESULT_TYPE_FORM + assert "flow_id" in result + + flow = flows[0] + assert "context" in flow + assert flow["context"][CONF_HOST] == "192.168.1.123" + assert flow["context"][CONF_NAME] == "example" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2.get("title") == "example" + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + + assert "data" in result2 + assert result2["data"][CONF_HOST] == "192.168.1.123" + assert result2["data"][CONF_MAC] == "aabbccddeeff" + + +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) +async def test_connection_error( + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on WLED connection error.""" + aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.com"}, + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "user" + assert result.get("errors") == {"base": "cannot_connect"} + + +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) +async def test_zeroconf_connection_error( + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on WLED connection error.""" + aioclient_mock.get("http://192.168.1.123/json/", exc=aiohttp.ClientError) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "cannot_connect" + + +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) +async def test_zeroconf_confirm_connection_error( + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on WLED connection error.""" + aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_ZEROCONF, + CONF_HOST: "example.com", + CONF_NAME: "test", + }, + data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "cannot_connect" + + +async def test_user_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "192.168.1.123"}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_zeroconf_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_zeroconf_with_mac_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "192.168.1.123", + "hostname": "example.local.", + "properties": {CONF_MAC: "aabbccddeeff"}, + }, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" From bc006c9ecc0708044ec6fbb879a3e2f4a7769849 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 15 May 2021 22:53:10 +0200 Subject: [PATCH 474/852] Add strict type annotations to aftership (#50692) * add strict type annotations * import PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA * bring needed return back --- .coveragerc | 2 +- .strict-typing | 1 + homeassistant/components/aftership/const.py | 42 +++++++- homeassistant/components/aftership/sensor.py | 101 +++++++++---------- mypy.ini | 11 ++ 5 files changed, 104 insertions(+), 53 deletions(-) diff --git a/.coveragerc b/.coveragerc index b0da1981f87..d38f9e5b992 100644 --- a/.coveragerc +++ b/.coveragerc @@ -24,7 +24,7 @@ omit = homeassistant/components/adguard/switch.py homeassistant/components/ads/* homeassistant/components/aemet/weather_update_coordinator.py - homeassistant/components/aftership/sensor.py + homeassistant/components/aftership/* homeassistant/components/agent_dvr/__init__.py homeassistant/components/agent_dvr/alarm_control_panel.py homeassistant/components/agent_dvr/camera.py diff --git a/.strict-typing b/.strict-typing index 92a0bcd55e8..638c5de5ae2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -4,6 +4,7 @@ homeassistant.components homeassistant.components.acer_projector.* +homeassistant.components.aftership.* homeassistant.components.airly.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* diff --git a/homeassistant/components/aftership/const.py b/homeassistant/components/aftership/const.py index ef7d2397daf..d0176cde15d 100644 --- a/homeassistant/components/aftership/const.py +++ b/homeassistant/components/aftership/const.py @@ -1,2 +1,42 @@ """Constants for the Aftership integration.""" -DOMAIN = "aftership" +from __future__ import annotations + +from datetime import timedelta +from typing import Final + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +DOMAIN: Final = "aftership" + +ATTRIBUTION: Final = "Information provided by AfterShip" +ATTR_TRACKINGS: Final = "trackings" + +BASE: Final = "https://track.aftership.com/" + +CONF_SLUG: Final = "slug" +CONF_TITLE: Final = "title" +CONF_TRACKING_NUMBER: Final = "tracking_number" + +DEFAULT_NAME: Final = "aftership" +UPDATE_TOPIC: Final = f"{DOMAIN}_update" + +ICON: Final = "mdi:package-variant-closed" + +MIN_TIME_BETWEEN_UPDATES: Final = timedelta(minutes=15) + +SERVICE_ADD_TRACKING: Final = "add_tracking" +SERVICE_REMOVE_TRACKING: Final = "remove_tracking" + +ADD_TRACKING_SERVICE_SCHEMA: Final = vol.Schema( + { + vol.Required(CONF_TRACKING_NUMBER): cv.string, + vol.Optional(CONF_TITLE): cv.string, + vol.Optional(CONF_SLUG): cv.string, + } +) + +REMOVE_TRACKING_SERVICE_SCHEMA: Final = vol.Schema( + {vol.Required(CONF_SLUG): cv.string, vol.Required(CONF_TRACKING_NUMBER): cv.string} +) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index a5ffc511a26..4d3fb17b949 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -1,53 +1,47 @@ """Support for non-delivered packages recorded in AfterShip.""" -from datetime import timedelta +from __future__ import annotations + import logging +from typing import Any, Final from pyaftership.tracker import Tracking import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, HTTP_OK +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.service import ServiceCall +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -ATTRIBUTION = "Information provided by AfterShip" -ATTR_TRACKINGS = "trackings" - -BASE = "https://track.aftership.com/" - -CONF_SLUG = "slug" -CONF_TITLE = "title" -CONF_TRACKING_NUMBER = "tracking_number" - -DEFAULT_NAME = "aftership" -UPDATE_TOPIC = f"{DOMAIN}_update" - -ICON = "mdi:package-variant-closed" - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) - -SERVICE_ADD_TRACKING = "add_tracking" -SERVICE_REMOVE_TRACKING = "remove_tracking" - -ADD_TRACKING_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(CONF_TRACKING_NUMBER): cv.string, - vol.Optional(CONF_TITLE): cv.string, - vol.Optional(CONF_SLUG): cv.string, - } +from .const import ( + ADD_TRACKING_SERVICE_SCHEMA, + ATTR_TRACKINGS, + ATTRIBUTION, + BASE, + CONF_SLUG, + CONF_TITLE, + CONF_TRACKING_NUMBER, + DEFAULT_NAME, + DOMAIN, + ICON, + MIN_TIME_BETWEEN_UPDATES, + REMOVE_TRACKING_SERVICE_SCHEMA, + SERVICE_ADD_TRACKING, + SERVICE_REMOVE_TRACKING, + UPDATE_TOPIC, ) -REMOVE_TRACKING_SERVICE_SCHEMA = vol.Schema( - {vol.Required(CONF_SLUG): cv.string, vol.Required(CONF_TRACKING_NUMBER): cv.string} -) +_LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -55,7 +49,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the AfterShip sensor platform.""" apikey = config[CONF_API_KEY] name = config[CONF_NAME] @@ -75,7 +74,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([instance], True) - async def handle_add_tracking(call): + async def handle_add_tracking(call: ServiceCall) -> None: """Call when a user adds a new Aftership tracking from Home Assistant.""" title = call.data.get(CONF_TITLE) slug = call.data.get(CONF_SLUG) @@ -91,7 +90,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= schema=ADD_TRACKING_SERVICE_SCHEMA, ) - async def handle_remove_tracking(call): + async def handle_remove_tracking(call: ServiceCall) -> None: """Call when a user removes an Aftership tracking from Home Assistant.""" slug = call.data[CONF_SLUG] tracking_number = call.data[CONF_TRACKING_NUMBER] @@ -110,39 +109,39 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class AfterShipSensor(SensorEntity): """Representation of a AfterShip sensor.""" - def __init__(self, aftership, name): + def __init__(self, aftership: Tracking, name: str) -> None: """Initialize the sensor.""" - self._attributes = {} - self._name = name - self._state = None + self._attributes: dict[str, Any] = {} + self._name: str = name + self._state: int | None = None self.aftership = aftership @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def state(self): + def state(self) -> int | None: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return "packages" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return attributes for the sensor.""" return self._attributes @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend.""" return ICON - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( @@ -150,13 +149,13 @@ class AfterShipSensor(SensorEntity): ) ) - async def _force_update(self): + async def _force_update(self) -> None: """Force update of data.""" await self.async_update(no_throttle=True) self.async_write_ha_state() @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self, **kwargs): + async def async_update(self, **kwargs: Any) -> None: """Get the latest data from the AfterShip API.""" await self.aftership.get_trackings() @@ -170,7 +169,7 @@ class AfterShipSensor(SensorEntity): return status_to_ignore = {"delivered"} - status_counts = {} + status_counts: dict[str, int] = {} trackings = [] not_delivered_count = 0 diff --git a/mypy.ini b/mypy.ini index 895878bbb6e..bf60ab9bdf9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -55,6 +55,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.aftership.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.airly.*] check_untyped_defs = true disallow_incomplete_defs = true From 7f6b8bbd1ed68b0104bf3c5f18e7e06f65d258d8 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 15 May 2021 22:53:42 +0200 Subject: [PATCH 475/852] Add strict type annotations to aladdin_connect (#50693) * add strict type annotations * add missing return type annotation --- .coveragerc | 2 +- .strict-typing | 1 + .../components/aladdin_connect/const.py | 19 ++++++ .../components/aladdin_connect/cover.py | 61 +++++++++---------- .../components/aladdin_connect/model.py | 13 ++++ mypy.ini | 11 ++++ 6 files changed, 75 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/const.py create mode 100644 homeassistant/components/aladdin_connect/model.py diff --git a/.coveragerc b/.coveragerc index d38f9e5b992..460d3d2bc76 100644 --- a/.coveragerc +++ b/.coveragerc @@ -35,7 +35,7 @@ omit = homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/air_quality.py homeassistant/components/airvisual/sensor.py - homeassistant/components/aladdin_connect/cover.py + homeassistant/components/aladdin_connect/* homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py diff --git a/.strict-typing b/.strict-typing index 638c5de5ae2..389ff5261f8 100644 --- a/.strict-typing +++ b/.strict-typing @@ -6,6 +6,7 @@ homeassistant.components homeassistant.components.acer_projector.* homeassistant.components.aftership.* homeassistant.components.airly.* +homeassistant.components.aladdin_connect.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* homeassistant.components.bond.* diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py new file mode 100644 index 00000000000..7bfea738cef --- /dev/null +++ b/homeassistant/components/aladdin_connect/const.py @@ -0,0 +1,19 @@ +"""Platform for the Aladdin Connect cover component.""" +from __future__ import annotations + +from typing import Final + +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING + +NOTIFICATION_ID: Final = "aladdin_notification" +NOTIFICATION_TITLE: Final = "Aladdin Connect Cover Setup" + +STATES_MAP: Final[dict[str, str]] = { + "open": STATE_OPEN, + "opening": STATE_OPENING, + "closed": STATE_CLOSED, + "closing": STATE_CLOSING, +} + +SUPPORTED_FEATURES: Final = SUPPORT_OPEN | SUPPORT_CLOSE diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 8b61d29b78a..d4ae9cbb2fd 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,13 +1,14 @@ """Platform for the Aladdin Connect cover component.""" +from __future__ import annotations + import logging +from typing import Any, Final from aladdin_connect import AladdinConnectClient import voluptuous as vol from homeassistant.components.cover import ( - PLATFORM_SCHEMA, - SUPPORT_CLOSE, - SUPPORT_OPEN, + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, CoverEntity, ) from homeassistant.const import ( @@ -15,35 +16,33 @@ from homeassistant.const import ( CONF_USERNAME, STATE_CLOSED, STATE_CLOSING, - STATE_OPEN, STATE_OPENING, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -_LOGGER = logging.getLogger(__name__) +from .const import NOTIFICATION_ID, NOTIFICATION_TITLE, STATES_MAP, SUPPORTED_FEATURES +from .model import DoorDevice -NOTIFICATION_ID = "aladdin_notification" -NOTIFICATION_TITLE = "Aladdin Connect Cover Setup" +_LOGGER: Final = logging.getLogger(__name__) -STATES_MAP = { - "open": STATE_OPEN, - "opening": STATE_OPENING, - "closed": STATE_CLOSED, - "closing": STATE_CLOSING, -} - -SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the Aladdin Connect platform.""" - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] + username: str = config[CONF_USERNAME] + password: str = config[CONF_PASSWORD] acc = AladdinConnectClient(username, password) try: @@ -62,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AladdinDevice(CoverEntity): """Representation of Aladdin Connect cover.""" - def __init__(self, acc, device): + def __init__(self, acc: AladdinConnectClient, device: DoorDevice) -> None: """Initialize the cover.""" self._acc = acc self._device_id = device["device_id"] @@ -71,51 +70,51 @@ class AladdinDevice(CoverEntity): self._status = STATES_MAP.get(device["status"]) @property - def device_class(self): + def device_class(self) -> str: """Define this cover as a garage door.""" return "garage" @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORTED_FEATURES @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return f"{self._device_id}-{self._number}" @property - def name(self): + def name(self) -> str: """Return the name of the garage door.""" return self._name @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._status == STATE_OPENING @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._status == STATE_CLOSING @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return None if status is unknown, True if closed, else False.""" if self._status is None: return None return self._status == STATE_CLOSED - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Issue close command to cover.""" self._acc.close_door(self._device_id, self._number) - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Issue open command to cover.""" self._acc.open_door(self._device_id, self._number) - def update(self): + def update(self) -> None: """Update status of cover.""" acc_status = self._acc.get_door_status(self._device_id, self._number) self._status = STATES_MAP.get(acc_status) diff --git a/homeassistant/components/aladdin_connect/model.py b/homeassistant/components/aladdin_connect/model.py new file mode 100644 index 00000000000..4248f3504fe --- /dev/null +++ b/homeassistant/components/aladdin_connect/model.py @@ -0,0 +1,13 @@ +"""Models for Aladdin connect cover platform.""" +from __future__ import annotations + +from typing import TypedDict + + +class DoorDevice(TypedDict): + """Aladdin door device.""" + + device_id: str + door_number: int + name: str + status: str diff --git a/mypy.ini b/mypy.ini index bf60ab9bdf9..8dd42547fad 100644 --- a/mypy.ini +++ b/mypy.ini @@ -77,6 +77,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.aladdin_connect.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.automation.*] check_untyped_defs = true disallow_incomplete_defs = true From 256a2de7ce290036aad48057eb643884fb2ea8be Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sat, 15 May 2021 22:55:50 +0200 Subject: [PATCH 476/852] Add kraken code review changes (#50683) --- homeassistant/components/kraken/__init__.py | 4 ++- .../components/kraken/config_flow.py | 5 --- homeassistant/components/kraken/sensor.py | 34 ++++++++++--------- tests/components/kraken/test_config_flow.py | 1 + tests/components/kraken/test_init.py | 3 +- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index 0cadf051948..057156005e7 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -34,7 +34,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b kraken_data = KrakenData(hass, config_entry) await kraken_data.async_setup() hass.data[DOMAIN] = kraken_data - config_entry.add_update_listener(async_options_updated) + config_entry.async_on_unload( + config_entry.add_update_listener(async_options_updated) + ) hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py index 2c0afc800e6..87ab2262029 100644 --- a/homeassistant/components/kraken/config_flow.py +++ b/homeassistant/components/kraken/config_flow.py @@ -8,7 +8,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from .const import CONF_TRACKED_ASSET_PAIRS, DEFAULT_SCAN_INTERVAL, DOMAIN @@ -75,7 +74,3 @@ class KrakenOptionsFlowHandler(config_entries.OptionsFlow): } return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) - - -class AlreadyConfigured(HomeAssistantError): - """Error to indicate the asset pair is already configured.""" diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index e7009915fd9..1fab821f5dc 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -3,9 +3,10 @@ from __future__ import annotations import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import async_entries_for_config_entry +from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -24,24 +25,24 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Add kraken entities from a config_entry.""" @callback - async def async_update_sensors( - hass: HomeAssistant, config_entry: ConfigEntry - ) -> None: - device_registry = await hass.helpers.device_registry.async_get_registry() + def async_update_sensors(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + dev_reg = device_registry.async_get(hass) existing_devices = { device.name: device.id - for device in async_entries_for_config_entry( - device_registry, config_entry.entry_id + for device in device_registry.async_entries_for_config_entry( + dev_reg, config_entry.entry_id ) } + sensors = [] for tracked_asset_pair in config_entry.options[CONF_TRACKED_ASSET_PAIRS]: # Only create new devices - if create_device_name(tracked_asset_pair) in existing_devices: - existing_devices.pop(create_device_name(tracked_asset_pair)) + if ( + device_name := create_device_name(tracked_asset_pair) + ) in existing_devices: + existing_devices.pop(device_name) else: - sensors = [] for sensor_type in SENSOR_TYPES: sensors.append( KrakenSensor( @@ -50,13 +51,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensor_type, ) ) - async_add_entities(sensors, True) + async_add_entities(sensors, True) # Remove devices for asset pairs which are no longer tracked for device_id in existing_devices.values(): - device_registry.async_remove_device(device_id) + dev_reg.async_remove_device(device_id) - await async_update_sensors(hass, config_entry) + async_update_sensors(hass, config_entry) hass.data[DOMAIN].unsub_listeners.append( async_dispatcher_connect( @@ -67,7 +68,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class KrakenSensor(CoordinatorEntity): +class KrakenSensor(CoordinatorEntity, SensorEntity): """Define a Kraken sensor.""" def __init__( @@ -125,7 +126,7 @@ class KrakenSensor(CoordinatorEntity): def _handle_coordinator_update(self): self._update_internal_state() - return super()._handle_coordinator_update() + super()._handle_coordinator_update() def _update_internal_state(self): try: @@ -207,6 +208,7 @@ class KrakenSensor(CoordinatorEntity): """Return the unit the value is expressed in.""" if "number_of" not in self._sensor_type: return self._unit_of_measurement + return None @property def available(self): @@ -218,7 +220,7 @@ class KrakenSensor(CoordinatorEntity): """Return a device description for device registry.""" return { - "identifiers": {(DOMAIN, self._source_asset, self._target_asset)}, + "identifiers": {(DOMAIN, f"{self._source_asset}_{self._target_asset}")}, "name": self._device_name, "manufacturer": "Kraken.com", "entry_type": "service", diff --git a/tests/components/kraken/test_config_flow.py b/tests/components/kraken/test_config_flow.py index 6f29273e1a7..1a09fbe92c6 100644 --- a/tests/components/kraken/test_config_flow.py +++ b/tests/components/kraken/test_config_flow.py @@ -52,6 +52,7 @@ async def test_already_configured(hass): DOMAIN, context={"source": "user"} ) assert result["type"] == "abort" + assert result["reason"] == "already_configured" async def test_options(hass): diff --git a/tests/components/kraken/test_init.py b/tests/components/kraken/test_init.py index 69cfde42547..742e48eb1c0 100644 --- a/tests/components/kraken/test_init.py +++ b/tests/components/kraken/test_init.py @@ -3,7 +3,6 @@ from unittest.mock import patch from pykrakenapi.pykrakenapi import CallRateLimitError, KrakenAPIError -from homeassistant.components import kraken from homeassistant.components.kraken.const import DOMAIN from .const import TICKER_INFORMATION_RESPONSE, TRADEABLE_ASSET_PAIR_RESPONSE @@ -25,7 +24,7 @@ async def test_unload_entry(hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert await kraken.async_unload_entry(hass, entry) + assert await hass.config_entries.async_unload(entry.entry_id) assert DOMAIN not in hass.data From edccb7eb5857771c9ab0a8be2b0745883fef8b21 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 15 May 2021 23:59:57 +0200 Subject: [PATCH 477/852] Add strict type annotations to actiontect (#50672) * add strict type annotations * fix pylint, add coverage omit * apply suggestions * fix rebase conflict * import PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA * correct get_device_name() return annotation --- .coveragerc | 2 + .strict-typing | 1 + homeassistant/components/actiontec/const.py | 12 +++ .../components/actiontec/device_tracker.py | 80 +++++++++---------- homeassistant/components/actiontec/model.py | 11 +++ mypy.ini | 11 +++ 6 files changed, 74 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/actiontec/const.py create mode 100644 homeassistant/components/actiontec/model.py diff --git a/.coveragerc b/.coveragerc index 460d3d2bc76..6f6fa152fa7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,7 +9,9 @@ omit = # omit pieces of code that rely on external devices being present homeassistant/components/acer_projector/* + homeassistant/components/actiontec/const.py homeassistant/components/actiontec/device_tracker.py + homeassistant/components/actiontec/model.py homeassistant/components/acmeda/__init__.py homeassistant/components/acmeda/base.py homeassistant/components/acmeda/const.py diff --git a/.strict-typing b/.strict-typing index 389ff5261f8..7abbfbf7051 100644 --- a/.strict-typing +++ b/.strict-typing @@ -4,6 +4,7 @@ homeassistant.components homeassistant.components.acer_projector.* +homeassistant.components.actiontec.* homeassistant.components.aftership.* homeassistant.components.airly.* homeassistant.components.aladdin_connect.* diff --git a/homeassistant/components/actiontec/const.py b/homeassistant/components/actiontec/const.py new file mode 100644 index 00000000000..1043bd1bdb6 --- /dev/null +++ b/homeassistant/components/actiontec/const.py @@ -0,0 +1,12 @@ +"""Support for Actiontec MI424WR (Verizon FIOS) routers.""" +from __future__ import annotations + +import re +from typing import Final + +LEASES_REGEX: Final[re.Pattern] = re.compile( + r"(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})" + + r"\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))" + + r"\svalid\sfor:\s(?P(-?\d+))" + + r"\ssec" +) diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index c88ed546b9d..3783ad881e2 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -1,30 +1,28 @@ """Support for Actiontec MI424WR (Verizon FIOS) routers.""" -from collections import namedtuple +from __future__ import annotations + import logging -import re import telnetlib +from typing import Final import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util +from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) +from .const import LEASES_REGEX +from .model import Device -_LEASES_REGEX = re.compile( - r"(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})" - + r"\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))" - + r"\svalid\sfor:\s(?P(-?\d+))" - + r"\ssec" -) +_LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, @@ -33,43 +31,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_scanner(hass, config): +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> ActiontecDeviceScanner | None: """Validate the configuration and return an Actiontec scanner.""" scanner = ActiontecDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None -Device = namedtuple("Device", ["mac", "ip", "last_update"]) - - class ActiontecDeviceScanner(DeviceScanner): """This class queries an actiontec router for connected devices.""" - def __init__(self, config): + def __init__(self, config: ConfigType) -> None: """Initialize the scanner.""" - self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] - self.last_results = [] + self.host: str = config[CONF_HOST] + self.username: str = config[CONF_USERNAME] + self.password: str = config[CONF_PASSWORD] + self.last_results: list[Device] = [] data = self.get_actiontec_data() self.success_init = data is not None _LOGGER.info("Scanner initialized") - def scan_devices(self): + def scan_devices(self) -> list[str]: """Scan for new devices and return a list with found device IDs.""" self._update_info() - return [client.mac for client in self.last_results] + return [client.mac_address for client in self.last_results] - def get_device_name(self, device): + def get_device_name(self, device: str) -> str | None: # type: ignore[override] """Return the name of the given device or None if we don't know.""" - if not self.last_results: - return None for client in self.last_results: - if client.mac == device: - return client.ip + if client.mac_address == device: + return client.ip_address return None - def _update_info(self): + def _update_info(self) -> bool: """Ensure the information from the router is up to date. Return boolean if scanning successful. @@ -78,19 +73,16 @@ class ActiontecDeviceScanner(DeviceScanner): if not self.success_init: return False - now = dt_util.now() actiontec_data = self.get_actiontec_data() - if not actiontec_data: + if actiontec_data is None: return False self.last_results = [ - Device(data["mac"], name, now) - for name, data in actiontec_data.items() - if data["timevalid"] > -60 + device for device in actiontec_data if device.timevalid > -60 ] _LOGGER.info("Scan successful") return True - def get_actiontec_data(self): + def get_actiontec_data(self) -> list[Device] | None: """Retrieve data from Actiontec MI424WR and return parsed result.""" try: telnet = telnetlib.Telnet(self.host) @@ -106,18 +98,20 @@ class ActiontecDeviceScanner(DeviceScanner): telnet.write(b"exit\n") except EOFError: _LOGGER.exception("Unexpected response from router") - return + return None except ConnectionRefusedError: _LOGGER.exception("Connection refused by router. Telnet enabled?") return None - devices = {} + devices: list[Device] = [] for lease in leases_result: - match = _LEASES_REGEX.search(lease.decode("utf-8")) + match = LEASES_REGEX.search(lease.decode("utf-8")) if match is not None: - devices[match.group("ip")] = { - "ip": match.group("ip"), - "mac": match.group("mac").upper(), - "timevalid": int(match.group("timevalid")), - } + devices.append( + Device( + match.group("ip"), + match.group("mac").upper(), + int(match.group("timevalid")), + ) + ) return devices diff --git a/homeassistant/components/actiontec/model.py b/homeassistant/components/actiontec/model.py new file mode 100644 index 00000000000..ff28d6d4ac6 --- /dev/null +++ b/homeassistant/components/actiontec/model.py @@ -0,0 +1,11 @@ +"""Model definitions for Actiontec MI424WR (Verizon FIOS) routers.""" +from dataclasses import dataclass + + +@dataclass +class Device: + """Actiontec device class.""" + + ip_address: str + mac_address: str + timevalid: int diff --git a/mypy.ini b/mypy.ini index 8dd42547fad..1506a06e839 100644 --- a/mypy.ini +++ b/mypy.ini @@ -55,6 +55,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.actiontec.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.aftership.*] check_untyped_defs = true disallow_incomplete_defs = true From b8713774c834cb1e6ef0e489642866ebcc71b8d8 Mon Sep 17 00:00:00 2001 From: Zac West <74188+zacwest@users.noreply.github.com> Date: Sat, 15 May 2021 22:50:24 -0700 Subject: [PATCH 478/852] Make confirmable notification blueprint use unique actions (#50706) --- .../blueprints/confirmable_notification.yaml | 18 ++++++++--- tests/components/script/test_blueprint.py | 31 +++++++++++-------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/script/blueprints/confirmable_notification.yaml b/homeassistant/components/script/blueprints/confirmable_notification.yaml index ae170eab0a7..d52e5a61651 100644 --- a/homeassistant/components/script/blueprints/confirmable_notification.yaml +++ b/homeassistant/components/script/blueprints/confirmable_notification.yaml @@ -51,6 +51,10 @@ blueprint: mode: restart sequence: + - alias: "Set up variables" + variables: + action_confirm: "{{ 'CONFIRM_' ~ context.id }}" + action_dismiss: "{{ 'DISMISS_' ~ context.id }}" - alias: "Send notification" domain: mobile_app type: notify @@ -59,16 +63,22 @@ sequence: message: !input message data: actions: - - action: "CONFIRM" + - action: "{{ action_confirm }}" title: !input confirm_text - - action: "DISMISS" + - action: "{{ action_dismiss }}" title: !input dismiss_text - alias: "Awaiting response" wait_for_trigger: - platform: event event_type: mobile_app_notification_action + event_data: + action: "{{ action_confirm }}" + - platform: event + event_type: mobile_app_notification_action + event_data: + action: "{{ action_dismiss }}" - choose: - - conditions: "{{ wait.trigger.event.data.action == 'CONFIRM' }}" + - conditions: "{{ wait.trigger.event.data.action == action_confirm }}" sequence: !input confirm_action - - conditions: "{{ wait.trigger.event.data.action == 'DISMISS' }}" + - conditions: "{{ wait.trigger.event.data.action == action_dismiss }}" sequence: !input dismiss_action diff --git a/tests/components/script/test_blueprint.py b/tests/components/script/test_blueprint.py index d5ba914df05..1c02a35792b 100644 --- a/tests/components/script/test_blueprint.py +++ b/tests/components/script/test_blueprint.py @@ -7,7 +7,8 @@ from unittest.mock import patch from homeassistant.components import script from homeassistant.components.blueprint.models import Blueprint, DomainBlueprints -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Context, HomeAssistant, callback +from homeassistant.helpers import template from homeassistant.setup import async_setup_component from homeassistant.util import yaml @@ -70,44 +71,48 @@ async def test_confirmable_notification(hass: HomeAssistant) -> None: ) turn_on_calls = async_mock_service(hass, "homeassistant", "turn_on") + context = Context() with patch( "homeassistant.components.mobile_app.device_action.async_call_action_from_config" ) as mock_call_action: # Trigger script - await hass.services.async_call(script.DOMAIN, "confirm") + await hass.services.async_call(script.DOMAIN, "confirm", context=context) # Give script the time to attach the trigger. await asyncio.sleep(0.1) - hass.bus.async_fire("mobile_app_notification_action", {"action": "CONFIRM"}) + hass.bus.async_fire("mobile_app_notification_action", {"action": "ANYTHING_ELSE"}) + hass.bus.async_fire( + "mobile_app_notification_action", {"action": "CONFIRM_" + Context().id} + ) + hass.bus.async_fire( + "mobile_app_notification_action", {"action": "CONFIRM_" + context.id} + ) await hass.async_block_till_done() assert len(mock_call_action.mock_calls) == 1 _hass, config, variables, _context = mock_call_action.mock_calls[0][1] - title_tpl = config.pop("title") - message_tpl = config.pop("message") - title_tpl.hass = hass - message_tpl.hass = hass + template.attach(hass, config) + rendered_config = template.render_complex(config, variables) - assert config == { + assert rendered_config == { + "title": "Lord of the things", + "message": "Throw ring in mountain?", "alias": "Send notification", "domain": "mobile_app", "type": "notify", "device_id": "frodo", "data": { "actions": [ - {"action": "CONFIRM", "title": "Confirm"}, - {"action": "DISMISS", "title": "Dismiss"}, + {"action": "CONFIRM_" + _context.id, "title": "Confirm"}, + {"action": "DISMISS_" + _context.id, "title": "Dismiss"}, ] }, } - assert title_tpl.async_render(variables) == "Lord of the things" - assert message_tpl.async_render(variables) == "Throw ring in mountain?" - assert len(turn_on_calls) == 1 assert turn_on_calls[0].data == { "entity_id": ["mount.doom"], From a92acdb52825cf94707fae870693d7f6935b4da9 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sun, 16 May 2021 08:06:28 +0200 Subject: [PATCH 479/852] Fix selectors and defaults in LCN service.yaml (#50705) --- homeassistant/components/lcn/services.yaml | 422 ++++++++++----------- 1 file changed, 211 insertions(+), 211 deletions(-) diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml index 8755551c46a..8ca4a6f3d4e 100644 --- a/homeassistant/components/lcn/services.yaml +++ b/homeassistant/components/lcn/services.yaml @@ -19,10 +19,10 @@ output_abs: selector: select: options: - - 'OUTPUT1' - - 'OUTPUT2' - - 'OUTPUT3' - - 'OUTPUT4' + - "output1" + - "output2" + - "output3" + - "output4" brightness: name: Brightness description: Absolute brightness in percent. @@ -63,10 +63,10 @@ output_rel: selector: select: options: - - 'OUTPUT1' - - 'OUTPUT2' - - 'OUTPUT3' - - 'OUTPUT4' + - "output1" + - "output2" + - "output3" + - "output4" brightness: name: Brightness description: Relative brightness in percent. @@ -76,7 +76,7 @@ output_rel: number: min: -100 max: 100 - unit_of_measurement: '%' + unit_of_measurement: "%" output_toggle: name: Toggle output @@ -97,10 +97,10 @@ output_toggle: selector: select: options: - - 'OUTPUT1' - - 'OUTPUT2' - - 'OUTPUT3' - - 'OUTPUT4' + - "output1" + - "output2" + - "output3" + - "output4" transition: name: Transition description: Transition time. @@ -151,18 +151,18 @@ led: selector: select: options: - - 'LED1' - - 'LED2' - - 'LED3' - - 'LED4' - - 'LED5' - - 'LED6' - - 'LED7' - - 'LED8' - - 'LED9' - - 'LED10' - - 'LED11' - - 'LED12' + - "led1" + - "led2" + - "led3" + - "led4" + - "led5" + - "led6" + - "led7" + - "led8" + - "led9" + - "led10" + - "led11" + - "led12" state: name: State description: Led state @@ -171,10 +171,10 @@ led: selector: select: options: - - 'blink' - - 'flicker' - - 'off' - - 'on' + - "blink" + - "flicker" + - "off" + - "on" var_abs: name: Set absolute variable @@ -192,30 +192,30 @@ var_abs: description: Variable or setpoint name required: true example: "var1" - default: NATIVE + default: native selector: select: options: - - 'R1VAR' - - 'R2VAR' - - 'R1VARSETPOINT' - - 'R2VARSETPOINT' - - 'TVAR' - - 'VAR1ORTVAR' - - 'VAR2ORR1VAR' - - 'VAR3ORR2VAR' - - 'VAR1' - - 'VAR2' - - 'VAR3' - - 'VAR4' - - 'VAR5' - - 'VAR6' - - 'VAR7' - - 'VAR8' - - 'VAR9' - - 'VAR10' - - 'VAR11' - - 'VAR12' + - "r1var" + - "r2var" + - "r1varsetpoint" + - "r2varsetpoint" + - "tvar" + - "var1ortvar" + - "var2orr1var" + - "var3orr2var" + - "var1" + - "var2" + - "var3" + - "var4" + - "var5" + - "var6" + - "var7" + - "var8" + - "var9" + - "var10" + - "var11" + - "var12" value: name: Value description: Value to set @@ -232,29 +232,29 @@ var_abs: selector: select: options: - - '' - - '%' - - '°' - - '°C' - - '°F' - - 'AMPERE' - - 'AMP' - - 'A' - - 'DEGREE' - - 'NATIVE' - - 'K' - - 'LCN' - - 'LUX_T' - - 'LX_T' - - 'LUX_I' - - 'LUX' - - 'LX' - - 'M/S' - - 'METERPERSECOND' - - 'PERCENT' - - 'PPM' - - 'V' - - 'VOLT' + - "" + - "%" + - "°" + - "°C" + - "°F" + - "ampere" + - "amp" + - "a" + - "degree" + - "native" + - "k" + - "lcn" + - "lux_t" + - "lx_t" + - "lux_i" + - "lux" + - "lx" + - "m/s" + - "meterpersecond" + - "percent" + - "ppm" + - "v" + - "volt" var_reset: name: Reset variable @@ -273,26 +273,26 @@ var_reset: selector: select: options: - - 'R1VAR' - - 'R2VAR' - - 'R1VARSETPOINT' - - 'R2VARSETPOINT' - - 'TVAR' - - 'VAR1ORTVAR' - - 'VAR2ORR1VAR' - - 'VAR3ORR2VAR' - - 'VAR1' - - 'VAR2' - - 'VAR3' - - 'VAR4' - - 'VAR5' - - 'VAR6' - - 'VAR7' - - 'VAR8' - - 'VAR9' - - 'VAR10' - - 'VAR11' - - 'VAR12' + - "r1var" + - "r2var" + - "r1varsetpoint" + - "r2varsetpoint" + - "tvar" + - "var1ortvar" + - "var2orr1var" + - "var3orr2var" + - "var1" + - "var2" + - "var3" + - "var4" + - "var5" + - "var6" + - "var7" + - "var8" + - "var9" + - "var10" + - "var11" + - "var12" var_rel: name: Shift variable @@ -313,43 +313,43 @@ var_rel: selector: select: options: - - 'R1VAR' - - 'R2VAR' - - 'R1VARSETPOINT' - - 'R2VARSETPOINT' - - 'THRS1' - - 'THRS2' - - 'THRS3' - - 'THRS4' - - 'THRS5' - - 'THRS2_1' - - 'THRS2_2' - - 'THRS2_3' - - 'THRS2_4' - - 'THRS3_1' - - 'THRS3_2' - - 'THRS3_3' - - 'THRS3_4' - - 'THRS4_1' - - 'THRS4_2' - - 'THRS4_3' - - 'THRS4_4' - - 'TVAR' - - 'VAR1ORTVAR' - - 'VAR2ORR1VAR' - - 'VAR3ORR2VAR' - - 'VAR1' - - 'VAR2' - - 'VAR3' - - 'VAR4' - - 'VAR5' - - 'VAR6' - - 'VAR7' - - 'VAR8' - - 'VAR9' - - 'VAR10' - - 'VAR11' - - 'VAR12' + - "r1var" + - "r2var" + - "r1varsetpoint" + - "r2varsetpoint" + - "thrs1" + - "thrs2" + - "thrs3" + - "thrs4" + - "thrs5" + - "thrs2_1" + - "thrs2_2" + - "thrs2_3" + - "thrs2_4" + - "thrs3_1" + - "thrs3_2" + - "thrs3_3" + - "thrs3_4" + - "thrs4_1" + - "thrs4_2" + - "thrs4_3" + - "thrs4_4" + - "tvar" + - "var1ortvar" + - "var2orr1var" + - "var3orr2var" + - "var1" + - "var2" + - "var3" + - "var4" + - "var5" + - "var6" + - "var7" + - "var8" + - "var9" + - "var10" + - "var11" + - "var12" value: name: Value description: Shift value @@ -363,43 +363,43 @@ var_rel: name: Unit of measurement description: Unit of value example: "celsius" - default: NATIVE + default: native selector: select: options: - - '' - - '%' - - '°' - - '°C' - - '°F' - - 'AMPERE' - - 'AMP' - - 'A' - - 'DEGREE' - - 'NATIVE' - - 'K' - - 'LCN' - - 'LUX_T' - - 'LX_T' - - 'LUX_I' - - 'LUX' - - 'LX' - - 'M/S' - - 'METERPERSECOND' - - 'PERCENT' - - 'PPM' - - 'V' - - 'VOLT' + - "" + - "%" + - "°" + - "°C" + - "°F" + - "ampere" + - "amp" + - "a" + - "degree" + - "native" + - "k" + - "lcn" + - "lux_t" + - "lx_t" + - "lux_i" + - "lux" + - "lx" + - "m/s" + - "meterpersecond" + - "percent" + - "ppm" + - "v" + - "volt" value_reference: name: Reference value description: Reference value for setpoint and threshold example: "current" - default: CURRENT + default: current selector: select: options: - - 'CURRENT' - - 'PROG' + - "current" + - "prog" lock_regulator: name: Lock regulator @@ -420,23 +420,23 @@ lock_regulator: selector: select: options: - - 'THRS1' - - 'THRS2' - - 'THRS3' - - 'THRS4' - - 'THRS5' - - 'THRS2_1' - - 'THRS2_2' - - 'THRS2_3' - - 'THRS2_4' - - 'THRS3_1' - - 'THRS3_2' - - 'THRS3_3' - - 'THRS3_4' - - 'THRS4_1' - - 'THRS4_2' - - 'THRS4_3' - - 'THRS4_4' + - "thrs1" + - "thrs2" + - "thrs3" + - "thrs4" + - "thrs5" + - "thrs2_1" + - "thrs2_2" + - "thrs2_3" + - "thrs2_4" + - "thrs3_1" + - "thrs3_2" + - "thrs3_3" + - "thrs3_4" + - "thrs4_1" + - "thrs4_2" + - "thrs4_3" + - "thrs4_4" state: name: State description: New setpoint state @@ -467,14 +467,14 @@ send_keys: name: State description: "Key state upon sending (optional, must be hit for deferred)" example: "hit" - default: HIT + default: hit selector: select: options: - - 'HIT' - - 'MAKE' - - 'BREAK' - - 'DONTSEND' + - "hit" + - "make" + - "break" + - "dontsend" time: name: Time description: Send delay. @@ -488,24 +488,24 @@ send_keys: name: Time unit description: Time unit of send delay. example: "s" - default: S + default: s selector: select: options: - - 'D' - - 'DAY' - - 'DAYS' - - 'H' - - 'HOUR' - - 'HOURS' - - 'M' - - 'MIN' - - 'MINUTE' - - 'MINUTES' - - 'S' - - 'SEC' - - 'SECOND' - - 'SECONDS' + - "d" + - "day" + - "days" + - "h" + - "hour" + - "hours" + - "m" + - "min" + - "minute" + - "minutes" + - "s" + - "sec" + - "second" + - "seconds" lock_keys: name: Lock keys @@ -521,8 +521,8 @@ lock_keys: table: name: Table description: "Table with keys to lock (must be A for interval)." - example: "A5" - default: A + example: "a5" + default: a selector: text: state: @@ -545,24 +545,24 @@ lock_keys: name: Time unit description: Time unit of lock interval. example: "s" - default: S + default: s selector: select: options: - - 'D' - - 'DAY' - - 'DAYS' - - 'H' - - 'HOUR' - - 'HOURS' - - 'M' - - 'MIN' - - 'MINUTE' - - 'MINUTES' - - 'S' - - 'SEC' - - 'SECOND' - - 'SECONDS' + - "d" + - "day" + - "days" + - "h" + - "hour" + - "hours" + - "m" + - "min" + - "minute" + - "minutes" + - "s" + - "sec" + - "second" + - "seconds" dyn_text: name: Dynamic text From 222336a1db1ad3908fd4e3c48444048a71550d89 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 16 May 2021 08:14:28 +0200 Subject: [PATCH 480/852] Create KNX scene entities directly from config (#50686) --- homeassistant/components/knx/factory.py | 15 ------------- homeassistant/components/knx/scene.py | 28 +++++++++++++++++++------ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index ebc9bfc0ce2..b62023234e8 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -11,7 +11,6 @@ from xknx.devices import ( Fan as XknxFan, Light as XknxLight, Notification as XknxNotification, - Scene as XknxScene, Sensor as XknxSensor, Weather as XknxWeather, ) @@ -26,7 +25,6 @@ from .schema import ( CoverSchema, FanSchema, LightSchema, - SceneSchema, SensorSchema, WeatherSchema, ) @@ -53,9 +51,6 @@ def create_knx_device( if platform is SupportedPlatforms.NOTIFY: return _create_notify(knx_module, config) - if platform is SupportedPlatforms.SCENE: - return _create_scene(knx_module, config) - if platform is SupportedPlatforms.BINARY_SENSOR: return _create_binary_sensor(knx_module, config) @@ -288,16 +283,6 @@ def _create_notify(knx_module: XKNX, config: ConfigType) -> XknxNotification: ) -def _create_scene(knx_module: XKNX, config: ConfigType) -> XknxScene: - """Return a KNX scene to be used within XKNX.""" - return XknxScene( - knx_module, - name=config[CONF_NAME], - group_address=config[KNX_ADDRESS], - scene_number=config[SceneSchema.CONF_SCENE_NUMBER], - ) - - def _create_binary_sensor(knx_module: XKNX, config: ConfigType) -> XknxBinarySensor: """Return a KNX binary sensor to be used within XKNX.""" device_name = config[CONF_NAME] diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 23f375972e6..e3b00815424 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -3,15 +3,18 @@ from __future__ import annotations from typing import Any +from xknx import XKNX from xknx.devices import Scene as XknxScene from homeassistant.components.scene import Scene +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity +from .schema import SceneSchema async def async_setup_platform( @@ -21,20 +24,33 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the scenes for KNX platform.""" + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + entities = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxScene): - entities.append(KNXScene(device)) + for entity_config in platform_config: + entities.append(KNXScene(xknx, entity_config)) + async_add_entities(entities) class KNXScene(KnxEntity, Scene): """Representation of a KNX scene.""" - def __init__(self, device: XknxScene) -> None: + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Init KNX scene.""" self._device: XknxScene - super().__init__(device) + super().__init__( + device=XknxScene( + xknx, + name=config[CONF_NAME], + group_address=config[KNX_ADDRESS], + scene_number=config[SceneSchema.CONF_SCENE_NUMBER], + ) + ) self._unique_id = ( f"{self._device.scene_value.group_address}_{self._device.scene_number}" ) From 0556c35e240491e54405f7e95c7dc8eaa7d71e2f Mon Sep 17 00:00:00 2001 From: Dror Eiger <45061021+deiger@users.noreply.github.com> Date: Sun, 16 May 2021 09:26:16 +0300 Subject: [PATCH 481/852] Set zwave_js cover device_class for shutters and blinds (#50643) * Set device_class for shutters and blinds * Add missing. imports * Add tests for device class setting * Clean up * Avoid storing the node in an unused variable * Fix entity name * Extend qubino shutter discovery Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/cover.py | 12 + .../components/zwave_js/discovery.py | 6 +- tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_cover.py | 23 + .../zwave_js/cover_qubino_shutter_state.json | 765 ++++++++++++++++++ 5 files changed, 819 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/zwave_js/cover_qubino_shutter_state.json diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 4a73fa2bcab..302ccd9cd32 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -9,7 +9,10 @@ from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.cover import ( ATTR_POSITION, + DEVICE_CLASS_BLIND, DEVICE_CLASS_GARAGE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW, DOMAIN as COVER_DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, @@ -76,6 +79,15 @@ def percent_to_zwave_position(value: int) -> int: class ZWaveCover(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave Cover device.""" + @property + def device_class(self) -> str | None: + """Return the class of this device, from component DEVICE_CLASSES.""" + if self.info.platform_hint == "window_shutter": + return DEVICE_CLASS_SHUTTER + if self.info.platform_hint == "window_blind": + return DEVICE_CLASS_BLIND + return DEVICE_CLASS_WINDOW + @property def is_closed(self) -> bool | None: """Return true if cover is closed.""" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 3a47706dcaf..29976850480 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -220,6 +220,7 @@ DISCOVERY_SCHEMAS = [ # Fibaro Shutter Fibaro FGS222 ZWaveDiscoverySchema( platform="cover", + hint="window_shutter", manufacturer_id={0x010F}, product_id={0x1000}, product_type={0x0302}, @@ -228,14 +229,16 @@ DISCOVERY_SCHEMAS = [ # Qubino flush shutter ZWaveDiscoverySchema( platform="cover", + hint="window_shutter", manufacturer_id={0x0159}, - product_id={0x0052}, + product_id={0x0052, 0x0053}, product_type={0x0003}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # Graber/Bali/Spring Fashion Covers ZWaveDiscoverySchema( platform="cover", + hint="window_blind", manufacturer_id={0x026E}, product_id={0x5A31}, product_type={0x4353}, @@ -244,6 +247,7 @@ DISCOVERY_SCHEMAS = [ # iBlinds v2 window blind motor ZWaveDiscoverySchema( platform="cover", + hint="window_blind", manufacturer_id={0x0287}, product_id={0x000D}, product_type={0x0003}, diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 14937b0ff2a..5935f5da29e 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -282,6 +282,12 @@ def iblinds_v2_state_fixture(): return json.loads(load_fixture("zwave_js/cover_iblinds_v2_state.json")) +@pytest.fixture(name="qubino_shutter_state", scope="session") +def qubino_shutter_state_fixture(): + """Load the Qubino Shutter node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_qubino_shutter_state.json")) + + @pytest.fixture(name="aeon_smart_switch_6_state", scope="session") def aeon_smart_switch_6_state_fixture(): """Load the AEON Labs (ZW096) Smart Switch 6 node state fixture data.""" @@ -603,6 +609,14 @@ def iblinds_cover_fixture(client, iblinds_v2_state): return node +@pytest.fixture(name="qubino_shutter") +def qubino_shutter_cover_fixture(client, qubino_shutter_state): + """Mock a Qubino flush shutter node.""" + node = Node(client, copy.deepcopy(qubino_shutter_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="aeon_smart_switch_6") def aeon_smart_switch_6_fixture(client, aeon_smart_switch_6_state): """Mock an AEON Labs (ZW096) Smart Switch 6 node.""" diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 2378453e31a..9d7a16ac8cf 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -3,7 +3,10 @@ from zwave_js_server.event import Event from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, + DEVICE_CLASS_BLIND, DEVICE_CLASS_GARAGE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW, DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, @@ -19,6 +22,8 @@ from homeassistant.const import ( WINDOW_COVER_ENTITY = "cover.zws_12" GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5" +BLIND_COVER_ENTITY = "cover.window_blind_controller" +SHUTTER_COVER_ENTITY = "cover.flush_shutter_dc" async def test_window_cover(hass, client, chain_actuator_zws12, integration): @@ -27,6 +32,8 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): state = hass.states.get(WINDOW_COVER_ENTITY) assert state + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_WINDOW + assert state.state == "closed" assert state.attributes[ATTR_CURRENT_POSITION] == 0 @@ -299,6 +306,22 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert state.state == "closed" +async def test_blind_cover(hass, client, iblinds_v2, integration): + """Test a blind cover entity.""" + state = hass.states.get(BLIND_COVER_ENTITY) + + assert state + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_BLIND + + +async def test_shutter_cover(hass, client, qubino_shutter, integration): + """Test a shutter cover entity.""" + state = hass.states.get(SHUTTER_COVER_ENTITY) + + assert state + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SHUTTER + + async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): """Test the cover entity.""" node = gdc_zw062 diff --git a/tests/fixtures/zwave_js/cover_qubino_shutter_state.json b/tests/fixtures/zwave_js/cover_qubino_shutter_state.json new file mode 100644 index 00000000000..65725606e1c --- /dev/null +++ b/tests/fixtures/zwave_js/cover_qubino_shutter_state.json @@ -0,0 +1,765 @@ +{ + "nodeId": 5, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "status": 4, + "ready": true, + "deviceClass": { + "basic": { "key": 4, "label": "Routing Slave" }, + "generic": { "key": 17, "label": "Routing Slave" }, + "specific": { "key": 7, "label": "Routing Slave" }, + "mandatorySupportedCCs": [], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 345, + "productId": 83, + "productType": 3, + "firmwareVersion": "7.2", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 345, + "manufacturer": "Qubino", + "label": "ZMNHOD", + "description": "Flush Shutter DC", + "devices": [{ "productType": "0x0003", "productId": "0x0053" }], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "paramInformation": { "_map": {} } + }, + "label": "ZMNHOD", + "neighbors": [1, 2], + "interviewAttempts": 1, + "endpoints": [ + { "nodeId": 5, "index": 0, "installerIcon": 6656, "userIcon": 6656 } + ], + "commandClasses": [], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 3, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": "unknown" + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { "switchType": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { "switchType": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": "unknown" + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 345 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 83 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.38" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["7.2"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh]", + "unit": "kWh", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyKey": 65537, + "propertyName": "deltaTime", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh] (prev. time delta)", + "unit": "s", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W]", + "unit": "W", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyKey": 66049, + "propertyName": "deltaTime", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W] (prev. time delta)", + "unit": "s", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values" + } + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyKey": 65537, + "propertyName": "previousValue", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh] (prev. value)", + "unit": "kWh", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + } + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyKey": 66049, + "propertyName": "previousValue", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W] (prev. value)", + "unit": "W", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Alarm Type" + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Alarm Level" + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-load status", + "propertyName": "Power Management", + "propertyKeyName": "Over-load status", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Over-load status", + "states": { "0": "idle", "8": "Over-load detected" }, + "ccSpecific": { "notificationType": 8 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Activate/deactivate functions ALL ON / ALL OFF", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 65535, + "default": 255, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "ALL ON is not active, ALL OFF is not active", + "1": "ALL ON is not active ALL OFF active", + "2": "ALL ON is not active ALL OFF is not active", + "255": "ALL ON active, ALL OFF active" + }, + "label": "Activate/deactivate functions ALL ON / ALL OFF", + "isFromConfig": true + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyName": "Power report (Watts) on power change for Q1 or Q2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Power report (Watts) on power change for Q1 or Q2", + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 42, + "propertyName": "Power report (Watts) by time interval for Q1 or Q2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 32767, + "default": 300, + "format": 0, + "allowManualEntry": true, + "label": "Power report (Watts) by time interval for Q1 or Q2", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 71, + "propertyName": "Operating modes", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Shutter mode.", + "1": "Venetian mode (up/down and slate rotation)" + }, + "label": "Operating modes", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 72, + "propertyName": "Slats tilting full turn time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 32767, + "default": 150, + "format": 0, + "allowManualEntry": true, + "label": "Slats tilting full turn time", + "isFromConfig": true + }, + "value": 630 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 73, + "propertyName": "Slats position", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 1, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Return to previous position only with Z-wave", + "1": "Return to previous position with Z-wave or button" + }, + "label": "Slats position", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 74, + "propertyName": "Motor moving up/down time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 32767, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Motor moving up/down time", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 76, + "propertyName": "Motor operation detection", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 6, + "format": 0, + "allowManualEntry": true, + "label": "Motor operation detection", + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 78, + "propertyName": "Forced Shutter DC calibration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { "0": "Default", "1": "Start calibration process." }, + "label": "Forced Shutter DC calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 85, + "propertyName": "Power consumption max delay time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 3, + "max": 50, + "default": 8, + "format": 0, + "allowManualEntry": true, + "label": "Power consumption max delay time", + "isFromConfig": true + }, + "value": 8 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 86, + "propertyName": "Power consumption at limit switch delay time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 3, + "max": 50, + "default": 8, + "format": 0, + "allowManualEntry": true, + "label": "Power consumption at limit switch delay time", + "isFromConfig": true + }, + "value": 8 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 90, + "propertyName": "Time delay for next motor movement", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 30, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Time delay for next motor movement", + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 110, + "propertyName": "Temperature sensor offset settings", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 1, + "max": 32536, + "default": 32536, + "format": 0, + "allowManualEntry": true, + "label": "Temperature sensor offset settings", + "isFromConfig": true + }, + "value": 32536 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 120, + "propertyName": "Digital temperature sensor reporting", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 127, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Digital temperature sensor reporting", + "isFromConfig": true + }, + "value": 5 + } + ] +} From 1e11bfae0514ad110e193d2eb4b83819ec50eebe Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 16 May 2021 08:34:14 +0200 Subject: [PATCH 482/852] Create KNX fan entities directly from config (#50702) --- homeassistant/components/knx/factory.py | 22 ------------- homeassistant/components/knx/fan.py | 42 +++++++++++++++++++------ 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index b62023234e8..c5c95f50150 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -8,7 +8,6 @@ from xknx.devices import ( ClimateMode as XknxClimateMode, Cover as XknxCover, Device as XknxDevice, - Fan as XknxFan, Light as XknxLight, Notification as XknxNotification, Sensor as XknxSensor, @@ -23,7 +22,6 @@ from .schema import ( BinarySensorSchema, ClimateSchema, CoverSchema, - FanSchema, LightSchema, SensorSchema, WeatherSchema, @@ -57,9 +55,6 @@ def create_knx_device( if platform is SupportedPlatforms.WEATHER: return _create_weather(knx_module, config) - if platform is SupportedPlatforms.FAN: - return _create_fan(knx_module, config) - return None @@ -335,20 +330,3 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather: ), group_address_humidity=config.get(WeatherSchema.CONF_KNX_HUMIDITY_ADDRESS), ) - - -def _create_fan(knx_module: XKNX, config: ConfigType) -> XknxFan: - """Return a KNX Fan device to be used within XKNX.""" - - fan = XknxFan( - knx_module, - name=config[CONF_NAME], - group_address_speed=config.get(KNX_ADDRESS), - group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS), - group_address_oscillation=config.get(FanSchema.CONF_OSCILLATION_ADDRESS), - group_address_oscillation_state=config.get( - FanSchema.CONF_OSCILLATION_STATE_ADDRESS - ), - max_step=config.get(FanSchema.CONF_MAX_STEP), - ) - return fan diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 2bc9ab8e654..4b4a84c26d2 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -4,9 +4,11 @@ from __future__ import annotations import math from typing import Any +from xknx import XKNX from xknx.devices import Fan as XknxFan from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -16,8 +18,9 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .const import DOMAIN +from .const import DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity +from .schema import FanSchema DEFAULT_PERCENTAGE = 50 @@ -29,25 +32,44 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up fans for KNX platform.""" + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + entities = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxFan): - entities.append(KNXFan(device)) + for entity_config in platform_config: + entities.append(KNXFan(xknx, entity_config)) + async_add_entities(entities) class KNXFan(KnxEntity, FanEntity): """Representation of a KNX fan.""" - def __init__(self, device: XknxFan) -> None: + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX fan.""" self._device: XknxFan - super().__init__(device) + max_step = config.get(FanSchema.CONF_MAX_STEP) + super().__init__( + device=XknxFan( + xknx, + name=config[CONF_NAME], + group_address_speed=config.get(KNX_ADDRESS), + group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS), + group_address_oscillation=config.get( + FanSchema.CONF_OSCILLATION_ADDRESS + ), + group_address_oscillation_state=config.get( + FanSchema.CONF_OSCILLATION_STATE_ADDRESS + ), + max_step=max_step, + ) + ) self._unique_id = f"{self._device.speed.group_address}" - self._step_range: tuple[int, int] | None = None - if device.max_step: - # FanSpeedMode.STEP: - self._step_range = (1, device.max_step) + # FanSpeedMode.STEP if max_step is set + self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" From 224cc779c433414170425e6aed1a0e5b97c0838c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 16 May 2021 08:40:19 +0200 Subject: [PATCH 483/852] Correct Modbus platform cover restore state (#50421) * Correct cover restore state. * Change mock usage. * Add states to convert. --- homeassistant/components/modbus/cover.py | 16 ++++++- tests/components/modbus/test_modbus_cover.py | 44 ++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index edb81ae7eb3..48dc08a18b9 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -12,6 +12,12 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_track_time_interval @@ -105,7 +111,15 @@ class ModbusCover(CoverEntity, RestoreEntity): """Handle entity which will be added.""" state = await self.async_get_last_state() if state: - self._value = state.state + convert = { + STATE_CLOSED: self._state_closed, + STATE_CLOSING: self._state_closing, + STATE_OPENING: self._state_opening, + STATE_OPEN: self._state_open, + STATE_UNAVAILABLE: None, + STATE_UNKNOWN: None, + } + self._value = convert[state.state] async_track_time_interval(self.hass, self.async_update, self._scan_interval) diff --git a/tests/components/modbus/test_modbus_cover.py b/tests/components/modbus/test_modbus_cover.py index 09d23ebf8bd..f30ee79bd52 100644 --- a/tests/components/modbus/test_modbus_cover.py +++ b/tests/components/modbus/test_modbus_cover.py @@ -8,6 +8,11 @@ from homeassistant.components.modbus.const import ( CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING, CONF_REGISTER, + CONF_STATE_CLOSED, + CONF_STATE_CLOSING, + CONF_STATE_OPEN, + CONF_STATE_OPENING, + CONF_STATUS_REGISTER, CONF_STATUS_REGISTER_TYPE, ) from homeassistant.const import ( @@ -16,11 +21,16 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SLAVE, STATE_CLOSED, + STATE_CLOSING, STATE_OPEN, + STATE_OPENING, ) +from homeassistant.core import State from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from tests.common import mock_restore_cache + @pytest.mark.parametrize( "do_options", @@ -202,3 +212,37 @@ async def test_service_cover_update(hass, mock_pymodbus): "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == STATE_OPEN + + +@pytest.mark.parametrize( + "state", [STATE_CLOSED, STATE_CLOSING, STATE_OPENING, STATE_OPEN] +) +async def test_restore_state_cover(hass, state): + """Run test for cover restore state.""" + + entity_id = "cover.test" + cover_name = "test" + config = { + CONF_NAME: cover_name, + CALL_TYPE_COIL: 1234, + CONF_STATE_OPEN: 1, + CONF_STATE_CLOSED: 0, + CONF_STATE_OPENING: 2, + CONF_STATE_CLOSING: 3, + CONF_STATUS_REGISTER: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + } + mock_restore_cache( + hass, + (State(f"{entity_id}", state),), + ) + await base_config_test( + hass, + config, + cover_name, + COVER_DOMAIN, + CONF_COVERS, + None, + method_discovery=True, + ) + assert hass.states.get(entity_id).state == state From b84cf915f3740734b5bb1d51b59a231aed6cb182 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 16 May 2021 04:11:35 -0500 Subject: [PATCH 484/852] Centralize storage and updating of Sonos favorites (#50581) Co-authored-by: Martin Hjelmare --- homeassistant/components/sonos/__init__.py | 13 ++- homeassistant/components/sonos/const.py | 1 + homeassistant/components/sonos/entity.py | 8 ++ homeassistant/components/sonos/favorites.py | 95 +++++++++++++++++++ .../components/sonos/media_player.py | 4 +- homeassistant/components/sonos/speaker.py | 29 ++---- 6 files changed, 127 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/sonos/favorites.py diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index cdc1169f9f7..c513a73b6e8 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections import OrderedDict import datetime import logging import socket @@ -32,6 +33,7 @@ from .const import ( SONOS_GROUP_UPDATE, SONOS_SEEN, ) +from .favorites import SonosFavorites from .speaker import SonosSpeaker _LOGGER = logging.getLogger(__name__) @@ -65,7 +67,9 @@ class SonosData: def __init__(self) -> None: """Initialize the data.""" - self.discovered: dict[str, SonosSpeaker] = {} + # OrderedDict behavior used by SonosFavorites + self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict() + self.favorites: dict[str, SonosFavorites] = {} self.topology_condition = asyncio.Condition() self.discovery_thread = None self.hosts_heartbeat = None @@ -122,10 +126,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = hass.data[DATA_SONOS] if soco.uid not in data.discovered: - _LOGGER.debug("Adding new speaker") speaker_info = soco.get_speaker_info(True) + _LOGGER.debug("Adding new speaker: %s", speaker_info) speaker = SonosSpeaker(hass, soco, speaker_info) data.discovered[soco.uid] = speaker + if soco.household_id not in data.favorites: + data.favorites[soco.household_id] = SonosFavorites( + hass, soco.household_id + ) + data.favorites[soco.household_id].update() speaker.setup() else: dispatcher_send(hass, f"{SONOS_SEEN}-{soco.uid}", soco) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index e016a473328..9aaecee08af 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -136,6 +136,7 @@ SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_ENTITY_UPDATE = "sonos_entity_update" SONOS_GROUP_UPDATE = "sonos_group_update" +SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated" SONOS_STATE_UPDATED = "sonos_state_updated" SONOS_SEEN = "sonos_seen" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 146725f90e2..8632357d618 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -16,6 +16,7 @@ from .const import ( DOMAIN, SONOS_ENTITY_CREATED, SONOS_ENTITY_UPDATE, + SONOS_HOUSEHOLD_UPDATED, SONOS_STATE_UPDATED, ) from .speaker import SonosSpeaker @@ -48,6 +49,13 @@ class SonosEntity(Entity): self.async_write_ha_state, ) ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SONOS_HOUSEHOLD_UPDATED}-{self.soco.household_id}", + self.async_write_ha_state, + ) + ) async_dispatcher_send( self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain ) diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py new file mode 100644 index 00000000000..19dcb5184e5 --- /dev/null +++ b/homeassistant/components/sonos/favorites.py @@ -0,0 +1,95 @@ +"""Class representing Sonos favorites.""" +from __future__ import annotations + +from collections.abc import Iterator +import datetime +import logging +from typing import Callable + +from pysonos.data_structures import DidlFavorite +from pysonos.events_base import Event as SonosEvent +from pysonos.exceptions import SoCoException + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import DATA_SONOS, SONOS_HOUSEHOLD_UPDATED + +_LOGGER = logging.getLogger(__name__) + + +class SonosFavorites: + """Storage class for Sonos favorites.""" + + def __init__(self, hass: HomeAssistant, household_id: str) -> None: + """Initialize the data.""" + self.hass = hass + self.household_id = household_id + self._favorites: list[DidlFavorite] = [] + self._event_version: str | None = None + self._next_update: Callable | None = None + + def __iter__(self) -> Iterator: + """Return an iterator for the known favorites.""" + favorites = self._favorites.copy() + return iter(favorites) + + @callback + def async_delayed_update(self, event: SonosEvent) -> None: + """Add a delay when triggered by an event. + + Updated favorites are not always immediately available. + + """ + event_id = event.variables["favorites_update_id"] + if not self._event_version: + self._event_version = event_id + return + + if self._event_version == event_id: + _LOGGER.debug("Favorites haven't changed (event_id: %s)", event_id) + return + + self._event_version = event_id + + if self._next_update: + self._next_update() + + self._next_update = self.hass.helpers.event.async_call_later(3, self.update) + + def update(self, now: datetime.datetime | None = None) -> None: + """Request new Sonos favorites from a speaker.""" + new_favorites = None + discovered = self.hass.data[DATA_SONOS].discovered + + for uid, speaker in discovered.items(): + try: + new_favorites = speaker.soco.music_library.get_sonos_favorites() + except SoCoException as err: + _LOGGER.warning( + "Error requesting favorites from %s: %s", speaker.soco, err + ) + else: + # Prefer this SoCo instance next update + discovered.move_to_end(uid, last=False) + break + + if new_favorites is None: + _LOGGER.error("Could not reach any speakers to update favorites") + return + + self._favorites = [] + for fav in new_favorites: + try: + # exclude non-playable favorites with no linked resources + if fav.reference.resources: + self._favorites.append(fav) + except SoCoException as ex: + # Skip unknown types + _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) + _LOGGER.debug( + "Cached %s favorites for household %s", + len(self._favorites), + self.household_id, + ) + dispatcher_send(self.hass, f"{SONOS_HOUSEHOLD_UPDATED}-{self.household_id}") diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 45da26644e3..051a9e29e81 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -439,7 +439,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): elif source == SOURCE_TV: soco.switch_to_tv() else: - fav = [fav for fav in self.coordinator.favorites if fav.title == source] + fav = [fav for fav in self.speaker.favorites if fav.title == source] if len(fav) == 1: src = fav.pop() uri = src.reference.get_uri() @@ -456,7 +456,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): @property # type: ignore[misc] def source_list(self) -> list[str]: """List of available input sources.""" - sources = [fav.title for fav in self.coordinator.favorites] + sources = [fav.title for fav in self.speaker.favorites] model = self.coordinator.model_name.upper() if "PLAY:5" in model or "CONNECT" in model: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 0960c200da7..0c2b28dbdf2 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -12,7 +12,7 @@ import urllib.parse import async_timeout from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo -from pysonos.data_structures import DidlAudioBroadcast, DidlFavorite +from pysonos.data_structures import DidlAudioBroadcast from pysonos.events_base import Event as SonosEvent, SubscriptionBase from pysonos.exceptions import SoCoException from pysonos.music_library import MusicLibrary @@ -49,6 +49,7 @@ from .const import ( SOURCE_LINEIN, SOURCE_TV, ) +from .favorites import SonosFavorites from .helpers import soco_error EVENT_CHARGING = { @@ -127,8 +128,9 @@ class SonosSpeaker: self, hass: HomeAssistant, soco: SoCo, speaker_info: dict[str, Any] ) -> None: """Initialize a SonosSpeaker.""" - self.hass: HomeAssistant = hass - self.soco: SoCo = soco + self.hass = hass + self.soco = soco + self.household_id: str = soco.household_id self.media = SonosMedia(soco) self._is_ready: bool = False @@ -161,8 +163,6 @@ class SonosSpeaker: self.soco_snapshot: Snapshot | None = None self.snapshot_group: list[SonosSpeaker] | None = None - self.favorites: list[DidlFavorite] = [] - def setup(self) -> None: """Run initial setup of the speaker.""" self.set_basic_info() @@ -213,7 +213,6 @@ class SonosSpeaker: """Set basic information when speaker is reconnected.""" self.media.play_mode = self.soco.play_mode self.update_volume() - self.set_favorites() @property def available(self) -> bool: @@ -654,24 +653,16 @@ class SonosSpeaker: for speaker in hass.data[DATA_SONOS].discovered.values(): speaker.soco._zgs_cache.clear() # pylint: disable=protected-access - def set_favorites(self) -> None: - """Set available favorites.""" - self.favorites = [] - for fav in self.soco.music_library.get_sonos_favorites(): - try: - # Exclude non-playable favorites with no linked resources - if fav.reference.resources: - self.favorites.append(fav) - except SoCoException as ex: - # Skip unknown types - _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) + @property + def favorites(self) -> SonosFavorites: + """Return the SonosFavorites instance for this household.""" + return self.hass.data[DATA_SONOS].favorites[self.household_id] @callback def async_update_content(self, event: SonosEvent | None = None) -> None: """Update information about available content.""" if event and "favorites_update_id" in event.variables: - self.hass.async_add_job(self.set_favorites) - self.async_write_entity_states() + self.favorites.async_delayed_update(event) def update_volume(self) -> None: """Update information about current volume settings.""" From c2e2b046d99794525095e3ebac56b6009656da05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Le=20Baillif?= Date: Sun, 16 May 2021 11:12:05 +0200 Subject: [PATCH 485/852] Add new voices for Watson TTS (#48722) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/watson_tts/tts.py | 46 +++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index ad989ec39fc..62ddc917ce9 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -1,4 +1,6 @@ """Support for IBM Watson TTS integration.""" +import logging + from ibm_cloud_sdk_core.authenticators import IAMAuthenticator from ibm_watson import TextToSpeechV1 import voluptuous as vol @@ -6,6 +8,8 @@ import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider import homeassistant.helpers.config_validation as cv +_LOGGER = logging.getLogger(__name__) + CONF_URL = "watson_url" CONF_APIKEY = "watson_apikey" @@ -18,22 +22,32 @@ CONF_TEXT_TYPE = "text" # List from https://tinyurl.com/watson-tts-docs SUPPORTED_VOICES = [ "ar-AR_OmarVoice", + "ar-MS_OmarVoice", + "de-DE_BirgitV2Voice", "de-DE_BirgitV3Voice", "de-DE_BirgitVoice", + "de-DE_DieterV2Voice", "de-DE_DieterV3Voice", "de-DE_DieterVoice", "de-DE_ErikaV3Voice", + "en-AU_CraigVoice", + "en-AU_MadisonVoice", "en-GB_KateV3Voice", "en-GB_KateVoice", "en-GB_CharlotteV3Voice", "en-GB_JamesV3Voice", + "en-GB_KateV3Voice", + "en-GB_KateVoice", + "en-US_AllisonV2Voice", "en-US_AllisonV3Voice", "en-US_AllisonVoice", "en-US_EmilyV3Voice", "en-US_HenryV3Voice", "en-US_KevinV3Voice", + "en-US_LisaV2Voice", "en-US_LisaV3Voice", "en-US_LisaVoice", + "en-US_MichaelV2Voice", "en-US_MichaelV3Voice", "en-US_MichaelVoice", "en-US_OliviaV3Voice", @@ -45,12 +59,17 @@ SUPPORTED_VOICES = [ "es-LA_SofiaVoice", "es-US_SofiaV3Voice", "es-US_SofiaVoice", + "fr-CA_LouiseV3Voice", + "fr-FR_NicolasV3Voice", "fr-FR_ReneeV3Voice", "fr-FR_ReneeVoice", + "it-IT_FrancescaV2Voice", "it-IT_FrancescaV3Voice", "it-IT_FrancescaVoice", "ja-JP_EmiV3Voice", "ja-JP_EmiVoice", + "ko-KR_HyunjunVoice", + "ko-KR_SiWooVoice", "ko-KR_YoungmiVoice", "ko-KR_YunaVoice", "nl-NL_EmmaVoice", @@ -62,6 +81,25 @@ SUPPORTED_VOICES = [ "zh-CN_ZhangJingVoice", ] +DEPRECATED_VOICES = [ + "ar-AR_OmarVoice", + "de-DE_BirgitVoice", + "de-DE_DieterVoice", + "en-GB_KateVoice", + "en-GB_KateV3Voice", + "en-US_AllisonVoice", + "en-US_LisaVoice", + "en-US_MichaelVoice", + "es-ES_EnriqueVoice", + "es-ES_LauraVoice", + "es-LA_SofiaVoice", + "es-US_SofiaVoice", + "fr-FR_ReneeVoice", + "it-IT_FrancescaVoice", + "ja-JP_EmiVoice", + "pt-BR_IsabelaVoice", +] + SUPPORTED_OUTPUT_FORMATS = [ "audio/flac", "audio/mp3", @@ -82,7 +120,7 @@ CONTENT_TYPE_EXTENSIONS = { "audio/wav": "wav", } -DEFAULT_VOICE = "en-US_AllisonVoice" +DEFAULT_VOICE = "en-US_AllisonV3Voice" DEFAULT_OUTPUT_FORMAT = "audio/mp3" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -124,6 +162,12 @@ class WatsonTTSProvider(Provider): self.output_format = output_format self.name = "Watson TTS" + if self.default_voice in DEPRECATED_VOICES: + _LOGGER.warning( + "Watson TTS voice %s is deprecated, it may be removed in the future", + self.default_voice, + ) + @property def supported_languages(self): """Return a list of supported languages.""" From 3200b0150af6c275de229cf22ce3bf9ab9e36be5 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 16 May 2021 12:07:44 +0200 Subject: [PATCH 486/852] Create KNX notify entities directly from config (#50709) --- homeassistant/components/knx/factory.py | 13 ------------- homeassistant/components/knx/notify.py | 21 +++++++++++++++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index c5c95f50150..f89371ee1dc 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -9,7 +9,6 @@ from xknx.devices import ( Cover as XknxCover, Device as XknxDevice, Light as XknxLight, - Notification as XknxNotification, Sensor as XknxSensor, Weather as XknxWeather, ) @@ -46,9 +45,6 @@ def create_knx_device( if platform is SupportedPlatforms.SENSOR: return _create_sensor(knx_module, config) - if platform is SupportedPlatforms.NOTIFY: - return _create_notify(knx_module, config) - if platform is SupportedPlatforms.BINARY_SENSOR: return _create_binary_sensor(knx_module, config) @@ -269,15 +265,6 @@ def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor: ) -def _create_notify(knx_module: XKNX, config: ConfigType) -> XknxNotification: - """Return a KNX notification to be used within XKNX.""" - return XknxNotification( - knx_module, - name=config[CONF_NAME], - group_address=config[KNX_ADDRESS], - ) - - def _create_binary_sensor(knx_module: XKNX, config: ConfigType) -> XknxBinarySensor: """Return a KNX binary sensor to be used within XKNX.""" device_name = config[CONF_NAME] diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 62ba1109526..6f549d9cfac 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -3,13 +3,15 @@ from __future__ import annotations from typing import Any +from xknx import XKNX from xknx.devices import Notification as XknxNotification from homeassistant.components.notify import BaseNotificationService +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import DOMAIN, KNX_ADDRESS async def async_get_service( @@ -18,10 +20,21 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> KNXNotificationService | None: """Get the KNX notification service.""" + if not discovery_info or not discovery_info["platform_config"]: + return None + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + notification_devices = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxNotification): - notification_devices.append(device) + for device_config in platform_config: + notification_devices.append( + XknxNotification( + xknx, + name=device_config[CONF_NAME], + group_address=device_config[KNX_ADDRESS], + ) + ) return ( KNXNotificationService(notification_devices) if notification_devices else None ) From 703456abea820f359f057c6e47cd5197ddebe84d Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Sun, 16 May 2021 15:04:09 +0100 Subject: [PATCH 487/852] Better errors handling in mypy hassfest plugin (#50689) --- script/hassfest/mypy_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 1968d51d7f2..d6f731c803e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -323,7 +323,7 @@ def generate_and_validate(config: Config) -> str: config.add_error("mypy_config", f"Module '{module} doesn't exist") # Don't generate mypy.ini if there're errors found because it will likely crash. - if any(not err.fixable for err in config.errors): + if any(err.plugin == "mypy_config" for err in config.errors): return "" mypy_config = configparser.ConfigParser() @@ -369,7 +369,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: config_path = config.root / "mypy.ini" config.cache["mypy_config"] = content = generate_and_validate(config) - if config.errors: + if any(err.plugin == "mypy_config" for err in config.errors): return with open(str(config_path)) as fp: From 89dd3292ba4b9b1f13bdf769b71a026c0d800819 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 16 May 2021 19:23:37 +0200 Subject: [PATCH 488/852] Initial draft of statistics (#49852) --- .strict-typing | 1 + homeassistant/components/history/__init__.py | 2 +- homeassistant/components/recorder/__init__.py | 68 +++++- homeassistant/components/recorder/history.py | 2 +- homeassistant/components/recorder/models.py | 44 +++- homeassistant/components/recorder/purge.py | 58 ++--- .../components/recorder/statistics.py | 138 +++++++++++ homeassistant/components/recorder/util.py | 47 ++++ homeassistant/components/sensor/manifest.json | 3 +- homeassistant/components/sensor/recorder.py | 81 +++++++ mypy.ini | 11 + tests/components/recorder/test_init.py | 59 ++++- tests/components/recorder/test_purge.py | 4 +- tests/components/recorder/test_statistics.py | 88 +++++++ tests/components/sensor/test_recorder.py | 224 ++++++++++++++++++ 15 files changed, 774 insertions(+), 56 deletions(-) create mode 100644 homeassistant/components/recorder/statistics.py create mode 100644 homeassistant/components/sensor/recorder.py create mode 100644 tests/components/recorder/test_statistics.py create mode 100644 tests/components/sensor/test_recorder.py diff --git a/.strict-typing b/.strict-typing index 7abbfbf7051..28980c17903 100644 --- a/.strict-typing +++ b/.strict-typing @@ -41,6 +41,7 @@ homeassistant.components.persistent_notification.* homeassistant.components.proximity.* homeassistant.components.recorder.purge homeassistant.components.recorder.repack +homeassistant.components.recorder.statistics homeassistant.components.remote.* homeassistant.components.scene.* homeassistant.components.sensor.* diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index ac089bbb3b3..9b009e0fcea 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -55,7 +55,7 @@ CONFIG_SCHEMA = vol.Schema( @deprecated_function("homeassistant.components.recorder.history.get_significant_states") def get_significant_states(hass, *args, **kwargs): - """Wrap _get_significant_states with a sql session.""" + """Wrap _get_significant_states with an sql session.""" return history.get_significant_states(hass, *args, **kwargs) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 4b7709555d0..d7358e96100 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,12 +35,18 @@ from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER, convert_include_exclude_filter, ) -from homeassistant.helpers.event import async_track_time_interval, track_time_change +from homeassistant.helpers.event import ( + async_track_time_change, + async_track_time_interval, +) +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util -from . import history, migration, purge +from . import history, migration, purge, statistics from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX from .models import Base, Events, RecorderRuns, States from .pool import RecorderPool @@ -56,6 +62,7 @@ from .util import ( _LOGGER = logging.getLogger(__name__) SERVICE_PURGE = "purge" +SERVICE_STATISTICS = "statistics" SERVICE_ENABLE = "enable" SERVICE_DISABLE = "disable" @@ -194,6 +201,7 @@ def run_information_with_session(session, point_in_time: datetime | None = None) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" + hass.data[DOMAIN] = {} conf = config[DOMAIN] entity_filter = convert_include_exclude_filter(conf) auto_purge = conf[CONF_AUTO_PURGE] @@ -221,10 +229,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: instance.start() _async_register_services(hass, instance) history.async_setup(hass) + statistics.async_setup(hass) + await async_process_integration_platforms(hass, DOMAIN, _process_recorder_platform) return await instance.async_db_ready +async def _process_recorder_platform(hass, domain, platform): + """Process a recorder platform.""" + hass.data[DOMAIN][domain] = platform + + @callback def _async_register_services(hass, instance): """Register recorder services.""" @@ -263,6 +278,12 @@ class PurgeTask(NamedTuple): apply_filter: bool +class StatisticsTask(NamedTuple): + """An object to insert into the recorder queue to run a statistics task.""" + + start: datetime.datetime + + class WaitTask: """An object to insert into the recorder queue to tell it set the _queue_watch event.""" @@ -389,6 +410,13 @@ class Recorder(threading.Thread): self.queue.put(PurgeTask(keep_days, repack, apply_filter)) + def do_adhoc_statistics(self, **kwargs): + """Trigger an adhoc statistics run.""" + start = kwargs.get("start") + if not start: + start = statistics.get_start_time() + self.queue.put(StatisticsTask(start)) + @callback def async_register(self, shutdown_task, hass_started): """Post connection initialize.""" @@ -451,7 +479,8 @@ class Recorder(threading.Thread): @callback def _async_recorder_ready(self): - """Mark recorder ready.""" + """Finish start and mark recorder ready.""" + self._async_setup_periodic_tasks() self.async_recorder_ready.set() @callback @@ -459,6 +488,24 @@ class Recorder(threading.Thread): """Trigger the purge.""" self.queue.put(PurgeTask(self.keep_days, repack=False, apply_filter=False)) + @callback + def async_hourly_statistics(self, now): + """Trigger the hourly statistics run.""" + start = statistics.get_start_time() + self.queue.put(StatisticsTask(start)) + + def _async_setup_periodic_tasks(self): + """Prepare periodic tasks.""" + if self.auto_purge: + # Purge every night at 4:12am + async_track_time_change( + self.hass, self.async_purge, hour=4, minute=12, second=0 + ) + # Compile hourly statistics every hour at *:12 + async_track_time_change( + self.hass, self.async_hourly_statistics, minute=12, second=0 + ) + def run(self): """Start processing events to save.""" shutdown_task = object() @@ -507,11 +554,6 @@ class Recorder(threading.Thread): self._shutdown() return - # Start periodic purge - if self.auto_purge: - # Purge every night at 4:12am - track_time_change(self.hass, self.async_purge, hour=4, minute=12, second=0) - _LOGGER.debug("Recorder processing the queue") self.hass.add_job(self._async_recorder_ready) self._run_event_loop() @@ -608,11 +650,21 @@ class Recorder(threading.Thread): # Schedule a new purge task if this one didn't finish self.queue.put(PurgeTask(keep_days, repack, apply_filter)) + def _run_statistics(self, start): + """Run statistics task.""" + if statistics.compile_statistics(self, start): + return + # Schedule a new statistics task if this one didn't finish + self.queue.put(StatisticsTask(start)) + def _process_one_event(self, event): """Process one event.""" if isinstance(event, PurgeTask): self._run_purge(event.keep_days, event.repack, event.apply_filter) return + if isinstance(event, StatisticsTask): + self._run_statistics(event.start) + return if isinstance(event, WaitTask): self._queue_watch.set() return diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 63938b6774a..6c89fef2be3 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -52,7 +52,7 @@ QUERY_STATES = [ States.last_updated, ] -HISTORY_BAKERY = "history_bakery" +HISTORY_BAKERY = "recorder_history_bakery" def async_setup(hass): diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 6f414a437c9..af22239713b 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -6,6 +6,7 @@ from sqlalchemy import ( Boolean, Column, DateTime, + Float, ForeignKey, Index, Integer, @@ -38,7 +39,15 @@ TABLE_STATES = "states" TABLE_RECORDER_RUNS = "recorder_runs" TABLE_SCHEMA_CHANGES = "schema_changes" -ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES] +TABLE_STATISTICS = "statistics" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, +] DATETIME_TYPE = DateTime(timezone=True).with_variant( mysql.DATETIME(timezone=True, fsp=6), "mysql" @@ -198,6 +207,39 @@ class States(Base): # type: ignore return None +class Statistics(Base): # type: ignore + """Statistics.""" + + __table_args__ = { + "mysql_default_charset": "utf8mb4", + "mysql_collate": "utf8mb4_unicode_ci", + } + __tablename__ = TABLE_STATISTICS + id = Column(Integer, primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + source = Column(String(32)) + statistic_id = Column(String(255)) + start = Column(DATETIME_TYPE, index=True) + mean = Column(Float()) + min = Column(Float()) + max = Column(Float()) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "statistic_id", "start"), + ) + + @staticmethod + def from_stats(source, statistic_id, start, stats): + """Create object from a statistics.""" + return Statistics( + source=source, + statistic_id=statistic_id, + start=start, + **stats, + ) + + class RecorderRuns(Base): # type: ignore """Representation of recorder run.""" diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 22202ad1bbf..62914c01de7 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -3,10 +3,8 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -import time from typing import TYPE_CHECKING -from sqlalchemy.exc import OperationalError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct @@ -15,20 +13,15 @@ import homeassistant.util.dt as dt_util from .const import MAX_ROWS_TO_PURGE from .models import Events, RecorderRuns, States from .repack import repack_database -from .util import session_scope +from .util import retryable_database_job, session_scope if TYPE_CHECKING: from . import Recorder _LOGGER = logging.getLogger(__name__) -# Retry when one of the following MySQL errors occurred: -RETRYABLE_MYSQL_ERRORS = (1205, 1206, 1213) -# 1205: Lock wait timeout exceeded; try restarting transaction -# 1206: The total number of locks exceeds the lock table size -# 1213: Deadlock found when trying to get lock; try restarting transaction - +@retryable_database_job("purge") def purge_old_data( instance: Recorder, purge_days: int, repack: bool, apply_filter: bool = False ) -> bool: @@ -41,36 +34,25 @@ def purge_old_data( "Purging states and events before target %s", purge_before.isoformat(sep=" ", timespec="seconds"), ) - try: - with session_scope(session=instance.get_session()) as session: # type: ignore - # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record - event_ids = _select_event_ids_to_purge(session, purge_before) - state_ids = _select_state_ids_to_purge(session, purge_before, event_ids) - if state_ids: - _purge_state_ids(session, state_ids) - if event_ids: - _purge_event_ids(session, event_ids) - # If states or events purging isn't processing the purge_before yet, - # return false, as we are not done yet. - _LOGGER.debug("Purging hasn't fully completed yet") - return False - if apply_filter and _purge_filtered_data(instance, session) is False: - _LOGGER.debug("Cleanup filtered data hasn't fully completed yet") - return False - _purge_old_recorder_runs(instance, session, purge_before) - if repack: - repack_database(instance) - except OperationalError as err: - if ( - instance.engine.dialect.name == "mysql" - and err.orig.args[0] in RETRYABLE_MYSQL_ERRORS - ): - _LOGGER.info("%s; purge not completed, retrying", err.orig.args[1]) - time.sleep(instance.db_retry_wait) + + with session_scope(session=instance.get_session()) as session: # type: ignore + # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record + event_ids = _select_event_ids_to_purge(session, purge_before) + state_ids = _select_state_ids_to_purge(session, purge_before, event_ids) + if state_ids: + _purge_state_ids(session, state_ids) + if event_ids: + _purge_event_ids(session, event_ids) + # If states or events purging isn't processing the purge_before yet, + # return false, as we are not done yet. + _LOGGER.debug("Purging hasn't fully completed yet") return False - - _LOGGER.warning("Error purging history: %s", err) - + if apply_filter and _purge_filtered_data(instance, session) is False: + _LOGGER.debug("Cleanup filtered data hasn't fully completed yet") + return False + _purge_old_recorder_runs(instance, session, purge_before) + if repack: + repack_database(instance) return True diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py new file mode 100644 index 00000000000..65bed4423c5 --- /dev/null +++ b/homeassistant/components/recorder/statistics.py @@ -0,0 +1,138 @@ +"""Statistics helper.""" +from __future__ import annotations + +from collections import defaultdict +from datetime import datetime, timedelta +from itertools import groupby +import logging +from typing import TYPE_CHECKING + +from sqlalchemy import bindparam +from sqlalchemy.ext import baked + +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .models import Statistics, process_timestamp_to_utc_isoformat +from .util import execute, retryable_database_job, session_scope + +if TYPE_CHECKING: + from . import Recorder + +QUERY_STATISTICS = [ + Statistics.statistic_id, + Statistics.start, + Statistics.mean, + Statistics.min, + Statistics.max, +] + +STATISTICS_BAKERY = "recorder_statistics_bakery" + +_LOGGER = logging.getLogger(__name__) + + +def async_setup(hass): + """Set up the history hooks.""" + hass.data[STATISTICS_BAKERY] = baked.bakery() + + +def get_start_time() -> datetime.datetime: + """Return start time.""" + last_hour = dt_util.utcnow() - timedelta(hours=1) + start = last_hour.replace(minute=0, second=0, microsecond=0) + return start + + +@retryable_database_job("statistics") +def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: + """Compile statistics.""" + start = dt_util.as_utc(start) + end = start + timedelta(hours=1) + _LOGGER.debug( + "Compiling statistics for %s-%s", + start, + end, + ) + platform_stats = [] + for domain, platform in instance.hass.data[DOMAIN].items(): + if not hasattr(platform, "compile_statistics"): + continue + platform_stats.append(platform.compile_statistics(instance.hass, start, end)) + _LOGGER.debug( + "Statistics for %s during %s-%s: %s", domain, start, end, platform_stats[-1] + ) + + with session_scope(session=instance.get_session()) as session: # type: ignore + for stats in platform_stats: + for entity_id, stat in stats.items(): + session.add(Statistics.from_stats(DOMAIN, entity_id, start, stat)) + + return True + + +def statistics_during_period(hass, start_time, end_time=None, statistic_id=None): + """Return states changes during UTC period start_time - end_time.""" + with session_scope(hass=hass) as session: + baked_query = hass.data[STATISTICS_BAKERY]( + lambda session: session.query(*QUERY_STATISTICS) + ) + + baked_query += lambda q: q.filter(Statistics.start >= bindparam("start_time")) + + if end_time is not None: + baked_query += lambda q: q.filter(Statistics.start < bindparam("end_time")) + + if statistic_id is not None: + baked_query += lambda q: q.filter_by(statistic_id=bindparam("statistic_id")) + statistic_id = statistic_id.lower() + + baked_query += lambda q: q.order_by(Statistics.statistic_id, Statistics.start) + + stats = execute( + baked_query(session).params( + start_time=start_time, end_time=end_time, statistic_id=statistic_id + ) + ) + + statistic_ids = [statistic_id] if statistic_id is not None else None + + return _sorted_statistics_to_dict( + hass, session, stats, start_time, statistic_ids + ) + + +def _sorted_statistics_to_dict( + hass, + session, + stats, + start_time, + statistic_ids, +): + """Convert SQL results into JSON friendly data structure.""" + result = defaultdict(list) + # Set all statistic IDs to empty lists in result set to maintain the order + if statistic_ids is not None: + for stat_id in statistic_ids: + result[stat_id] = [] + + # Called in a tight loop so cache the function + # here + _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat + + # Append all changes to it + for ent_id, group in groupby(stats, lambda state: state.statistic_id): + ent_results = result[ent_id] + ent_results.extend( + { + "statistic_id": db_state.statistic_id, + "start": _process_timestamp_to_utc_isoformat(db_state.start), + "mean": db_state.mean, + "min": db_state.min, + "max": db_state.max, + } + for db_state in group + ) + + # Filter out the empty lists if some states had 0 results. + return {key: val for key, val in result.items() if val} diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 9f99dc2bf45..6231f493cc2 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -4,9 +4,11 @@ from __future__ import annotations from collections.abc import Generator from contextlib import contextmanager from datetime import timedelta +import functools import logging import os import time +from typing import TYPE_CHECKING from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.session import Session @@ -19,10 +21,14 @@ from .models import ( ALL_TABLES, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, RecorderRuns, process_timestamp, ) +if TYPE_CHECKING: + from . import Recorder + _LOGGER = logging.getLogger(__name__) RETRIES = 3 @@ -34,6 +40,12 @@ SQLITE3_POSTFIXES = ["", "-wal", "-shm"] # should do a check on the sqlite3 database. MAX_RESTART_TIME = timedelta(minutes=10) +# Retry when one of the following MySQL errors occurred: +RETRYABLE_MYSQL_ERRORS = (1205, 1206, 1213) +# 1205: Lock wait timeout exceeded; try restarting transaction +# 1206: The total number of locks exceeds the lock table size +# 1213: Deadlock found when trying to get lock; try restarting transaction + @contextmanager def session_scope( @@ -167,6 +179,8 @@ def basic_sanity_check(cursor): """Check tables to make sure select does not fail.""" for table in ALL_TABLES: + if table == TABLE_STATISTICS: + continue if table in (TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES): cursor.execute(f"SELECT * FROM {table};") # nosec # not injection else: @@ -270,3 +284,36 @@ def end_incomplete_runs(session, start_time): "Ended unfinished session (id=%s from %s)", run.run_id, run.start ) session.add(run) + + +def retryable_database_job(description: str): + """Try to execute a database job. + + The job should return True if it finished, and False if it needs to be rescheduled. + """ + + def decorator(job: callable): + @functools.wraps(job) + def wrapper(instance: Recorder, *args, **kwargs): + try: + return job(instance, *args, **kwargs) + except OperationalError as err: + if ( + instance.engine.dialect.name == "mysql" + and err.orig.args[0] in RETRYABLE_MYSQL_ERRORS + ): + _LOGGER.info( + "%s; %s not completed, retrying", err.orig.args[1], description + ) + time.sleep(instance.db_retry_wait) + # Failed with retryable error + return False + + _LOGGER.warning("Error executing %s: %s", description, err) + + # Failed with permanent error + return True + + return wrapper + + return decorator diff --git a/homeassistant/components/sensor/manifest.json b/homeassistant/components/sensor/manifest.json index dc62ae3b031..163bc895975 100644 --- a/homeassistant/components/sensor/manifest.json +++ b/homeassistant/components/sensor/manifest.json @@ -3,5 +3,6 @@ "name": "Sensor", "documentation": "https://www.home-assistant.io/integrations/sensor", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "after_dependencies": ["recorder"] } diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py new file mode 100644 index 00000000000..f79b830a7d7 --- /dev/null +++ b/homeassistant/components/sensor/recorder.py @@ -0,0 +1,81 @@ +"""Statistics helper for sensor.""" +from __future__ import annotations + +import datetime +import statistics + +from homeassistant.components.recorder import history +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.const import ATTR_DEVICE_CLASS +from homeassistant.core import HomeAssistant + +from . import DOMAIN + +DEVICE_CLASS_STATISTICS = {"temperature": {"mean", "min", "max"}} + + +def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]: + """Get (entity_id, device_class) of all sensors for which to compile statistics.""" + all_sensors = hass.states.all(DOMAIN) + entity_ids = [] + + for state in all_sensors: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + state_class = state.attributes.get(ATTR_STATE_CLASS) + if not state_class or state_class != STATE_CLASS_MEASUREMENT: + continue + if not device_class or device_class not in DEVICE_CLASS_STATISTICS: + continue + entity_ids.append((state.entity_id, device_class)) + return entity_ids + + +# Faster than try/except +# From https://stackoverflow.com/a/23639915 +def _is_number(s: str) -> bool: # pylint: disable=invalid-name + """Return True if string is a number.""" + return s.replace(".", "", 1).isdigit() + + +def compile_statistics( + hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime +) -> dict: + """Compile statistics for all entities during start-end. + + Note: This will query the database and must not be run in the event loop + """ + result: dict = {} + + entities = _get_entities(hass) + + # Get history between start and end + history_list = history.get_significant_states( # type: ignore + hass, start, end, [i[0] for i in entities] + ) + + for entity_id, device_class in entities: + wanted_statistics = DEVICE_CLASS_STATISTICS[device_class] + + if entity_id not in history_list: + continue + + entity_history = history_list[entity_id] + fstates = [float(el.state) for el in entity_history if _is_number(el.state)] + + if not fstates: + continue + + result[entity_id] = {} + + # Make calculations + if "max" in wanted_statistics: + result[entity_id]["max"] = max(fstates) + if "min" in wanted_statistics: + result[entity_id]["min"] = min(fstates) + + # Note: The average calculation will be incorrect for unevenly spaced readings, + # this needs to be improved by weighting with time between measurements + if "mean" in wanted_statistics: + result[entity_id]["mean"] = statistics.fmean(fstates) + + return result diff --git a/mypy.ini b/mypy.ini index 1506a06e839..ee53f990d78 100644 --- a/mypy.ini +++ b/mypy.ini @@ -462,6 +462,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.statistics] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.remote.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index a045bb638ce..540764b2ed0 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -590,7 +590,7 @@ def run_tasks_at_time(hass, test_time): def test_auto_purge(hass_recorder): - """Test periodic purge alarm scheduling.""" + """Test periodic purge scheduling.""" hass = hass_recorder() original_tz = dt_util.DEFAULT_TIME_ZONE @@ -598,9 +598,10 @@ def test_auto_purge(hass_recorder): tz = dt_util.get_time_zone("Europe/Copenhagen") dt_util.set_default_time_zone(tz) - # Purging is schedule to happen at 4:12am every day. Exercise this behavior - # by firing alarms and advancing the clock around this time. Pick an arbitrary - # year in the future to avoid boundary conditions relative to the current date. + # Purging is scheduled to happen at 4:12am every day. Exercise this behavior by + # firing time changed events and advancing the clock around this time. Pick an + # arbitrary year in the future to avoid boundary conditions relative to the current + # date. # # The clock is started at 4:15am then advanced forward below now = dt_util.utcnow() @@ -637,6 +638,56 @@ def test_auto_purge(hass_recorder): dt_util.set_default_time_zone(original_tz) +def test_auto_statistics(hass_recorder): + """Test periodic statistics scheduling.""" + hass = hass_recorder() + + original_tz = dt_util.DEFAULT_TIME_ZONE + + tz = dt_util.get_time_zone("Europe/Copenhagen") + dt_util.set_default_time_zone(tz) + + # Statistics is scheduled to happen at *:12am every hour. Exercise this behavior by + # firing time changed events and advancing the clock around this time. Pick an + # arbitrary year in the future to avoid boundary conditions relative to the current + # date. + # + # The clock is started at 4:15am then advanced forward below + now = dt_util.utcnow() + test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) + run_tasks_at_time(hass, test_time) + + with patch( + "homeassistant.components.recorder.statistics.compile_statistics", + return_value=True, + ) as compile_statistics: + # Advance one hour, and the statistics task should run + test_time = test_time + timedelta(hours=1) + run_tasks_at_time(hass, test_time) + assert len(compile_statistics.mock_calls) == 1 + + compile_statistics.reset_mock() + + # Advance one hour, and the statistics task should run again + test_time = test_time + timedelta(hours=1) + run_tasks_at_time(hass, test_time) + assert len(compile_statistics.mock_calls) == 1 + + compile_statistics.reset_mock() + + # Advance less than one full hour. The task should not run. + test_time = test_time + timedelta(minutes=50) + run_tasks_at_time(hass, test_time) + assert len(compile_statistics.mock_calls) == 0 + + # Advance to the next hour, and the statistics task should run again + test_time = test_time + timedelta(hours=1) + run_tasks_at_time(hass, test_time) + assert len(compile_statistics.mock_calls) == 1 + + dt_util.set_default_time_zone(original_tz) + + def test_saving_sets_old_state(hass_recorder): """Test saving sets old state.""" hass = hass_recorder() diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 23164bd73f5..cdbe6e3c338 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -104,7 +104,7 @@ async def test_purge_old_states_encounters_temporary_mysql_error( mysql_exception.orig = MagicMock(args=(1205, "retryable")) with patch( - "homeassistant.components.recorder.purge.time.sleep" + "homeassistant.components.recorder.util.time.sleep" ) as sleep_mock, patch( "homeassistant.components.recorder.purge._purge_old_recorder_runs", side_effect=[mysql_exception, None], @@ -147,7 +147,7 @@ async def test_purge_old_states_encounters_operational_error( await async_wait_recording_done_without_instance(hass) assert "retrying" not in caplog.text - assert "Error purging history" in caplog.text + assert "Error executing purge" in caplog.text async def test_purge_old_events( diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py new file mode 100644 index 00000000000..ee05cb993b9 --- /dev/null +++ b/tests/components/recorder/test_statistics.py @@ -0,0 +1,88 @@ +"""The tests for sensor recorder platform.""" +# pylint: disable=protected-access,invalid-name +from datetime import timedelta +from unittest.mock import patch, sentinel + +from homeassistant.components.recorder import history +from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat +from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + +from tests.components.recorder.common import wait_recording_done + + +def test_compile_hourly_statistics(hass_recorder): + """Test compiling hourly statistics.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": 15.0, + "min": 10.0, + "max": 20.0, + } + ] + } + + +def record_states(hass): + """Record some test states. + + We inject a bunch of state updates temperature sensors. + """ + mp = "media_player.test" + sns1 = "sensor.test1" + sns2 = "sensor.test2" + sns3 = "sensor.test3" + sns1_attr = {"device_class": "temperature", "state_class": "measurement"} + sns2_attr = {"device_class": "temperature"} + sns3_attr = {} + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(minutes=1) + two = one + timedelta(minutes=15) + three = two + timedelta(minutes=30) + four = three + timedelta(minutes=15) + + states = {mp: [], sns1: [], sns2: [], sns3: []} + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "15", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "20", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "20", attributes=sns3_attr)) + + return zero, four, states diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py new file mode 100644 index 00000000000..a391161ee1e --- /dev/null +++ b/tests/components/sensor/test_recorder.py @@ -0,0 +1,224 @@ +"""The tests for sensor recorder platform.""" +# pylint: disable=protected-access,invalid-name +from datetime import timedelta +from unittest.mock import patch, sentinel + +import pytest + +from homeassistant.components.recorder import history +from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat +from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + +from tests.common import get_test_home_assistant, init_recorder_component +from tests.components.recorder.common import wait_recording_done + + +@pytest.fixture +def hass_recorder(): + """Home Assistant fixture with in-memory recorder.""" + hass = get_test_home_assistant() + + def setup_recorder(config=None): + """Set up with params.""" + init_recorder_component(hass, config) + hass.start() + hass.block_till_done() + hass.data[DATA_INSTANCE].block_till_done() + return hass + + yield setup_recorder + hass.stop() + + +def test_compile_hourly_statistics(hass_recorder): + """Test compiling hourly statistics.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": 15.0, + "min": 10.0, + "max": 20.0, + } + ] + } + + +def test_compile_hourly_statistics_unchanged(hass_recorder): + """Test compiling hourly statistics, with no changes during the hour.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=four) + wait_recording_done(hass) + stats = statistics_during_period(hass, four) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(four), + "mean": 20.0, + "min": 20.0, + "max": 20.0, + } + ] + } + + +def test_compile_hourly_statistics_partially_unavailable(hass_recorder): + """Test compiling hourly statistics, with the sensor being partially unavailable.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + zero, four, states = record_states_partially_unavailable(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": 17.5, + "min": 10.0, + "max": 25.0, + } + ] + } + + +def test_compile_hourly_statistics_unavailable(hass_recorder): + """Test compiling hourly statistics, with the sensor being unavailable.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + zero, four, states = record_states_partially_unavailable(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=four) + wait_recording_done(hass) + stats = statistics_during_period(hass, four) + assert stats == {} + + +def record_states(hass): + """Record some test states. + + We inject a bunch of state updates temperature sensors. + """ + mp = "media_player.test" + sns1 = "sensor.test1" + sns2 = "sensor.test2" + sns3 = "sensor.test3" + sns1_attr = {"device_class": "temperature", "state_class": "measurement"} + sns2_attr = {"device_class": "temperature"} + sns3_attr = {} + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(minutes=1) + two = one + timedelta(minutes=15) + three = two + timedelta(minutes=30) + four = three + timedelta(minutes=15) + + states = {mp: [], sns1: [], sns2: [], sns3: []} + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "15", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "20", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "20", attributes=sns3_attr)) + + return zero, four, states + + +def record_states_partially_unavailable(hass): + """Record some test states. + + We inject a bunch of state updates temperature sensors. + """ + mp = "media_player.test" + sns1 = "sensor.test1" + sns2 = "sensor.test2" + sns3 = "sensor.test3" + sns1_attr = {"device_class": "temperature", "state_class": "measurement"} + sns2_attr = {"device_class": "temperature"} + sns3_attr = {} + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(minutes=1) + two = one + timedelta(minutes=15) + three = two + timedelta(minutes=30) + four = three + timedelta(minutes=15) + + states = {mp: [], sns1: [], sns2: [], sns3: []} + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + states[sns1].append(set_state(sns1, "25", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "25", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "25", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + states[sns1].append(set_state(sns1, STATE_UNAVAILABLE, attributes=sns1_attr)) + states[sns2].append(set_state(sns2, STATE_UNAVAILABLE, attributes=sns2_attr)) + states[sns3].append(set_state(sns3, STATE_UNAVAILABLE, attributes=sns3_attr)) + + return zero, four, states From 05c6f3ca1dcb46932095fde6be6447358519a8ad Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 16 May 2021 22:45:28 +0200 Subject: [PATCH 489/852] Create KNX light entities directly from config (#50700) * create light entities directly from config * review changes --- homeassistant/components/knx/factory.py | 109 +------------------- homeassistant/components/knx/light.py | 129 +++++++++++++++++++++--- 2 files changed, 116 insertions(+), 122 deletions(-) diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index f89371ee1dc..7c4b7186075 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -8,7 +8,6 @@ from xknx.devices import ( ClimateMode as XknxClimateMode, Cover as XknxCover, Device as XknxDevice, - Light as XknxLight, Sensor as XknxSensor, Weather as XknxWeather, ) @@ -16,12 +15,11 @@ from xknx.devices import ( from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE from homeassistant.helpers.typing import ConfigType -from .const import KNX_ADDRESS, ColorTempModes, SupportedPlatforms +from .const import SupportedPlatforms from .schema import ( BinarySensorSchema, ClimateSchema, CoverSchema, - LightSchema, SensorSchema, WeatherSchema, ) @@ -33,9 +31,6 @@ def create_knx_device( config: ConfigType, ) -> XknxDevice | None: """Return the requested XKNX device.""" - if platform is SupportedPlatforms.LIGHT: - return _create_light(knx_module, config) - if platform is SupportedPlatforms.COVER: return _create_cover(knx_module, config) @@ -76,108 +71,6 @@ def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: ) -def _create_light_color( - color: str, config: ConfigType -) -> tuple[str | None, str | None, str | None, str | None]: - """Load color configuration from configuration structure.""" - if "individual_colors" in config and color in config["individual_colors"]: - sub_config = config["individual_colors"][color] - group_address_switch = sub_config.get(KNX_ADDRESS) - group_address_switch_state = sub_config.get(LightSchema.CONF_STATE_ADDRESS) - group_address_brightness = sub_config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS) - group_address_brightness_state = sub_config.get( - LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS - ) - return ( - group_address_switch, - group_address_switch_state, - group_address_brightness, - group_address_brightness_state, - ) - return None, None, None, None - - -def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: - """Return a KNX Light device to be used within XKNX.""" - - group_address_tunable_white = None - group_address_tunable_white_state = None - group_address_color_temp = None - group_address_color_temp_state = None - if config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.ABSOLUTE: - group_address_color_temp = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS) - group_address_color_temp_state = config.get( - LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS - ) - elif config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.RELATIVE: - group_address_tunable_white = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS) - group_address_tunable_white_state = config.get( - LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS - ) - - ( - red_switch, - red_switch_state, - red_brightness, - red_brightness_state, - ) = _create_light_color(LightSchema.CONF_RED, config) - ( - green_switch, - green_switch_state, - green_brightness, - green_brightness_state, - ) = _create_light_color(LightSchema.CONF_GREEN, config) - ( - blue_switch, - blue_switch_state, - blue_brightness, - blue_brightness_state, - ) = _create_light_color(LightSchema.CONF_BLUE, config) - ( - white_switch, - white_switch_state, - white_brightness, - white_brightness_state, - ) = _create_light_color(LightSchema.CONF_WHITE, config) - - return XknxLight( - knx_module, - name=config[CONF_NAME], - group_address_switch=config.get(KNX_ADDRESS), - group_address_switch_state=config.get(LightSchema.CONF_STATE_ADDRESS), - group_address_brightness=config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS), - group_address_brightness_state=config.get( - LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS - ), - group_address_color=config.get(LightSchema.CONF_COLOR_ADDRESS), - group_address_color_state=config.get(LightSchema.CONF_COLOR_STATE_ADDRESS), - group_address_rgbw=config.get(LightSchema.CONF_RGBW_ADDRESS), - group_address_rgbw_state=config.get(LightSchema.CONF_RGBW_STATE_ADDRESS), - group_address_tunable_white=group_address_tunable_white, - group_address_tunable_white_state=group_address_tunable_white_state, - group_address_color_temperature=group_address_color_temp, - group_address_color_temperature_state=group_address_color_temp_state, - group_address_switch_red=red_switch, - group_address_switch_red_state=red_switch_state, - group_address_brightness_red=red_brightness, - group_address_brightness_red_state=red_brightness_state, - group_address_switch_green=green_switch, - group_address_switch_green_state=green_switch_state, - group_address_brightness_green=green_brightness, - group_address_brightness_green_state=green_brightness_state, - group_address_switch_blue=blue_switch, - group_address_switch_blue_state=blue_switch_state, - group_address_brightness_blue=blue_brightness, - group_address_brightness_blue_state=blue_brightness_state, - group_address_switch_white=white_switch, - group_address_switch_white_state=white_switch_state, - group_address_brightness_white=white_brightness, - group_address_brightness_white_state=white_brightness_state, - min_kelvin=config[LightSchema.CONF_MIN_KELVIN], - max_kelvin=config[LightSchema.CONF_MAX_KELVIN], - ) - - def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: """Return a KNX Climate device to be used within XKNX.""" climate_mode = XknxClimateMode( diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 46768e97b96..1c20c68b145 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any, cast +from xknx import XKNX from xknx.devices import Light as XknxLight from xknx.telegram.address import parse_device_group_address @@ -18,13 +19,14 @@ from homeassistant.components.light import ( COLOR_MODE_RGBW, LightEntity, ) +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.color as color_util -from .const import DOMAIN, KNX_ADDRESS +from .const import DOMAIN, KNX_ADDRESS, ColorTempModes from .knx_entity import KnxEntity from .schema import LightSchema @@ -36,24 +38,26 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up lights for KNX platform.""" - _async_migrate_unique_id(hass, discovery_info) + if not discovery_info or not discovery_info["platform_config"]: + return + platform_config = discovery_info["platform_config"] + _async_migrate_unique_id(hass, platform_config) + + xknx: XKNX = hass.data[DOMAIN].xknx + entities = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxLight): - entities.append(KNXLight(device)) + for entity_config in platform_config: + entities.append(KNXLight(xknx, entity_config)) + async_add_entities(entities) @callback def _async_migrate_unique_id( - hass: HomeAssistant, discovery_info: DiscoveryInfoType | None + hass: HomeAssistant, platform_config: list[ConfigType] ) -> None: """Change unique_ids used in 2021.4 to exchange individual color switch address for brightness address.""" entity_registry = er.async_get(hass) - if not discovery_info or not discovery_info["platform_config"]: - return - - platform_config = discovery_info["platform_config"] for entity_config in platform_config: individual_colors_config = entity_config.get(LightSchema.CONF_INDIVIDUAL_COLORS) if individual_colors_config is None: @@ -115,16 +119,113 @@ def _async_migrate_unique_id( entity_registry.async_update_entity(entity_id, new_unique_id=new_uid) +def _create_light(xknx: XKNX, config: ConfigType) -> XknxLight: + """Return a KNX Light device to be used within XKNX.""" + + def individual_color_addresses(color: str, feature: str) -> Any | None: + """Load individual color address list from configuration structure.""" + if ( + LightSchema.CONF_INDIVIDUAL_COLORS not in config + or color not in config[LightSchema.CONF_INDIVIDUAL_COLORS] + ): + return None + return config[LightSchema.CONF_INDIVIDUAL_COLORS][color].get(feature) + + group_address_tunable_white = None + group_address_tunable_white_state = None + group_address_color_temp = None + group_address_color_temp_state = None + if config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.ABSOLUTE: + group_address_color_temp = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS) + group_address_color_temp_state = config.get( + LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS + ) + elif config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.RELATIVE: + group_address_tunable_white = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS) + group_address_tunable_white_state = config.get( + LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS + ) + + return XknxLight( + xknx, + name=config[CONF_NAME], + group_address_switch=config.get(KNX_ADDRESS), + group_address_switch_state=config.get(LightSchema.CONF_STATE_ADDRESS), + group_address_brightness=config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS), + group_address_brightness_state=config.get( + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + ), + group_address_color=config.get(LightSchema.CONF_COLOR_ADDRESS), + group_address_color_state=config.get(LightSchema.CONF_COLOR_STATE_ADDRESS), + group_address_rgbw=config.get(LightSchema.CONF_RGBW_ADDRESS), + group_address_rgbw_state=config.get(LightSchema.CONF_RGBW_STATE_ADDRESS), + group_address_tunable_white=group_address_tunable_white, + group_address_tunable_white_state=group_address_tunable_white_state, + group_address_color_temperature=group_address_color_temp, + group_address_color_temperature_state=group_address_color_temp_state, + group_address_switch_red=individual_color_addresses( + LightSchema.CONF_RED, KNX_ADDRESS + ), + group_address_switch_red_state=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_STATE_ADDRESS + ), + group_address_brightness_red=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_ADDRESS + ), + group_address_brightness_red_state=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + ), + group_address_switch_green=individual_color_addresses( + LightSchema.CONF_RED, KNX_ADDRESS + ), + group_address_switch_green_state=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_STATE_ADDRESS + ), + group_address_brightness_green=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_ADDRESS + ), + group_address_brightness_green_state=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + ), + group_address_switch_blue=individual_color_addresses( + LightSchema.CONF_RED, KNX_ADDRESS + ), + group_address_switch_blue_state=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_STATE_ADDRESS + ), + group_address_brightness_blue=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_ADDRESS + ), + group_address_brightness_blue_state=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + ), + group_address_switch_white=individual_color_addresses( + LightSchema.CONF_RED, KNX_ADDRESS + ), + group_address_switch_white_state=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_STATE_ADDRESS + ), + group_address_brightness_white=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_ADDRESS + ), + group_address_brightness_white_state=individual_color_addresses( + LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + ), + min_kelvin=config[LightSchema.CONF_MIN_KELVIN], + max_kelvin=config[LightSchema.CONF_MAX_KELVIN], + ) + + class KNXLight(KnxEntity, LightEntity): """Representation of a KNX light.""" - def __init__(self, device: XknxLight) -> None: + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX light.""" self._device: XknxLight - super().__init__(device) + super().__init__(_create_light(xknx, config)) self._unique_id = self._device_unique_id() - self._min_kelvin = device.min_kelvin or LightSchema.DEFAULT_MIN_KELVIN - self._max_kelvin = device.max_kelvin or LightSchema.DEFAULT_MAX_KELVIN + self._min_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] + self._max_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] self._min_mireds = color_util.color_temperature_kelvin_to_mired( self._max_kelvin ) From 6b38adaa3dd47b46da6e728730baf5c0df87bba2 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Mon, 17 May 2021 04:36:23 +0100 Subject: [PATCH 490/852] Downgrade setuptools to fix CI (#50734) --- .github/workflows/ci.yaml | 4 ++-- homeassistant/helpers/template.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fe14e44beed..a68aaa549c6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -56,7 +56,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U "pip<20.3" setuptools + pip install -U "pip<20.3" "setuptools<56.2" pip install -r requirements.txt -r requirements_test.txt - name: Generate partial pre-commit restore key id: generate-pre-commit-key @@ -580,7 +580,7 @@ jobs: python -m venv venv . venv/bin/activate - pip install -U "pip<20.3" setuptools wheel + pip install -U "pip<20.3" "setuptools<56.2" wheel pip install -r requirements_all.txt pip install -r requirements_test.txt pip install -e . diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 6ac220788e0..40101e17128 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -24,7 +24,7 @@ import weakref import jinja2 from jinja2 import contextfilter, contextfunction from jinja2.sandbox import ImmutableSandboxedEnvironment -from jinja2.utils import Namespace # type: ignore +from jinja2.utils import Namespace import voluptuous as vol from homeassistant.const import ( @@ -581,9 +581,8 @@ class Template: self._strict = strict env = self._env - self._compiled = cast( - jinja2.Template, - jinja2.Template.from_code(env, self._compiled_code, env.globals, None), + self._compiled = jinja2.Template.from_code( + env, self._compiled_code, env.globals, None ) return self._compiled From 22d8f9519e3e985f073dcad9b916de86c4c59e63 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 17 May 2021 05:49:31 +0200 Subject: [PATCH 491/852] Fix configflow strings for step user in fritz (#50742) --- homeassistant/components/fritz/strings.json | 10 ++++++++++ homeassistant/components/fritz/translations/de.json | 4 ++-- homeassistant/components/fritz/translations/en.json | 10 ++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index c32e9c7bac2..407f08b6ddf 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -17,6 +17,16 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "user": { + "title": "Setup FRITZ!Box Tools", + "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "abort": { diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index 8fd24fa43dc..c152132900a 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -29,7 +29,7 @@ "description": "Aktualisiere die Anmeldeinformationen von FRITZ! Box Tools f\u00fcr: {host} . \n\nFRITZ! Box Tools kann sich nicht bei deiner FRITZ! Box anmelden.", "title": "Aktualisieren der FRITZ! Box Tools - Anmeldeinformationen" }, - "start_config": { + "user": { "data": { "host": "Host", "password": "Passwort", @@ -37,7 +37,7 @@ "username": "Benutzername" }, "description": "Einrichten der FRITZ! Box Tools zur Steuerung Ihrer FRITZ! Box.\n Ben\u00f6tigt: Benutzername, Passwort.", - "title": "Setup FRITZ! Box Tools - obligatorisch" + "title": "Setup FRITZ! Box Tools" } } } diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index f712831021a..2c977b47119 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -28,6 +28,16 @@ }, "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", "title": "Updating FRITZ!Box Tools - credentials" + }, + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", + "title": "Setup FRITZ!Box Tools" } } } From db7331847f5d854ff042ce385ac617a0c9844cd6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 17 May 2021 05:49:48 +0200 Subject: [PATCH 492/852] AlexaEqualizerController fix wrong class beeing used (#50724) --- homeassistant/components/alexa/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index c6ae05e9d6f..f31901ce037 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -614,7 +614,7 @@ class MediaPlayerCapabilities(AlexaEntity): yield AlexaChannelController(self.entity) if supported & media_player.const.SUPPORT_SELECT_SOUND_MODE: - inputs = AlexaInputController.get_valid_inputs( + inputs = AlexaEqualizerController.get_valid_inputs( self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST, []) ) if len(inputs) > 0: From 877cb43c06ca74ad86978c534dadaf715daca254 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 17 May 2021 05:17:18 +0000 Subject: [PATCH 493/852] [ci skip] Translation update --- .../components/apple_tv/translations/nl.json | 2 +- .../components/apple_tv/translations/pl.json | 2 +- .../components/arcam_fmj/translations/nl.json | 2 +- .../components/arcam_fmj/translations/pl.json | 2 +- .../azure_devops/translations/nl.json | 2 +- .../azure_devops/translations/pl.json | 2 +- .../components/blebox/translations/nl.json | 2 +- .../components/blebox/translations/pl.json | 2 +- .../components/bond/translations/nl.json | 2 +- .../components/bond/translations/pl.json | 2 +- .../components/bosch_shc/translations/en.json | 41 +++++++++---------- .../components/broadlink/translations/de.json | 3 ++ .../components/brother/translations/nl.json | 2 +- .../components/brother/translations/pl.json | 2 +- .../components/bsblan/translations/nl.json | 2 +- .../components/bsblan/translations/pl.json | 2 +- .../buienradar/translations/pl.json | 29 +++++++++++++ .../components/canary/translations/nl.json | 2 +- .../components/canary/translations/pl.json | 2 +- .../components/cast/translations/de.json | 7 ++-- .../components/cast/translations/en.json | 2 +- .../components/cast/translations/es.json | 17 ++++++++ .../components/cast/translations/nl.json | 6 +-- .../components/cast/translations/pl.json | 21 +++++++++- .../cloudflare/translations/nl.json | 2 +- .../cloudflare/translations/pl.json | 2 +- .../components/deconz/translations/nl.json | 2 +- .../components/deconz/translations/pl.json | 2 +- .../components/denonavr/translations/nl.json | 2 +- .../components/denonavr/translations/pl.json | 2 +- .../components/directv/translations/nl.json | 2 +- .../components/directv/translations/pl.json | 2 +- .../components/doorbird/translations/nl.json | 2 +- .../components/doorbird/translations/pl.json | 2 +- .../components/elgato/translations/de.json | 2 +- .../components/elgato/translations/nl.json | 2 +- .../components/elgato/translations/pl.json | 8 ++-- .../components/emonitor/translations/nl.json | 2 +- .../components/emonitor/translations/pl.json | 2 +- .../enphase_envoy/translations/nl.json | 2 +- .../enphase_envoy/translations/pl.json | 2 +- .../components/epson/translations/es.json | 3 +- .../components/epson/translations/pl.json | 3 +- .../components/esphome/translations/nl.json | 2 +- .../components/esphome/translations/pl.json | 2 +- .../components/ezviz/translations/nl.json | 2 +- .../components/flume/translations/pl.json | 10 ++++- .../forked_daapd/translations/nl.json | 2 +- .../forked_daapd/translations/pl.json | 2 +- .../components/fritz/translations/de.json | 4 +- .../components/fritz/translations/en.json | 11 +++++ .../components/fritz/translations/nl.json | 2 +- .../components/fritz/translations/pl.json | 2 +- .../components/fritzbox/translations/nl.json | 2 +- .../components/fritzbox/translations/pl.json | 2 +- .../fritzbox_callmonitor/translations/nl.json | 2 +- .../fritzbox_callmonitor/translations/pl.json | 2 +- .../components/goalzero/translations/en.json | 10 ++++- .../components/gogogate2/translations/ca.json | 3 +- .../components/gogogate2/translations/es.json | 1 + .../components/gogogate2/translations/et.json | 1 + .../components/gogogate2/translations/nl.json | 1 + .../components/gogogate2/translations/no.json | 3 +- .../components/gogogate2/translations/pl.json | 3 +- .../components/gogogate2/translations/ru.json | 3 +- .../gogogate2/translations/zh-Hant.json | 3 +- .../growatt_server/translations/en.json | 2 +- .../growatt_server/translations/es.json | 27 ++++++++++++ .../growatt_server/translations/pl.json | 27 ++++++++++++ .../components/guardian/translations/es.json | 3 ++ .../components/guardian/translations/pl.json | 3 ++ .../components/harmony/translations/nl.json | 2 +- .../components/harmony/translations/pl.json | 2 +- .../components/homekit/translations/nl.json | 2 +- .../homekit_controller/translations/nl.json | 4 +- .../homekit_controller/translations/pl.json | 2 +- .../huawei_lte/translations/nl.json | 2 +- .../huawei_lte/translations/pl.json | 2 +- .../components/ipp/translations/nl.json | 2 +- .../components/ipp/translations/pl.json | 2 +- .../components/isy994/translations/nl.json | 2 +- .../components/isy994/translations/pl.json | 2 +- .../components/kodi/translations/nl.json | 2 +- .../components/kodi/translations/pl.json | 2 +- .../components/kraken/translations/en.json | 22 ++++++++++ .../components/light/translations/es.json | 2 +- .../lutron_caseta/translations/nl.json | 2 +- .../lutron_caseta/translations/pl.json | 2 +- .../components/mqtt/translations/de.json | 2 +- .../components/mqtt/translations/nl.json | 2 +- .../components/mqtt/translations/pl.json | 6 ++- .../components/myq/translations/pl.json | 10 ++++- .../components/nam/translations/es.json | 24 +++++++++++ .../components/nexia/translations/en.json | 3 +- .../components/nuki/translations/nl.json | 2 +- .../components/nzbget/translations/nl.json | 2 +- .../components/nzbget/translations/pl.json | 2 +- .../ovo_energy/translations/nl.json | 2 +- .../ovo_energy/translations/pl.json | 2 +- .../components/plaato/translations/nl.json | 2 +- .../components/plugwise/translations/nl.json | 2 +- .../components/plugwise/translations/pl.json | 2 +- .../components/powerwall/translations/nl.json | 2 +- .../components/powerwall/translations/pl.json | 2 +- .../rainmachine/translations/es.json | 1 + .../rainmachine/translations/nl.json | 2 +- .../rainmachine/translations/pl.json | 1 + .../components/roku/translations/nl.json | 2 +- .../components/roku/translations/pl.json | 2 +- .../components/roomba/translations/nl.json | 2 +- .../components/roomba/translations/pl.json | 2 +- .../components/samsungtv/translations/nl.json | 2 +- .../components/samsungtv/translations/pl.json | 2 +- .../screenlogic/translations/nl.json | 2 +- .../screenlogic/translations/pl.json | 2 +- .../components/smappee/translations/nl.json | 2 +- .../components/smappee/translations/pl.json | 2 +- .../somfy_mylink/translations/nl.json | 2 +- .../somfy_mylink/translations/pl.json | 2 +- .../components/sonarr/translations/nl.json | 2 +- .../components/sonarr/translations/pl.json | 2 +- .../components/songpal/translations/nl.json | 2 +- .../components/songpal/translations/pl.json | 2 +- .../squeezebox/translations/nl.json | 2 +- .../squeezebox/translations/pl.json | 2 +- .../components/syncthing/translations/de.json | 1 + .../components/syncthing/translations/es.json | 22 ++++++++++ .../components/syncthing/translations/pl.json | 19 ++++++++- .../components/syncthru/translations/nl.json | 2 +- .../components/syncthru/translations/pl.json | 2 +- .../synology_dsm/translations/nl.json | 2 +- .../synology_dsm/translations/pl.json | 2 +- .../system_bridge/translations/es.json | 4 +- .../system_bridge/translations/nl.json | 2 +- .../system_bridge/translations/pl.json | 19 +++++++-- .../components/tado/translations/de.json | 2 +- .../components/tuya/translations/nl.json | 2 +- .../components/unifi/translations/nl.json | 2 +- .../components/unifi/translations/pl.json | 2 +- .../components/upnp/translations/nl.json | 2 +- .../components/upnp/translations/pl.json | 2 +- .../components/vizio/translations/de.json | 10 ++--- .../components/vizio/translations/nl.json | 2 +- .../components/wilight/translations/nl.json | 2 +- .../components/wilight/translations/pl.json | 2 +- .../components/withings/translations/de.json | 2 +- .../components/withings/translations/nl.json | 2 +- .../components/withings/translations/pl.json | 2 +- .../components/wled/translations/nl.json | 2 +- .../components/wled/translations/pl.json | 2 +- .../xiaomi_aqara/translations/nl.json | 2 +- .../xiaomi_aqara/translations/pl.json | 2 +- .../xiaomi_miio/translations/nl.json | 2 +- .../xiaomi_miio/translations/pl.json | 2 +- .../components/yeelight/translations/ca.json | 4 ++ .../components/yeelight/translations/de.json | 3 ++ .../components/yeelight/translations/es.json | 4 ++ .../components/yeelight/translations/et.json | 4 ++ .../components/yeelight/translations/nl.json | 4 ++ .../components/yeelight/translations/no.json | 4 ++ .../components/yeelight/translations/pl.json | 4 ++ .../components/yeelight/translations/ru.json | 4 ++ .../yeelight/translations/zh-Hant.json | 4 ++ .../components/zha/translations/nl.json | 2 +- .../components/zha/translations/pl.json | 15 ++++++- 165 files changed, 498 insertions(+), 179 deletions(-) create mode 100644 homeassistant/components/buienradar/translations/pl.json create mode 100644 homeassistant/components/growatt_server/translations/es.json create mode 100644 homeassistant/components/growatt_server/translations/pl.json create mode 100644 homeassistant/components/kraken/translations/en.json create mode 100644 homeassistant/components/nam/translations/es.json create mode 100644 homeassistant/components/syncthing/translations/es.json diff --git a/homeassistant/components/apple_tv/translations/nl.json b/homeassistant/components/apple_tv/translations/nl.json index e313e972188..cc04522334d 100644 --- a/homeassistant/components/apple_tv/translations/nl.json +++ b/homeassistant/components/apple_tv/translations/nl.json @@ -16,7 +16,7 @@ "no_usable_service": "Er is een apparaat gevonden, maar er kon geen manier worden gevonden om er verbinding mee te maken. Als u dit bericht blijft zien, probeert u het IP-adres in te voeren of uw Apple TV opnieuw op te starten.", "unknown": "Onverwachte fout" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "U staat op het punt om de Apple TV met de naam `{name}` toe te voegen aan Home Assistant.\n\n**Om het proces te voltooien, moet u mogelijk meerdere PIN-codes invoeren.**\n\nLet op: u kunt uw Apple TV *niet* uitschakelen met deze integratie. Alleen de mediaspeler in Home Assistant wordt uitgeschakeld!", diff --git a/homeassistant/components/apple_tv/translations/pl.json b/homeassistant/components/apple_tv/translations/pl.json index e8950d1c714..48de231527e 100644 --- a/homeassistant/components/apple_tv/translations/pl.json +++ b/homeassistant/components/apple_tv/translations/pl.json @@ -16,7 +16,7 @@ "no_usable_service": "Znaleziono urz\u0105dzenie, ale nie uda\u0142o si\u0119 zidentyfikowa\u0107 \u017cadnego sposobu na nawi\u0105zanie z nim po\u0142\u0105czenia. Je\u015bli nadal widzisz t\u0119 wiadomo\u015b\u0107, spr\u00f3buj poda\u0107 jego adres IP lub uruchom ponownie Apple TV.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Zamierzasz doda\u0107 Apple TV o nazwie \"{name}\" do Home Assistanta. \n\n **Aby uko\u0144czy\u0107 ca\u0142y proces, mo\u017ce by\u0107 konieczne wprowadzenie wielu kod\u00f3w PIN.** \n\nPami\u0119taj, \u017ce \"NIE\" b\u0119dziesz w stanie wy\u0142\u0105czy\u0107 Apple TV dzi\u0119ki tej integracji. Wy\u0142\u0105cza si\u0119 tylko sam odtwarzacz multimedialny w Home Assistant!", diff --git a/homeassistant/components/arcam_fmj/translations/nl.json b/homeassistant/components/arcam_fmj/translations/nl.json index 03465d5c53d..45a7be867b9 100644 --- a/homeassistant/components/arcam_fmj/translations/nl.json +++ b/homeassistant/components/arcam_fmj/translations/nl.json @@ -9,7 +9,7 @@ "one": "Leeg", "other": "Leeg" }, - "flow_title": "Arcam FMJ op {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "Wil je Arcam FMJ op `{host}` toevoegen aan Home Assistant?" diff --git a/homeassistant/components/arcam_fmj/translations/pl.json b/homeassistant/components/arcam_fmj/translations/pl.json index af28552d892..1373aae5cf2 100644 --- a/homeassistant/components/arcam_fmj/translations/pl.json +++ b/homeassistant/components/arcam_fmj/translations/pl.json @@ -11,7 +11,7 @@ "one": "jeden", "other": "inne" }, - "flow_title": "Arcam FMJ na {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "Czy chcesz doda\u0107 Arcam FMJ na \"{host}\" do Home Assistanta?" diff --git a/homeassistant/components/azure_devops/translations/nl.json b/homeassistant/components/azure_devops/translations/nl.json index 971af5b8d58..a57dd85c495 100644 --- a/homeassistant/components/azure_devops/translations/nl.json +++ b/homeassistant/components/azure_devops/translations/nl.json @@ -9,7 +9,7 @@ "invalid_auth": "Ongeldige authenticatie", "project_error": "Kon geen projectinformatie ophalen." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/azure_devops/translations/pl.json b/homeassistant/components/azure_devops/translations/pl.json index 18c4080a696..e285af979bd 100644 --- a/homeassistant/components/azure_devops/translations/pl.json +++ b/homeassistant/components/azure_devops/translations/pl.json @@ -9,7 +9,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie", "project_error": "Nie mo\u017cna uzyska\u0107 informacji o projekcie" }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/blebox/translations/nl.json b/homeassistant/components/blebox/translations/nl.json index 0fad3e9264f..65a775e6f72 100644 --- a/homeassistant/components/blebox/translations/nl.json +++ b/homeassistant/components/blebox/translations/nl.json @@ -9,7 +9,7 @@ "unknown": "Onverwachte fout", "unsupported_version": "BleBox-apparaat heeft verouderde firmware. Upgrade het eerst." }, - "flow_title": "BleBox-apparaat: {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/blebox/translations/pl.json b/homeassistant/components/blebox/translations/pl.json index 9f61e308f64..21540182505 100644 --- a/homeassistant/components/blebox/translations/pl.json +++ b/homeassistant/components/blebox/translations/pl.json @@ -9,7 +9,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d", "unsupported_version": "Urz\u0105dzenie BleBox ma nieaktualny firmware. Prosz\u0119 go najpierw zaktualizowa\u0107." }, - "flow_title": "Urz\u0105dzenie BleBox: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/bond/translations/nl.json b/homeassistant/components/bond/translations/nl.json index 67812678082..fcf519d681d 100644 --- a/homeassistant/components/bond/translations/nl.json +++ b/homeassistant/components/bond/translations/nl.json @@ -9,7 +9,7 @@ "old_firmware": "Niet-ondersteunde oude firmware op het Bond-apparaat - voer een upgrade uit voordat u doorgaat", "unknown": "Onverwachte fout" }, - "flow_title": "Bond: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bond/translations/pl.json b/homeassistant/components/bond/translations/pl.json index 6f5f2d276ff..a4026879703 100644 --- a/homeassistant/components/bond/translations/pl.json +++ b/homeassistant/components/bond/translations/pl.json @@ -9,7 +9,7 @@ "old_firmware": "Stare, nieobs\u0142ugiwane oprogramowanie na urz\u0105dzeniu Bond - zaktualizuj przed kontynuowaniem", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Bond: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/en.json b/homeassistant/components/bosch_shc/translations/en.json index fcac72b418e..65f675e2f4d 100644 --- a/homeassistant/components/bosch_shc/translations/en.json +++ b/homeassistant/components/bosch_shc/translations/en.json @@ -1,6 +1,16 @@ { - "title": "Bosch SHC", "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "pairing_failed": "Pairing failed; please check the Bosch Smart Home Controller is in pairing mode (LED flashing) as well as your password is correct.", + "session_error": "Session error: API return Non-OK result.", + "unknown": "Unexpected error" + }, "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { @@ -11,31 +21,18 @@ "password": "Password of the Smart Home Controller" } }, - "user": { - "description": "Set up your Bosch Smart Home Controller to allow monitoring and control with Home Assistant.", - "title": "SHC authentication parameters", - "data": { - "host": "Host" - } - }, "reauth_confirm": { - "title": "SHC authentication parameters", "description": "The bosch_shc integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + }, + "user": { "data": { "host": "Host" - } + }, + "description": "Set up your Bosch Smart Home Controller to allow monitoring and control with Home Assistant.", + "title": "SHC authentication parameters" } - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "pairing_failed": "Pairing failed; please check the Bosch Smart Home Controller is in pairing mode (LED flashing) as well as your password is correct.", - "session_error": "Session error: API return Non-OK result.", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "Device is already configured", - "reauth_successful": "Re-authentication was successful" } - } + }, + "title": "Bosch SHC" } \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index 7ad3ab95ec9..d1ab0987a3a 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -23,6 +23,9 @@ }, "title": "W\u00e4hle einen Namen f\u00fcr das Ger\u00e4t" }, + "reset": { + "description": "{Name} ({Modell} unter {Host}) ist gesperrt. Du musst das Ger\u00e4t entsperren, um dich zu authentifizieren und die Konfiguration abzuschlie\u00dfen. Anweisungen:\n1. \u00d6ffne die Broadlink-App.\n2. Klicke auf auf das Ger\u00e4t.\n3. Klicke oben rechts auf `...`.\n4. Scrolle zum unteren Ende der Seite.\n5. Deaktiviere die Sperre." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/brother/translations/nl.json b/homeassistant/components/brother/translations/nl.json index 531038d827b..6440be77e75 100644 --- a/homeassistant/components/brother/translations/nl.json +++ b/homeassistant/components/brother/translations/nl.json @@ -9,7 +9,7 @@ "snmp_error": "SNMP-server uitgeschakeld of printer wordt niet ondersteund.", "wrong_host": "Ongeldige hostnaam of IP-adres." }, - "flow_title": "Brother Printer: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/brother/translations/pl.json b/homeassistant/components/brother/translations/pl.json index c4c1c3d7d7a..20031bf94ad 100644 --- a/homeassistant/components/brother/translations/pl.json +++ b/homeassistant/components/brother/translations/pl.json @@ -9,7 +9,7 @@ "snmp_error": "Serwer SNMP wy\u0142\u0105czony lub drukarka nie jest obs\u0142ugiwana", "wrong_host": "Niepoprawna nazwa hosta lub adres IP drukarki" }, - "flow_title": "Drukarka Brother: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/nl.json b/homeassistant/components/bsblan/translations/nl.json index 415cd759a8a..e07f6bc25f0 100644 --- a/homeassistant/components/bsblan/translations/nl.json +++ b/homeassistant/components/bsblan/translations/nl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/pl.json b/homeassistant/components/bsblan/translations/pl.json index 5cf79db3fba..3667c2432bf 100644 --- a/homeassistant/components/bsblan/translations/pl.json +++ b/homeassistant/components/bsblan/translations/pl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/buienradar/translations/pl.json b/homeassistant/components/buienradar/translations/pl.json new file mode 100644 index 00000000000..3cf2dc85271 --- /dev/null +++ b/homeassistant/components/buienradar/translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "error": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "step": { + "user": { + "data": { + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Kod kraju do wy\u015bwietlania obraz\u00f3w z kamer.", + "delta": "Odst\u0119p czasu w sekundach mi\u0119dzy aktualizacjami obrazu z kamery", + "timeframe": "Czas w minutach poprzedzaj\u0105cy prognoz\u0119 opad\u00f3w" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/nl.json b/homeassistant/components/canary/translations/nl.json index fbe642bbc96..ed64af346ea 100644 --- a/homeassistant/components/canary/translations/nl.json +++ b/homeassistant/components/canary/translations/nl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Kon niet verbinden" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/canary/translations/pl.json b/homeassistant/components/canary/translations/pl.json index 1da4db78731..9090b184414 100644 --- a/homeassistant/components/canary/translations/pl.json +++ b/homeassistant/components/canary/translations/pl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index 2093306a508..1358ab16210 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -9,10 +9,10 @@ "step": { "config": { "data": { - "known_hosts": "Optionale Liste bekannter Hosts, wenn die mDNS-Erkennung nicht funktioniert." + "known_hosts": "Bekannte Hosts" }, - "description": "Bitte die Google Cast-Konfiguration eingeben.", - "title": "Google Cast" + "description": "Bekannte Hosts - Eine durch Kommas getrennte Liste von Hostnamen oder IP-Adressen von Cast-Ger\u00e4ten, die verwendet wird, wenn die mDNS-Erkennung nicht funktioniert.", + "title": "Google Cast-Konfiguration" }, "confirm": { "description": "M\u00f6chtest du Google Cast einrichten?" @@ -29,6 +29,7 @@ "ignore_cec": "CEC ignorieren", "uuid": "Zul\u00e4ssige UUIDs" }, + "description": "Erlaubte UUIDs - Eine kommagetrennte Liste von UUIDs von Cast-Ger\u00e4ten, die dem Home Assistant hinzugef\u00fcgt werden sollen. Nur verwenden, wenn Sie nicht alle verf\u00fcgbaren Cast-Ger\u00e4te hinzuf\u00fcgen m\u00f6chten.\nIgnore CEC - Eine kommagetrennte Liste von Chromecasts, die CEC-Daten zur Bestimmung des aktiven Eingangs ignorieren sollen. Dies wird an pychromecast.IGNORE_CEC \u00fcbergeben.", "title": "Erweiterte Google Cast-Konfiguration" }, "basic_options": { diff --git a/homeassistant/components/cast/translations/en.json b/homeassistant/components/cast/translations/en.json index 32d22a147b3..ac473a5efe5 100644 --- a/homeassistant/components/cast/translations/en.json +++ b/homeassistant/components/cast/translations/en.json @@ -29,7 +29,7 @@ "ignore_cec": "Ignore CEC", "uuid": "Allowed UUIDs" }, - "description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don\u2019t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be will be passed to pychromecast.IGNORE_CEC.", + "description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don\u2019t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be passed to pychromecast.IGNORE_CEC.", "title": "Advanced Google Cast configuration" }, "basic_options": { diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index 358bb5af6eb..dad23682dac 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -22,6 +22,23 @@ "options": { "error": { "invalid_known_hosts": "Los hosts conocidos deben ser una lista de hosts separados por comas." + }, + "step": { + "advanced_options": { + "data": { + "ignore_cec": "Ignorar CEC", + "uuid": "UUID permitidos" + }, + "description": "UUID permitidos: lista separada por comas de UUID de dispositivos Cast para a\u00f1adir a Home Assistant. \u00dasalo solo si no deseas a\u00f1adir todos los dispositivos Cast disponibles.\nIgnorar CEC: lista separada por comas de Chromecasts que deben ignorar los datos CEC para determinar la entrada activa. Esto se pasar\u00e1 a pychromecast.IGNORE_CEC.", + "title": "Configuraci\u00f3n avanzada de Google Cast" + }, + "basic_options": { + "data": { + "known_hosts": "Hosts conocidos" + }, + "description": "Hosts conocidos - Una lista separada por comas de nombres de host o direcciones IP de dispositivos cast, utilizar si el descubrimiento mDNS no est\u00e1 funcionando.", + "title": "Configuraci\u00f3n de Google Cast" + } } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/nl.json b/homeassistant/components/cast/translations/nl.json index 3928b227e5d..fec645993b9 100644 --- a/homeassistant/components/cast/translations/nl.json +++ b/homeassistant/components/cast/translations/nl.json @@ -9,10 +9,10 @@ "step": { "config": { "data": { - "known_hosts": "Optionele lijst van bekende hosts indien mDNS discovery niet werkt." + "known_hosts": "Bekende hosts" }, - "description": "Voer de Google Cast configuratie in.", - "title": "Google Cast" + "description": "Bekende hosts - Een door komma's gescheiden lijst van hostnamen of IP-adressen van cast-apparaten, te gebruiken als mDNS-ontdekking niet werkt.", + "title": "Google Cast configuratie" }, "confirm": { "description": "Wil je beginnen met instellen?" diff --git a/homeassistant/components/cast/translations/pl.json b/homeassistant/components/cast/translations/pl.json index 0fb732d704b..4b8459a5319 100644 --- a/homeassistant/components/cast/translations/pl.json +++ b/homeassistant/components/cast/translations/pl.json @@ -9,9 +9,9 @@ "step": { "config": { "data": { - "known_hosts": "Opcjonalna lista znanych host\u00f3w, je\u015bli wykrywanie mDNS nie dzia\u0142a." + "known_hosts": "Znane hosta." }, - "description": "Wprowad\u017a konfiguracj\u0119 Google Cast.", + "description": "Znane hosta - lista rozdzielonych przecinkami nazw host\u00f3w lub adres\u00f3w IP urz\u0105dze\u0144 przesy\u0142aj\u0105cych. U\u017cyj, je\u015bli wykrywanie mDNS nie dzia\u0142a.", "title": "Google Cast" }, "confirm": { @@ -22,6 +22,23 @@ "options": { "error": { "invalid_known_hosts": "Znane hosty musz\u0105 by\u0107 list\u0105 host\u00f3w oddzielonych przecinkami." + }, + "step": { + "advanced_options": { + "data": { + "ignore_cec": "Ignoruj CEC", + "uuid": "Dozwolone identyfikatory UUID" + }, + "description": "Dozwolone identyfikatory UUID - lista oddzielonych przecinkami identyfikator\u00f3w UUID urz\u0105dze\u0144 przesy\u0142aj\u0105cych, kt\u00f3re mo\u017cna doda\u0107 do Home Assistanta. U\u017cywaj tylko wtedy, gdy nie chcesz dodawa\u0107 wszystkich dost\u0119pnych urz\u0105dze\u0144 przesy\u0142aj\u0105cych.\nIgnoruj CEC - lista Chromecast\u00f3w oddzielonych przecinkami, kt\u00f3re powinny ignorowa\u0107 dane CEC przy okre\u015blaniu aktywnego wej\u015bcia. Zostanie to przekazane do pychromecast.IGNORE_CEC.", + "title": "Zaawansowana konfiguracja Google Cast" + }, + "basic_options": { + "data": { + "known_hosts": "Znane hosta" + }, + "description": "Znane hosta - lista rozdzielonych przecinkami nazw host\u00f3w lub adres\u00f3w IP urz\u0105dze\u0144 przesy\u0142aj\u0105cych. U\u017cyj, je\u015bli wykrywanie mDNS nie dzia\u0142a.", + "title": "Konfiguracja Google Cast" + } } } } \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/nl.json b/homeassistant/components/cloudflare/translations/nl.json index 94697419ff1..7095ff49983 100644 --- a/homeassistant/components/cloudflare/translations/nl.json +++ b/homeassistant/components/cloudflare/translations/nl.json @@ -9,7 +9,7 @@ "invalid_auth": "Ongeldige authenticatie", "invalid_zone": "Ongeldige zone" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/cloudflare/translations/pl.json b/homeassistant/components/cloudflare/translations/pl.json index 70c7869937a..7675c7df8a3 100644 --- a/homeassistant/components/cloudflare/translations/pl.json +++ b/homeassistant/components/cloudflare/translations/pl.json @@ -9,7 +9,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_zone": "Nieprawid\u0142owa strefa" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index 0d0a745bc1b..29031a7d731 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -11,7 +11,7 @@ "error": { "no_key": "Kon geen API-sleutel ophalen" }, - "flow_title": "deCONZ Zigbee gateway ( {host} )", + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "Wilt u Home Assistant configureren om verbinding te maken met de deCONZ gateway van de Home Assistant add-on {addon}?", diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index d2352bdb973..72d62aff959 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -11,7 +11,7 @@ "error": { "no_key": "Nie mo\u017cna uzyska\u0107 klucza API" }, - "flow_title": "Bramka deCONZ Zigbee ({host})", + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek {addon}?", diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json index fc4d17fe104..47da10106f3 100644 --- a/homeassistant/components/denonavr/translations/nl.json +++ b/homeassistant/components/denonavr/translations/nl.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Kan geen Denon AVR netwerkontvanger vinden" }, - "flow_title": "Denon AVR Network Receiver: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Bevestig het toevoegen van de ontvanger", diff --git a/homeassistant/components/denonavr/translations/pl.json b/homeassistant/components/denonavr/translations/pl.json index c874cc6fb7e..6b09baf7d4c 100644 --- a/homeassistant/components/denonavr/translations/pl.json +++ b/homeassistant/components/denonavr/translations/pl.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 urz\u0105dzenia AVR firmy Denon" }, - "flow_title": "Denon AVR: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Prosz\u0119 potwierdzi\u0107 dodanie urz\u0105dzenia", diff --git a/homeassistant/components/directv/translations/nl.json b/homeassistant/components/directv/translations/nl.json index 95709571234..3c0ca048e8b 100644 --- a/homeassistant/components/directv/translations/nl.json +++ b/homeassistant/components/directv/translations/nl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "data": { diff --git a/homeassistant/components/directv/translations/pl.json b/homeassistant/components/directv/translations/pl.json index db0dc7ea0a4..b7ee7c9251d 100644 --- a/homeassistant/components/directv/translations/pl.json +++ b/homeassistant/components/directv/translations/pl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "data": { diff --git a/homeassistant/components/doorbird/translations/nl.json b/homeassistant/components/doorbird/translations/nl.json index 1c43ee2d9c2..db32d96d831 100644 --- a/homeassistant/components/doorbird/translations/nl.json +++ b/homeassistant/components/doorbird/translations/nl.json @@ -10,7 +10,7 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/translations/pl.json b/homeassistant/components/doorbird/translations/pl.json index 79cdb5f5c8b..f55d2d406f2 100644 --- a/homeassistant/components/doorbird/translations/pl.json +++ b/homeassistant/components/doorbird/translations/pl.json @@ -10,7 +10,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/de.json b/homeassistant/components/elgato/translations/de.json index 1df8f91ecd6..cb7fb18f003 100644 --- a/homeassistant/components/elgato/translations/de.json +++ b/homeassistant/components/elgato/translations/de.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "M\u00f6chtest du das Elgato Key Light mit der Seriennummer \"{serial_number} \" zu Home Assistant hinzuf\u00fcgen?", - "title": "Elgato Key Light Ger\u00e4t entdeckt" + "title": "Elgato Light Ger\u00e4t entdeckt" } } } diff --git a/homeassistant/components/elgato/translations/nl.json b/homeassistant/components/elgato/translations/nl.json index 165d64df6b4..5fa47d30a10 100644 --- a/homeassistant/components/elgato/translations/nl.json +++ b/homeassistant/components/elgato/translations/nl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/pl.json b/homeassistant/components/elgato/translations/pl.json index 37a6b94a5b1..17b8522be35 100644 --- a/homeassistant/components/elgato/translations/pl.json +++ b/homeassistant/components/elgato/translations/pl.json @@ -7,18 +7,18 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { "host": "Nazwa hosta lub adres IP", "port": "Port" }, - "description": "Konfiguracja Elgato Key Light w celu integracji z Home Assistantem." + "description": "Konfiguracja Elgato Light w celu integracji z Home Assistantem." }, "zeroconf_confirm": { - "description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Key Light o numerze seryjnym `{serial_number}` do Home Assistanta?", - "title": "Wykryto urz\u0105dzenie Elgato Key Light" + "description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Light o numerze seryjnym `{serial_number}` do Home Assistanta?", + "title": "Wykryto urz\u0105dzenie Elgato Light" } } } diff --git a/homeassistant/components/emonitor/translations/nl.json b/homeassistant/components/emonitor/translations/nl.json index 3ca0e625cf6..802514fdadb 100644 --- a/homeassistant/components/emonitor/translations/nl.json +++ b/homeassistant/components/emonitor/translations/nl.json @@ -7,7 +7,7 @@ "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Wilt u {name} ({host}) instellen?", diff --git a/homeassistant/components/emonitor/translations/pl.json b/homeassistant/components/emonitor/translations/pl.json index a5b250c3f4d..c742b620ea9 100644 --- a/homeassistant/components/emonitor/translations/pl.json +++ b/homeassistant/components/emonitor/translations/pl.json @@ -7,7 +7,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", diff --git a/homeassistant/components/enphase_envoy/translations/nl.json b/homeassistant/components/enphase_envoy/translations/nl.json index da43476cd81..45f595dd86e 100644 --- a/homeassistant/components/enphase_envoy/translations/nl.json +++ b/homeassistant/components/enphase_envoy/translations/nl.json @@ -9,7 +9,7 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/enphase_envoy/translations/pl.json b/homeassistant/components/enphase_envoy/translations/pl.json index e35e215bffa..bf6c0ce430c 100644 --- a/homeassistant/components/enphase_envoy/translations/pl.json +++ b/homeassistant/components/enphase_envoy/translations/pl.json @@ -9,7 +9,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/epson/translations/es.json b/homeassistant/components/epson/translations/es.json index 77837bb25ce..972251b17d5 100644 --- a/homeassistant/components/epson/translations/es.json +++ b/homeassistant/components/epson/translations/es.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "powered_off": "\u00bfEst\u00e1 encendido el proyector? Debes encender el proyector para la configuraci\u00f3n inicial." }, "step": { "user": { diff --git a/homeassistant/components/epson/translations/pl.json b/homeassistant/components/epson/translations/pl.json index 98acbf5ef4b..8534ffd62e3 100644 --- a/homeassistant/components/epson/translations/pl.json +++ b/homeassistant/components/epson/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "powered_off": "Czy projektor jest w\u0142\u0105czony? Aby przeprowadzi\u0107 wst\u0119pn\u0105 konfiguracj\u0119, musisz w\u0142\u0105czy\u0107 projektor." }, "step": { "user": { diff --git a/homeassistant/components/esphome/translations/nl.json b/homeassistant/components/esphome/translations/nl.json index 1aae006feed..019c33004e7 100644 --- a/homeassistant/components/esphome/translations/nl.json +++ b/homeassistant/components/esphome/translations/nl.json @@ -9,7 +9,7 @@ "invalid_auth": "Ongeldige authenticatie", "resolve_error": "Kan het adres van de ESP niet vinden. Als deze fout aanhoudt, stel dan een statisch IP-adres in: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index 34b8d9bd0e1..7619f29ad64 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -9,7 +9,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie", "resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/ezviz/translations/nl.json b/homeassistant/components/ezviz/translations/nl.json index a6f7b3e985c..7b43acb3947 100644 --- a/homeassistant/components/ezviz/translations/nl.json +++ b/homeassistant/components/ezviz/translations/nl.json @@ -35,7 +35,7 @@ "username": "Gebruikersnaam" }, "description": "Geef handmatig de URL van uw regio op", - "title": "Verbind met aangepast Elvis URL" + "title": "Verbind met aangepast Ezviz URL" } } }, diff --git a/homeassistant/components/flume/translations/pl.json b/homeassistant/components/flume/translations/pl.json index f84513f1cb5..9ea3a635089 100644 --- a/homeassistant/components/flume/translations/pl.json +++ b/homeassistant/components/flume/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,6 +10,13 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Has\u0142o u\u017cytkownika {username} nie jest ju\u017c wa\u017cne.", + "title": "Ponownie uwierzytelnij konto Flume" + }, "user": { "data": { "client_id": "Identyfikator klienta", diff --git a/homeassistant/components/forked_daapd/translations/nl.json b/homeassistant/components/forked_daapd/translations/nl.json index 7eec6a34571..1dfb4c56eab 100644 --- a/homeassistant/components/forked_daapd/translations/nl.json +++ b/homeassistant/components/forked_daapd/translations/nl.json @@ -12,7 +12,7 @@ "wrong_password": "Onjuist wachtwoord.", "wrong_server_type": "De forked-daapd-integratie vereist een forked-daapd-server met versie >= 27.0." }, - "flow_title": "forked-daapd server: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/pl.json b/homeassistant/components/forked_daapd/translations/pl.json index 781dd9fca6b..bb20159bd1a 100644 --- a/homeassistant/components/forked_daapd/translations/pl.json +++ b/homeassistant/components/forked_daapd/translations/pl.json @@ -12,7 +12,7 @@ "wrong_password": "Nieprawid\u0142owe has\u0142o", "wrong_server_type": "Integracja forked-daapd wymaga serwera forked-daapd w wersji >= 27.0" }, - "flow_title": "Serwer forked-daapd: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index c152132900a..8fd24fa43dc 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -29,7 +29,7 @@ "description": "Aktualisiere die Anmeldeinformationen von FRITZ! Box Tools f\u00fcr: {host} . \n\nFRITZ! Box Tools kann sich nicht bei deiner FRITZ! Box anmelden.", "title": "Aktualisieren der FRITZ! Box Tools - Anmeldeinformationen" }, - "user": { + "start_config": { "data": { "host": "Host", "password": "Passwort", @@ -37,7 +37,7 @@ "username": "Benutzername" }, "description": "Einrichten der FRITZ! Box Tools zur Steuerung Ihrer FRITZ! Box.\n Ben\u00f6tigt: Benutzername, Passwort.", - "title": "Setup FRITZ! Box Tools" + "title": "Setup FRITZ! Box Tools - obligatorisch" } } } diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index 2c977b47119..cd5e1776d76 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -9,6 +9,7 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", + "connection_error": "Failed to connect", "invalid_auth": "Invalid authentication" }, "flow_title": "{name}", @@ -29,6 +30,16 @@ "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", "title": "Updating FRITZ!Box Tools - credentials" }, + "start_config": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", + "title": "Setup FRITZ!Box Tools - mandatory" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json index 563603aef5f..54c6e758f33 100644 --- a/homeassistant/components/fritz/translations/nl.json +++ b/homeassistant/components/fritz/translations/nl.json @@ -11,7 +11,7 @@ "connection_error": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, - "flow_title": "FRITZ!Box Tools: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritz/translations/pl.json b/homeassistant/components/fritz/translations/pl.json index 9d3f934d177..b9f17e23624 100644 --- a/homeassistant/components/fritz/translations/pl.json +++ b/homeassistant/components/fritz/translations/pl.json @@ -11,7 +11,7 @@ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, - "flow_title": "Narz\u0119dzia FRITZ!Box: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json index c1f9c83e395..b1be4c8214f 100644 --- a/homeassistant/components/fritzbox/translations/nl.json +++ b/homeassistant/components/fritzbox/translations/nl.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Ongeldige authenticatie" }, - "flow_title": "AVM FRITZ!SmartHome: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/pl.json b/homeassistant/components/fritzbox/translations/pl.json index dc05e431832..d9832ee51a4 100644 --- a/homeassistant/components/fritzbox/translations/pl.json +++ b/homeassistant/components/fritzbox/translations/pl.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Niepoprawne uwierzytelnienie" }, - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/nl.json b/homeassistant/components/fritzbox_callmonitor/translations/nl.json index bc706861313..20d884535b8 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/nl.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/nl.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Ongeldige authenticatie" }, - "flow_title": "AVM FRITZ!Box oproepmonitor: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/pl.json b/homeassistant/components/fritzbox_callmonitor/translations/pl.json index fa0317f5c9d..29ef1d6b3b6 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/pl.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/pl.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Niepoprawne uwierzytelnienie" }, - "flow_title": "Monitor po\u0142\u0105cze\u0144 AVM FRITZ!Box: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json index 2f2e1ac0d2b..26a92757a4b 100644 --- a/homeassistant/components/goalzero/translations/en.json +++ b/homeassistant/components/goalzero/translations/en.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Device is already configured", + "invalid_host": "Invalid hostname or IP address", + "unknown": "Unexpected error" }, "error": { "cannot_connect": "Failed to connect", @@ -9,12 +11,16 @@ "unknown": "Unexpected error" }, "step": { + "confirm_discovery": { + "description": "DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Host", "name": "Name" }, - "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wi-fi network. DHCP reservation is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Set it up in your router settings for the device. Refer to your router's user manual.", + "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wi-fi network. DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/gogogate2/translations/ca.json b/homeassistant/components/gogogate2/translations/ca.json index a68c0e6384c..542a0cd2e0d 100644 --- a/homeassistant/components/gogogate2/translations/ca.json +++ b/homeassistant/components/gogogate2/translations/ca.json @@ -7,6 +7,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { @@ -15,7 +16,7 @@ "username": "Nom d'usuari" }, "description": "Proporciona, a continuaci\u00f3, la informaci\u00f3 necess\u00e0ria.", - "title": "Configuraci\u00f3 de GogoGate2 o iSmartGate" + "title": "Configuraci\u00f3 de Gogogate2 o ismartgate" } } } diff --git a/homeassistant/components/gogogate2/translations/es.json b/homeassistant/components/gogogate2/translations/es.json index 1498cc12368..65d14508ce6 100644 --- a/homeassistant/components/gogogate2/translations/es.json +++ b/homeassistant/components/gogogate2/translations/es.json @@ -7,6 +7,7 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/gogogate2/translations/et.json b/homeassistant/components/gogogate2/translations/et.json index b3ab67388a1..966fca125c0 100644 --- a/homeassistant/components/gogogate2/translations/et.json +++ b/homeassistant/components/gogogate2/translations/et.json @@ -7,6 +7,7 @@ "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/gogogate2/translations/nl.json b/homeassistant/components/gogogate2/translations/nl.json index 5418735ec07..a32bb1af69b 100644 --- a/homeassistant/components/gogogate2/translations/nl.json +++ b/homeassistant/components/gogogate2/translations/nl.json @@ -7,6 +7,7 @@ "cannot_connect": "Kon niet verbinden", "invalid_auth": "Ongeldige authenticatie" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/gogogate2/translations/no.json b/homeassistant/components/gogogate2/translations/no.json index 315a5fa9e2a..6640d35681a 100644 --- a/homeassistant/components/gogogate2/translations/no.json +++ b/homeassistant/components/gogogate2/translations/no.json @@ -7,6 +7,7 @@ "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, + "flow_title": "{device} ( {ip_address} )", "step": { "user": { "data": { @@ -15,7 +16,7 @@ "username": "Brukernavn" }, "description": "Gi n\u00f8dvendig informasjon nedenfor.", - "title": "Sett opp GogoGate2 eller iSmartGate" + "title": "Sett opp Gogogate2 eller ismartgate" } } } diff --git a/homeassistant/components/gogogate2/translations/pl.json b/homeassistant/components/gogogate2/translations/pl.json index 6569c444c20..fbd2c7af42e 100644 --- a/homeassistant/components/gogogate2/translations/pl.json +++ b/homeassistant/components/gogogate2/translations/pl.json @@ -7,6 +7,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { @@ -15,7 +16,7 @@ "username": "Nazwa u\u017cytkownika" }, "description": "Wprowad\u017a wymagane informacje poni\u017cej.", - "title": "Konfiguracja GogoGate2 lub iSmartGate" + "title": "Konfiguracja Gogogate2 lub ismartgate" } } } diff --git a/homeassistant/components/gogogate2/translations/ru.json b/homeassistant/components/gogogate2/translations/ru.json index 4efa554fc91..3c5af51d94e 100644 --- a/homeassistant/components/gogogate2/translations/ru.json +++ b/homeassistant/components/gogogate2/translations/ru.json @@ -7,6 +7,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { @@ -15,7 +16,7 @@ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 GogoGate2.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 GogoGate2 \u0438\u043b\u0438 iSmartGate" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Gogogate2 \u0438\u043b\u0438 ismartgate" } } } diff --git a/homeassistant/components/gogogate2/translations/zh-Hant.json b/homeassistant/components/gogogate2/translations/zh-Hant.json index 607794131ef..0e11373b129 100644 --- a/homeassistant/components/gogogate2/translations/zh-Hant.json +++ b/homeassistant/components/gogogate2/translations/zh-Hant.json @@ -7,6 +7,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { @@ -15,7 +16,7 @@ "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8acb\u65bc\u4e0b\u65b9\u63d0\u4f9b\u6240\u9700\u8cc7\u8a0a\u3002", - "title": "\u8a2d\u5b9a GogoGate2 \u6216 iSmartGate" + "title": "\u8a2d\u5b9a Gogogate2 \u6216 ismartgate" } } } diff --git a/homeassistant/components/growatt_server/translations/en.json b/homeassistant/components/growatt_server/translations/en.json index 365f577a007..5461c822320 100644 --- a/homeassistant/components/growatt_server/translations/en.json +++ b/homeassistant/components/growatt_server/translations/en.json @@ -16,7 +16,7 @@ "user": { "data": { "name": "Name", - "password": "Name", + "password": "Password", "username": "Username" }, "title": "Enter your Growatt information" diff --git a/homeassistant/components/growatt_server/translations/es.json b/homeassistant/components/growatt_server/translations/es.json new file mode 100644 index 00000000000..23860f225da --- /dev/null +++ b/homeassistant/components/growatt_server/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "No se han encontrado plantas en esta cuenta." + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "plant": { + "data": { + "plant_id": "Planta" + }, + "title": "Selecciona tu planta" + }, + "user": { + "data": { + "name": "Nombre", + "password": "Nombre", + "username": "Usuario" + }, + "title": "Introduce tu informaci\u00f3n de Growatt." + } + } + }, + "title": "Servidor Growatt" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/pl.json b/homeassistant/components/growatt_server/translations/pl.json new file mode 100644 index 00000000000..2041a577489 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_plants": "Nie znaleziono plantacji na tym koncie" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "plant": { + "data": { + "plant_id": "Plantacja" + }, + "title": "Wybierz swoj\u0105 plantacj\u0119" + }, + "user": { + "data": { + "name": "Nazwa", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Wprowad\u017a dane Growatt." + } + } + }, + "title": "Serwer Growatt" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index 9e6cfdaa7c7..981534cca9b 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -6,6 +6,9 @@ "cannot_connect": "No se pudo conectar" }, "step": { + "discovery_confirm": { + "description": "\u00bfQuieres configurar este dispositivo Guardian?" + }, "user": { "data": { "ip_address": "Direcci\u00f3n IP", diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json index 7dfa4b06c5b..663265a7a11 100644 --- a/homeassistant/components/guardian/translations/pl.json +++ b/homeassistant/components/guardian/translations/pl.json @@ -6,6 +6,9 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 to urz\u0105dzenie Guardian?" + }, "user": { "data": { "ip_address": "Adres IP", diff --git a/homeassistant/components/harmony/translations/nl.json b/homeassistant/components/harmony/translations/nl.json index 33cbeca8893..aaf16ed2dc7 100644 --- a/homeassistant/components/harmony/translations/nl.json +++ b/homeassistant/components/harmony/translations/nl.json @@ -7,7 +7,7 @@ "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "Wil je {name} ({host}) instellen?", diff --git a/homeassistant/components/harmony/translations/pl.json b/homeassistant/components/harmony/translations/pl.json index 0a288b0ce07..7e29aa8ac9a 100644 --- a/homeassistant/components/harmony/translations/pl.json +++ b/homeassistant/components/harmony/translations/pl.json @@ -7,7 +7,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index c2751e9eb6b..b11038cc806 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -38,7 +38,7 @@ "entities": "Entiteiten", "mode": "Mode" }, - "description": "Kies de entiteiten die u wilt opnemen. In de accessoiremodus is slechts een enkele entiteit inbegrepen. In de bridge-include-modus worden alle entiteiten in het domein opgenomen, tenzij specifieke entiteiten zijn geselecteerd. In de bridge-uitsluitingsmodus worden alle entiteiten in het domein opgenomen, behalve de uitgesloten entiteiten. Voor de beste prestaties is elke tv-mediaspeler en camera een apart HomeKit-accessoire.", + "description": "Kies de entiteiten die u wilt opnemen. In de accessoiremodus is slechts een enkele entiteit inbegrepen. In de bridge-include-modus worden alle entiteiten in het domein opgenomen, tenzij specifieke entiteiten zijn geselecteerd. In de bridge-uitsluitingsmodus worden alle entiteiten in het domein opgenomen, behalve de uitgesloten entiteiten. Voor de beste prestaties wordt voor elke media speler, activiteit gebaseerde afstandsbediening, slot en camera een afzonderlijke Homekit-accessoire gemaakt.", "title": "Selecteer de entiteiten die u wilt opnemen" }, "init": { diff --git a/homeassistant/components/homekit_controller/translations/nl.json b/homeassistant/components/homekit_controller/translations/nl.json index 57692426ce0..64b1d0802db 100644 --- a/homeassistant/components/homekit_controller/translations/nl.json +++ b/homeassistant/components/homekit_controller/translations/nl.json @@ -17,10 +17,10 @@ "unable_to_pair": "Kan niet koppelen, probeer het opnieuw.", "unknown_error": "Apparaat meldde een onbekende fout. Koppelen mislukt." }, - "flow_title": "{name} via HomeKit-accessoireprotocol", + "flow_title": "{name}", "step": { "busy_error": { - "description": "Onderbreek het koppelen op alle controllers, of probeer het apparaat opnieuw op te starten, en ga dan verder om het koppelen te hervatten.", + "description": "Onderbreek het koppelen op alle controllers, of probeer het apparaat opnieuw op te starten, ga dan verder om het koppelen te hervatten.", "title": "Het apparaat is al aan het koppelen met een andere controller" }, "max_tries_error": { diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json index 3ccdfe452e5..87a7d4ef2b5 100644 --- a/homeassistant/components/homekit_controller/translations/pl.json +++ b/homeassistant/components/homekit_controller/translations/pl.json @@ -17,7 +17,7 @@ "unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie", "unknown_error": "Urz\u0105dzenie zg\u0142osi\u0142o nieznany b\u0142\u0105d. Parowanie nie powiod\u0142o si\u0119." }, - "flow_title": "{name} poprzez akcesorium HomeKit", + "flow_title": "{name}", "step": { "busy_error": { "description": "Przerwij parowanie we wszystkich kontrolerach lub spr\u00f3buj ponownie uruchomi\u0107 urz\u0105dzenie, a nast\u0119pnie wzn\u00f3w parowanie", diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index 11d450abc3b..d851b239436 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -15,7 +15,7 @@ "response_error": "Onbekende fout van het apparaat", "unknown": "Onverwachte fout" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 2d71c097b3b..efb441f8972 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -15,7 +15,7 @@ "response_error": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w urz\u0105dzeniu", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ipp/translations/nl.json b/homeassistant/components/ipp/translations/nl.json index f3d2ee60797..5296fcac36f 100644 --- a/homeassistant/components/ipp/translations/nl.json +++ b/homeassistant/components/ipp/translations/nl.json @@ -13,7 +13,7 @@ "cannot_connect": "Kan geen verbinding maken", "connection_upgrade": "Kan geen verbinding maken met de printer. Probeer het opnieuw met SSL / TLS-optie aangevinkt." }, - "flow_title": "Printer: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json index 8a058c9b9d6..feba8470d7e 100644 --- a/homeassistant/components/ipp/translations/pl.json +++ b/homeassistant/components/ipp/translations/pl.json @@ -13,7 +13,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "connection_upgrade": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z drukark\u0105. Spr\u00f3buj ponownie z zaznaczon\u0105 opcj\u0105 SSL/TLS." }, - "flow_title": "Drukarka: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json index 9fed7b8c99c..be274ad84a9 100644 --- a/homeassistant/components/isy994/translations/nl.json +++ b/homeassistant/components/isy994/translations/nl.json @@ -9,7 +9,7 @@ "invalid_host": "De hostvermelding had de niet volledig URL-indeling, bijvoorbeeld http://192.168.10.100:80", "unknown": "Onverwachte fout" }, - "flow_title": "Universele apparaten ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json index 532d0a72cd5..c0ee1b5d3a3 100644 --- a/homeassistant/components/isy994/translations/pl.json +++ b/homeassistant/components/isy994/translations/pl.json @@ -9,7 +9,7 @@ "invalid_host": "Wpis hosta nie by\u0142 w pe\u0142nym formacie URL, np. http://192.168.10.100:80", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Urz\u0105dzenia uniwersalne ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/kodi/translations/nl.json b/homeassistant/components/kodi/translations/nl.json index 4143d933d19..879ca83051f 100644 --- a/homeassistant/components/kodi/translations/nl.json +++ b/homeassistant/components/kodi/translations/nl.json @@ -12,7 +12,7 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/kodi/translations/pl.json b/homeassistant/components/kodi/translations/pl.json index 2194c480c03..caac93e2b2d 100644 --- a/homeassistant/components/kodi/translations/pl.json +++ b/homeassistant/components/kodi/translations/pl.json @@ -12,7 +12,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/kraken/translations/en.json b/homeassistant/components/kraken/translations/en.json new file mode 100644 index 00000000000..ced53b76e10 --- /dev/null +++ b/homeassistant/components/kraken/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to start set up?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update interval", + "tracked_asset_pairs": "Tracked Asset Pairs" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/es.json b/homeassistant/components/light/translations/es.json index 790fd0e34ef..f24379a7838 100644 --- a/homeassistant/components/light/translations/es.json +++ b/homeassistant/components/light/translations/es.json @@ -3,7 +3,7 @@ "action_type": { "brightness_decrease": "Disminuir brillo de {entity_name}", "brightness_increase": "Aumentar brillo de {entity_name}", - "flash": "Destellos {entidad_nombre}", + "flash": "Destellos {entity_name}", "toggle": "Alternar {entity_name}", "turn_off": "Apagar {entity_name}", "turn_on": "Encender {entity_name}" diff --git a/homeassistant/components/lutron_caseta/translations/nl.json b/homeassistant/components/lutron_caseta/translations/nl.json index b281d3cd22c..c2a342123d0 100644 --- a/homeassistant/components/lutron_caseta/translations/nl.json +++ b/homeassistant/components/lutron_caseta/translations/nl.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Kon niet verbinden" }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "Kan bridge (host: {host} ) niet instellen, ge\u00efmporteerd uit configuration.yaml.", diff --git a/homeassistant/components/lutron_caseta/translations/pl.json b/homeassistant/components/lutron_caseta/translations/pl.json index 8a8c0a759b0..b890cf3323a 100644 --- a/homeassistant/components/lutron_caseta/translations/pl.json +++ b/homeassistant/components/lutron_caseta/translations/pl.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "Nie mo\u017cna skonfigurowa\u0107 mostka (host: {host}) zaimportowanego z pliku configuration.yaml.", diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 2c5895a98c8..ffd2d1b36a1 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -70,7 +70,7 @@ "discovery": "Erkennung aktivieren", "will_enable": "Letzten Willen aktivieren" }, - "description": "Bitte die MQTT-Einstellungen ausw\u00e4hlen.", + "description": "Erkennung - Wenn die Erkennung aktiviert ist (empfohlen), erkennt Home Assistant automatisch Ger\u00e4te und Entit\u00e4ten, die ihre Konfiguration auf dem MQTT-Broker ver\u00f6ffentlichen. Wenn die Erkennung deaktiviert ist, muss die gesamte Konfiguration manuell vorgenommen werden.\nGeburtsnachricht - Die Geburtsnachricht wird jedes Mal gesendet, wenn sich Home Assistant (erneut) mit dem MQTT-Broker verbindet.\nWill-Nachricht - Die Will-Nachricht wird jedes Mal gesendet, wenn Home Assistant die Verbindung zum Broker verliert, sowohl im Falle einer sauberen (z. B. Herunterfahren von Home Assistant) als auch im Falle einer unsauberen (z. B. Absturz von Home Assistant oder Verlust der Netzwerkverbindung) Verbindungstrennung.", "title": "MQTT-Optionen" } } diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index fc8606d7b43..e9c2469a061 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -79,7 +79,7 @@ "will_retain": "Will message behouden", "will_topic": "Will message topic" }, - "description": "Selecteer MQTT-opties.", + "description": "Detectie - Als detectie is ingeschakeld (aanbevolen), zal Home Assistant automatisch apparaten en entiteiten detecteren die hun configuratie publiceren op de MQTT-broker. Als detectie is uitgeschakeld, moet alle configuratie handmatig worden uitgevoerd.\n Birth message - Het birth message wordt elke keer dat Home Assistant (opnieuw) verbinding maakt met de MQTT-broker, verzonden.\n Will message - Het will message wordt telkens verzonden wanneer Home Assistant de verbinding met de broker verliest, zowel in het geval van een schone (bijv. Home Assistant wordt uitgeschakeld) als in geval van een onjuiste (bijv. Home Assistant crasht of verliest de netwerkverbinding) verbroken verbinding.", "title": "MQTT-opties" } } diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 17ea7407f3c..2103cc2c441 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -62,7 +62,8 @@ "port": "Port", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT" + "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT", + "title": "Opcje brokera" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Flaga \"retain\" wiadomo\u015bci \"will\"", "will_topic": "Temat wiadomo\u015bci \"will\"" }, - "description": "Opcje MQTT" + "description": "Wykrywanie - je\u015bli wykrywanie jest w\u0142\u0105czone (zalecane), Home Assistant automatycznie wykryje urz\u0105dzenia i encje, kt\u00f3re publikuj\u0105 swoj\u0105 konfiguracj\u0119 w brokerze MQTT. Je\u015bli wykrywanie jest wy\u0142\u0105czone, ca\u0142\u0105 konfiguracj\u0119 nale\u017cy wykona\u0107 r\u0119cznie.\nWiadomo\u015b\u0107 Birth - wiadomo\u015b\u0107 Birth zostanie wys\u0142ana za ka\u017cdym razem, gdy Home Assistant (ponownie) po\u0142\u0105czy si\u0119 z brokerem MQTT.\nWiadomo\u015b\u0107 Will - wiadomo\u015b\u0107 Will b\u0119dzie wysy\u0142ana za ka\u017cdym razem, gdy Home Assistant utraci po\u0142\u0105czenie z brokerem, zar\u00f3wno w przypadku czystego (np. wy\u0142\u0105czenie Home Assistanta), jak i w przypadku nieczystego (np. zawieszenie Home Assistanta lub utrata po\u0142\u0105czenia sieciowego) roz\u0142\u0105czenia.", + "title": "Opcje MQTT" } } } diff --git a/homeassistant/components/myq/translations/pl.json b/homeassistant/components/myq/translations/pl.json index ee87eb52294..9bd66ab67d1 100644 --- a/homeassistant/components/myq/translations/pl.json +++ b/homeassistant/components/myq/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,6 +10,13 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Has\u0142o u\u017cytkownika {username} nie jest ju\u017c wa\u017cne.", + "title": "Ponownie uwierzytelnij konto MyQ" + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json new file mode 100644 index 00000000000..59f6df43701 --- /dev/null +++ b/homeassistant/components/nam/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "device_unsupported": "El dispositivo no es compatible." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "\u00bfQuieres configurar Nettigo Air Monitor en {host} ?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Configurar la integraci\u00f3n de Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/en.json b/homeassistant/components/nexia/translations/en.json index 050db24e0cf..20b0a137970 100644 --- a/homeassistant/components/nexia/translations/en.json +++ b/homeassistant/components/nexia/translations/en.json @@ -14,7 +14,8 @@ "brand": "Brand", "password": "Password", "username": "Username" - } + }, + "title": "Connect to mynexia.com" } } } diff --git a/homeassistant/components/nuki/translations/nl.json b/homeassistant/components/nuki/translations/nl.json index 3bfa8f60b70..4157d21ffd3 100644 --- a/homeassistant/components/nuki/translations/nl.json +++ b/homeassistant/components/nuki/translations/nl.json @@ -13,7 +13,7 @@ "data": { "token": "Toegangstoken" }, - "description": "De Nuki integratie moet opnieuw authenticeren met je bridge.", + "description": "De Nuki integratie moet opnieuw authenticeren met uw bridge.", "title": "Verifieer de integratie opnieuw" }, "user": { diff --git a/homeassistant/components/nzbget/translations/nl.json b/homeassistant/components/nzbget/translations/nl.json index 89d58d14292..5f98d7435ec 100644 --- a/homeassistant/components/nzbget/translations/nl.json +++ b/homeassistant/components/nzbget/translations/nl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Kon niet verbinden" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/pl.json b/homeassistant/components/nzbget/translations/pl.json index 9ae5b46b680..624bccef154 100644 --- a/homeassistant/components/nzbget/translations/pl.json +++ b/homeassistant/components/nzbget/translations/pl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/nl.json b/homeassistant/components/ovo_energy/translations/nl.json index d598e17d93b..245575d8666 100644 --- a/homeassistant/components/ovo_energy/translations/nl.json +++ b/homeassistant/components/ovo_energy/translations/nl.json @@ -5,7 +5,7 @@ "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/pl.json b/homeassistant/components/ovo_energy/translations/pl.json index 5767f3f7cf2..c426d140417 100644 --- a/homeassistant/components/ovo_energy/translations/pl.json +++ b/homeassistant/components/ovo_energy/translations/pl.json @@ -5,7 +5,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/plaato/translations/nl.json b/homeassistant/components/plaato/translations/nl.json index d50763d0e1a..23fae52b020 100644 --- a/homeassistant/components/plaato/translations/nl.json +++ b/homeassistant/components/plaato/translations/nl.json @@ -19,7 +19,7 @@ "token": "Plak hier de verificatie-token", "use_webhook": "Webhook gebruiken" }, - "description": "Om de API te kunnenopvragen is een `auth_token` nodig, die kan worden verkregen door [deze] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructies te volgen\n\n Geselecteerd apparaat: **{device_type}** \n\nIndien u liever de ingebouwde webhook methode gebruikt (alleen Airlock) vink dan het vakje hieronder aan en laat Auth Token leeg", + "description": "Om de API te kunnen opvragen is een `auth_token` nodig, die kan worden verkregen door [deze] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructies te volgen\n\n Geselecteerd apparaat: **{device_type}** \n\nIndien u liever de ingebouwde webhook methode gebruikt (alleen Airlock) vink dan het vakje hieronder aan en laat Auth Token leeg", "title": "Selecteer API-methode" }, "user": { diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index af77f6f15e1..160cf182d3d 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -8,7 +8,7 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "Glimlach: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 0e6d3b16d4f..5d0bdd0f4e6 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -8,7 +8,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/nl.json b/homeassistant/components/powerwall/translations/nl.json index 5149f391669..87b78e719d0 100644 --- a/homeassistant/components/powerwall/translations/nl.json +++ b/homeassistant/components/powerwall/translations/nl.json @@ -10,7 +10,7 @@ "unknown": "Onverwachte fout", "wrong_version": "Uw powerwall gebruikt een softwareversie die niet wordt ondersteund. Overweeg om dit probleem te upgraden of te melden, zodat het kan worden opgelost." }, - "flow_title": "Tesla Powerwall ({ip_adres})", + "flow_title": "({ip_adres})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json index 272f28df3b9..8d4f82fa14e 100644 --- a/homeassistant/components/powerwall/translations/pl.json +++ b/homeassistant/components/powerwall/translations/pl.json @@ -10,7 +10,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d", "wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107." }, - "flow_title": "Tesla UPS ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/es.json b/homeassistant/components/rainmachine/translations/es.json index 9562aa59928..317339ed39f 100644 --- a/homeassistant/components/rainmachine/translations/es.json +++ b/homeassistant/components/rainmachine/translations/es.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json index f5b4103b9e7..cbf76b879cb 100644 --- a/homeassistant/components/rainmachine/translations/nl.json +++ b/homeassistant/components/rainmachine/translations/nl.json @@ -6,7 +6,7 @@ "error": { "invalid_auth": "Ongeldige authenticatie" }, - "flow_title": "RainMachine {ip}", + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/pl.json b/homeassistant/components/rainmachine/translations/pl.json index ff8918660f8..665152e8e0f 100644 --- a/homeassistant/components/rainmachine/translations/pl.json +++ b/homeassistant/components/rainmachine/translations/pl.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Niepoprawne uwierzytelnienie" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json index daecee2f1dc..6bf3435a19b 100644 --- a/homeassistant/components/roku/translations/nl.json +++ b/homeassistant/components/roku/translations/nl.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "data": { diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json index 1a570c64347..41ea348543e 100644 --- a/homeassistant/components/roku/translations/pl.json +++ b/homeassistant/components/roku/translations/pl.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "data": { diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index f35ed8539e4..a18bd89ae12 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index 863023f321b..d4624a91906 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index 2a6cca466ea..3f9e61c8a8c 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -7,7 +7,7 @@ "cannot_connect": "Kan geen verbinding maken", "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund." }, - "flow_title": "Samsung TV: {model}", + "flow_title": "{model}", "step": { "confirm": { "description": "Wilt u Samsung TV {model} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven", diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json index 07751797f85..66d6ce3c4f3 100644 --- a/homeassistant/components/samsungtv/translations/pl.json +++ b/homeassistant/components/samsungtv/translations/pl.json @@ -7,7 +7,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "not_supported": "Ten telewizor Samsung nie jest obecnie obs\u0142ugiwany" }, - "flow_title": "Samsung TV: {model}", + "flow_title": "{model}", "step": { "confirm": { "description": "Czy chcesz skonfigurowa\u0107 telewizor Samsung {model}? Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie. R\u0119czne konfiguracje tego telewizora zostan\u0105 zast\u0105pione.", diff --git a/homeassistant/components/screenlogic/translations/nl.json b/homeassistant/components/screenlogic/translations/nl.json index 7c752e0ae4d..ff73cc5920c 100644 --- a/homeassistant/components/screenlogic/translations/nl.json +++ b/homeassistant/components/screenlogic/translations/nl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/pl.json b/homeassistant/components/screenlogic/translations/pl.json index 64e2573ddb0..545e8451a47 100644 --- a/homeassistant/components/screenlogic/translations/pl.json +++ b/homeassistant/components/screenlogic/translations/pl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/smappee/translations/nl.json b/homeassistant/components/smappee/translations/nl.json index 0bc976e587c..ed9ef6e5e9f 100644 --- a/homeassistant/components/smappee/translations/nl.json +++ b/homeassistant/components/smappee/translations/nl.json @@ -9,7 +9,7 @@ "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/smappee/translations/pl.json b/homeassistant/components/smappee/translations/pl.json index ac3393e51e1..91abb6c3825 100644 --- a/homeassistant/components/smappee/translations/pl.json +++ b/homeassistant/components/smappee/translations/pl.json @@ -9,7 +9,7 @@ "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})" }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/nl.json b/homeassistant/components/somfy_mylink/translations/nl.json index bb897f132a2..4ab135993a1 100644 --- a/homeassistant/components/somfy_mylink/translations/nl.json +++ b/homeassistant/components/somfy_mylink/translations/nl.json @@ -8,7 +8,7 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/pl.json b/homeassistant/components/somfy_mylink/translations/pl.json index 7e49ecb2bca..3da5c423e1a 100644 --- a/homeassistant/components/somfy_mylink/translations/pl.json +++ b/homeassistant/components/somfy_mylink/translations/pl.json @@ -8,7 +8,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/nl.json b/homeassistant/components/sonarr/translations/nl.json index 3203f3ac128..d2ded412f1b 100644 --- a/homeassistant/components/sonarr/translations/nl.json +++ b/homeassistant/components/sonarr/translations/nl.json @@ -9,7 +9,7 @@ "cannot_connect": "Kon niet verbinden", "invalid_auth": "Ongeldige authenticatie" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "De Sonarr-integratie moet handmatig opnieuw worden geverifieerd met de Sonarr-API die wordt gehost op: {host}", diff --git a/homeassistant/components/sonarr/translations/pl.json b/homeassistant/components/sonarr/translations/pl.json index 217f0b06d27..7985bdf8f7f 100644 --- a/homeassistant/components/sonarr/translations/pl.json +++ b/homeassistant/components/sonarr/translations/pl.json @@ -9,7 +9,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "Integracja Sonarr musi by\u0107 r\u0119cznie ponownie uwierzytelniona za pomoc\u0105 API Sonarr pod adresem: {host}", diff --git a/homeassistant/components/songpal/translations/nl.json b/homeassistant/components/songpal/translations/nl.json index 9566bf999a8..0550b154ca4 100644 --- a/homeassistant/components/songpal/translations/nl.json +++ b/homeassistant/components/songpal/translations/nl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Kon niet verbinden" }, - "flow_title": "Sony Songpal {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "init": { "description": "Wilt u {name} ( {host} ) instellen?" diff --git a/homeassistant/components/songpal/translations/pl.json b/homeassistant/components/songpal/translations/pl.json index 911c98db651..18bbb3cb1f8 100644 --- a/homeassistant/components/songpal/translations/pl.json +++ b/homeassistant/components/songpal/translations/pl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?" diff --git a/homeassistant/components/squeezebox/translations/nl.json b/homeassistant/components/squeezebox/translations/nl.json index f60fc640db2..fc05f6d4807 100644 --- a/homeassistant/components/squeezebox/translations/nl.json +++ b/homeassistant/components/squeezebox/translations/nl.json @@ -10,7 +10,7 @@ "no_server_found": "Kan server niet automatisch vinden.", "unknown": "Onverwachte fout" }, - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/squeezebox/translations/pl.json b/homeassistant/components/squeezebox/translations/pl.json index 8a092a27062..1f03e2f3702 100644 --- a/homeassistant/components/squeezebox/translations/pl.json +++ b/homeassistant/components/squeezebox/translations/pl.json @@ -10,7 +10,7 @@ "no_server_found": "Nie uda\u0142o si\u0119 automatycznie wykry\u0107 serwera", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/syncthing/translations/de.json b/homeassistant/components/syncthing/translations/de.json index bef066f385b..06db3e89b97 100644 --- a/homeassistant/components/syncthing/translations/de.json +++ b/homeassistant/components/syncthing/translations/de.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "title": "Syncthing-Integration einrichten", "token": "Token", "url": "URL", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" diff --git a/homeassistant/components/syncthing/translations/es.json b/homeassistant/components/syncthing/translations/es.json new file mode 100644 index 00000000000..550c1874010 --- /dev/null +++ b/homeassistant/components/syncthing/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "title": "Configurar integraci\u00f3n de Syncthing", + "token": "Token", + "url": "URL", + "verify_ssl": "Verificar certificado SSL" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/pl.json b/homeassistant/components/syncthing/translations/pl.json index 6e104374b80..e4e2619f1eb 100644 --- a/homeassistant/components/syncthing/translations/pl.json +++ b/homeassistant/components/syncthing/translations/pl.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "title": "Konfiguracja integracji Syncthing", + "token": "Token", + "url": "URL", + "verify_ssl": "Weryfikacja certyfikatu SSL" + } + } } - } + }, + "title": "Syncthing" } \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/nl.json b/homeassistant/components/syncthru/translations/nl.json index d86e9fa2087..a2388cd335b 100644 --- a/homeassistant/components/syncthru/translations/nl.json +++ b/homeassistant/components/syncthru/translations/nl.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "Apparaat ondersteunt SyncThru niet", "unknown_state": "Printerstatus onbekend, controleer URL en netwerkconnectiviteit" }, - "flow_title": "Samsung SyncThru Printer: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/syncthru/translations/pl.json b/homeassistant/components/syncthru/translations/pl.json index cacb7e92b1f..72ed6fd0576 100644 --- a/homeassistant/components/syncthru/translations/pl.json +++ b/homeassistant/components/syncthru/translations/pl.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "Urz\u0105dzenie nie obs\u0142uguje SyncThru", "unknown_state": "Nieznany stan drukarki, sprawd\u017a adres URL i pod\u0142\u0105czenie do sieci" }, - "flow_title": "Drukarka Samsung SyncThru: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index be8d7d45348..6fa342800a2 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -10,7 +10,7 @@ "otp_failed": "Tweestapsverificatie is mislukt, probeer het opnieuw met een nieuwe toegangscode", "unknown": "Onbekende fout: controleer de logs voor meer informatie" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/pl.json b/homeassistant/components/synology_dsm/translations/pl.json index b5929c307de..e060f666f3c 100644 --- a/homeassistant/components/synology_dsm/translations/pl.json +++ b/homeassistant/components/synology_dsm/translations/pl.json @@ -10,7 +10,7 @@ "otp_failed": "Uwierzytelnianie dwuetapowe nie powiod\u0142o si\u0119, spr\u00f3buj ponownie z nowym kodem dost\u0119pu", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/system_bridge/translations/es.json b/homeassistant/components/system_bridge/translations/es.json index fc2c8a111ae..2b7a859fcd1 100644 --- a/homeassistant/components/system_bridge/translations/es.json +++ b/homeassistant/components/system_bridge/translations/es.json @@ -10,6 +10,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, + "flow_title": "{name}", "step": { "authenticate": { "data": { @@ -26,5 +27,6 @@ "description": "Por favor, introduce tus datos de conexi\u00f3n." } } - } + }, + "title": "Pasarela del sistema" } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/nl.json b/homeassistant/components/system_bridge/translations/nl.json index f04d15cc453..eefadebda35 100644 --- a/homeassistant/components/system_bridge/translations/nl.json +++ b/homeassistant/components/system_bridge/translations/nl.json @@ -10,7 +10,7 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "System Bridge: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/system_bridge/translations/pl.json b/homeassistant/components/system_bridge/translations/pl.json index 423e0eb2f65..0ae579842d6 100644 --- a/homeassistant/components/system_bridge/translations/pl.json +++ b/homeassistant/components/system_bridge/translations/pl.json @@ -2,18 +2,31 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "{name}", "step": { + "authenticate": { + "data": { + "api_key": "Klucz API" + }, + "description": "Wprowad\u017a klucz API ustawiony w konfiguracji dla {name}." + }, "user": { "data": { - "host": "Nazwa hosta lub adres IP" - } + "api_key": "Klucz API", + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "description": "Wprowad\u017a dane po\u0142\u0105czenia." } } - } + }, + "title": "Mostek System" } \ No newline at end of file diff --git a/homeassistant/components/tado/translations/de.json b/homeassistant/components/tado/translations/de.json index f531a428be3..dec90a46aef 100644 --- a/homeassistant/components/tado/translations/de.json +++ b/homeassistant/components/tado/translations/de.json @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "fallback": "Aktivieren den Fallback-Modus." + "fallback": "Aktiviert den Fallback-Modus." }, "description": "Der Fallback-Modus wechselt beim n\u00e4chsten Zeitplanwechsel nach dem manuellen Anpassen einer Zone zu Smart Schedule.", "title": "Passe die Tado-Optionen an." diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index e0374fd3926..f1049d6882e 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -44,7 +44,7 @@ "support_color": "Forceer kleurenondersteuning", "temp_divider": "Temperatuurwaarde deler (0 = standaardwaarde)", "temp_step_override": "Doeltemperatuur stap", - "tuya_max_coltemp": "Max. Kleurtemperatuur gerapporteerd door apparaat", + "tuya_max_coltemp": "Max. kleurtemperatuur gerapporteerd door apparaat", "unit_of_measurement": "Temperatuureenheid gebruikt door apparaat" }, "description": "Configureer opties om weergegeven informatie aan te passen voor {device_type} apparaat `{device_name}`", diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index e5e5e3a1dfb..8be73aad793 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -10,7 +10,7 @@ "service_unavailable": "Kan geen verbinding maken", "unknown_client_mac": "Geen client beschikbaar op dat MAC-adres" }, - "flow_title": "UniFi Netwerk {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index 719120eeb5e..ebfb901871b 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -10,7 +10,7 @@ "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown_client_mac": "Brak klienta z tym adresem MAC" }, - "flow_title": "Sie\u0107 UniFi {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/translations/nl.json b/homeassistant/components/upnp/translations/nl.json index 331d5850fc4..c23d461da50 100644 --- a/homeassistant/components/upnp/translations/nl.json +++ b/homeassistant/components/upnp/translations/nl.json @@ -9,7 +9,7 @@ "one": "Een", "other": "Ander" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "init": { "one": "Leeg", diff --git a/homeassistant/components/upnp/translations/pl.json b/homeassistant/components/upnp/translations/pl.json index 4f519323fd0..3f66ec7c6f9 100644 --- a/homeassistant/components/upnp/translations/pl.json +++ b/homeassistant/components/upnp/translations/pl.json @@ -11,7 +11,7 @@ "one": "jeden", "other": "inne" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "init": { "few": "kilka", diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index e5d9c5a7bb8..28cb0d2c0b2 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "complete_pairing_failed": "Das Pairing konnte nicht abgeschlossen werden. Vergewissere dich, dass der eingegebene PIN korrekt ist und dass der Fernseher noch mit Strom versorgt wird und mit dem Netzwerk verbunden ist, bevor du es erneut versuchst.", - "existing_config_entry_found": "Ein bestehender Richten Sie das VIZIO SmartCast-Ger\u00e4t ein Config-Eintrag mit der gleichen Seriennummer wurde bereits konfiguriert. Sie m\u00fcssen den vorhandenen Eintrag l\u00f6schen, um diesen zu konfigurieren." + "existing_config_entry_found": "Ein bestehender VIZIO SmartCast-Ger\u00e4t Config-Eintrag mit der gleichen Seriennummer wurde bereits konfiguriert. Sie m\u00fcssen den vorhandenen Eintrag l\u00f6schen, um diesen zu konfigurieren." }, "step": { "pair_tv": { @@ -19,11 +19,11 @@ "title": "Schlie\u00dfen Sie den Pairing-Prozess ab" }, "pairing_complete": { - "description": "Dein Richten Sie das VIZIO SmartCast-Ger\u00e4t ein ist jetzt mit Home Assistant verbunden.", + "description": "Dein VIZIO SmartCast-Ger\u00e4t ist jetzt mit Home Assistant verbunden.", "title": "Kopplung abgeschlossen" }, "pairing_complete_import": { - "description": "Dein Richten Sie das VIZIO SmartCast-Ger\u00e4t ein ist jetzt mit Home Assistant verbunden.\n\nDein Zugangstoken ist '**{access_token}**'.", + "description": "Dein VIZIO SmartCast-Ger\u00e4t ist jetzt mit Home Assistant verbunden.\n\nDein Zugangstoken ist '**{access_token}**'.", "title": "Kopplung abgeschlossen" }, "user": { @@ -34,7 +34,7 @@ "name": "Name" }, "description": "Ein Zugangstoken wird nur f\u00fcr Fernsehger\u00e4te ben\u00f6tigt. Wenn du ein Fernsehger\u00e4t konfigurierst und noch kein Zugangstoken hast, lass es leer, um einen Pairing-Vorgang durchzuf\u00fchren.", - "title": "Richten Sie das VIZIO SmartCast-Ger\u00e4t ein" + "title": "VIZIO SmartCast-Ger\u00e4t" } } }, @@ -47,7 +47,7 @@ "volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe" }, "description": "Wenn Sie \u00fcber ein Smart-TV-Ger\u00e4t verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen.", - "title": "Aktualisiere die Richten Sie das VIZIO SmartCast-Ger\u00e4t ein-Optionen" + "title": "Aktualisiere die VIZIO SmartCast-Ger\u00e4t-Optionen" } } } diff --git a/homeassistant/components/vizio/translations/nl.json b/homeassistant/components/vizio/translations/nl.json index 48a7a7d353d..f17e0470799 100644 --- a/homeassistant/components/vizio/translations/nl.json +++ b/homeassistant/components/vizio/translations/nl.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Verbinding mislukt", "complete_pairing_failed": "Kan het koppelen niet voltooien. Zorg ervoor dat de door u opgegeven pincode correct is en dat de tv nog steeds van stroom wordt voorzien en is verbonden met het netwerk voordat u opnieuw verzendt.", - "existing_config_entry_found": "Een bestaande VIZIO SmartCast-apparaat config entry met hetzelfde serienummer is reeds geconfigureerd. U moet de bestaande invoer verwijderen om deze te kunnen configureren." + "existing_config_entry_found": "Een bestaande VIZIO SmartCast-apparaat config entry met hetzelfde serienummer is reeds geconfigureerd. U moet de bestaande entry verwijderen om deze te kunnen configureren." }, "step": { "pair_tv": { diff --git a/homeassistant/components/wilight/translations/nl.json b/homeassistant/components/wilight/translations/nl.json index c04105e0878..c2820f0ed6e 100644 --- a/homeassistant/components/wilight/translations/nl.json +++ b/homeassistant/components/wilight/translations/nl.json @@ -5,7 +5,7 @@ "not_supported_device": "Deze WiLight wordt momenteel niet ondersteund", "not_wilight_device": "Dit apparaat is geen WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Wil je WiLight {name} ? \n\n Het ondersteunt: {components}", diff --git a/homeassistant/components/wilight/translations/pl.json b/homeassistant/components/wilight/translations/pl.json index 45c3d1f8990..93957d016f5 100644 --- a/homeassistant/components/wilight/translations/pl.json +++ b/homeassistant/components/wilight/translations/pl.json @@ -5,7 +5,7 @@ "not_supported_device": "Ten WiLight nie jest obecnie obs\u0142ugiwany", "not_wilight_device": "To urz\u0105dzenie nie jest urz\u0105dzeniem WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Czy chcesz skonfigurowa\u0107 WiLight {name}?\n\nObs\u0142uguje: {components}", diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json index b4bd6a0c449..7d0aead7a66 100644 --- a/homeassistant/components/withings/translations/de.json +++ b/homeassistant/components/withings/translations/de.json @@ -21,7 +21,7 @@ "data": { "profile": "Profilname" }, - "description": "Welches Profil hast du auf der Withings-Website ausgew\u00e4hlt? Es ist wichtig, dass die Profile \u00fcbereinstimmen, da sonst die Daten falsch beschriftet werden.", + "description": "Gib einen eindeutigen Profilnamen f\u00fcr diese Daten an. Normalerweise ist dies der Name des Profils, das du im vorherigen Schritt ausgew\u00e4hlt hast.", "title": "Benutzerprofil" }, "reauth": { diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index d5a8d623349..a7400e3a693 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Account is al geconfigureerd" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Kies een authenticatie methode" diff --git a/homeassistant/components/withings/translations/pl.json b/homeassistant/components/withings/translations/pl.json index 0eeac7899ae..638171af846 100644 --- a/homeassistant/components/withings/translations/pl.json +++ b/homeassistant/components/withings/translations/pl.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Konto jest ju\u017c skonfigurowane" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json index 3e7b16a7f4a..a06d49c8902 100644 --- a/homeassistant/components/wled/translations/nl.json +++ b/homeassistant/components/wled/translations/nl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/pl.json b/homeassistant/components/wled/translations/pl.json index 6552b6de239..423c30d1fe8 100644 --- a/homeassistant/components/wled/translations/pl.json +++ b/homeassistant/components/wled/translations/pl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/nl.json b/homeassistant/components/xiaomi_aqara/translations/nl.json index 45a531249a4..9ef660a8e7f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/nl.json +++ b/homeassistant/components/xiaomi_aqara/translations/nl.json @@ -12,7 +12,7 @@ "invalid_key": "Ongeldige gatewaysleutel", "invalid_mac": "Ongeldig MAC-adres" }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/pl.json b/homeassistant/components/xiaomi_aqara/translations/pl.json index 28df744d8e7..da8a803d81f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pl.json +++ b/homeassistant/components/xiaomi_aqara/translations/pl.json @@ -12,7 +12,7 @@ "invalid_key": "Nieprawid\u0142owy klucz bramki", "invalid_mac": "Nieprawid\u0142owy adres MAC" }, - "flow_title": "Bramka Xiaomi Aqara: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index 394d43fc261..012b976bea3 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -9,7 +9,7 @@ "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft", "unknown_device": "Het apparaatmodel is niet bekend, niet in staat om het apparaat in te stellen met config flow." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index 8b7105b6736..a7c01ef346e 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -9,7 +9,7 @@ "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie", "unknown_device": "Model urz\u0105dzenia nie jest znany, nie mo\u017cna skonfigurowa\u0107 urz\u0105dzenia przy u\u017cyciu interfejsu u\u017cytkownika." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/yeelight/translations/ca.json b/homeassistant/components/yeelight/translations/ca.json index 77fe2b49c71..9bdbd01bfca 100644 --- a/homeassistant/components/yeelight/translations/ca.json +++ b/homeassistant/components/yeelight/translations/ca.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Vols configurar {model} ({host})?" + }, "pick_device": { "data": { "device": "Dispositiu" diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json index 6df9b3d09b2..1a8f9a463b7 100644 --- a/homeassistant/components/yeelight/translations/de.json +++ b/homeassistant/components/yeelight/translations/de.json @@ -8,6 +8,9 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { + "discovery_confirm": { + "description": "M\u00f6chten Sie {model} ({host}) einrichten?" + }, "pick_device": { "data": { "device": "Ger\u00e4te" diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json index d633a885fda..044a10c695d 100644 --- a/homeassistant/components/yeelight/translations/es.json +++ b/homeassistant/components/yeelight/translations/es.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "No se pudo conectar" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "\u00bfQuieres configurar {model} ({host})?" + }, "pick_device": { "data": { "device": "Dispositivo" diff --git a/homeassistant/components/yeelight/translations/et.json b/homeassistant/components/yeelight/translations/et.json index 55cdf2e0b28..450b85b03cd 100644 --- a/homeassistant/components/yeelight/translations/et.json +++ b/homeassistant/components/yeelight/translations/et.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Kas seadistada {model} ({host})?" + }, "pick_device": { "data": { "device": "Seade" diff --git a/homeassistant/components/yeelight/translations/nl.json b/homeassistant/components/yeelight/translations/nl.json index 7dd509cca06..6aecb4f0bd9 100644 --- a/homeassistant/components/yeelight/translations/nl.json +++ b/homeassistant/components/yeelight/translations/nl.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Kon niet verbinden" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Wilt u {model} ({host}) instellen?" + }, "pick_device": { "data": { "device": "Apparaat" diff --git a/homeassistant/components/yeelight/translations/no.json b/homeassistant/components/yeelight/translations/no.json index 5d3107b779f..bbfe545e919 100644 --- a/homeassistant/components/yeelight/translations/no.json +++ b/homeassistant/components/yeelight/translations/no.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Vil du sette opp {model} ( {host} )?" + }, "pick_device": { "data": { "device": "Enhet" diff --git a/homeassistant/components/yeelight/translations/pl.json b/homeassistant/components/yeelight/translations/pl.json index 574f8303a4c..9ea693fffcc 100644 --- a/homeassistant/components/yeelight/translations/pl.json +++ b/homeassistant/components/yeelight/translations/pl.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?" + }, "pick_device": { "data": { "device": "Urz\u0105dzenie" diff --git a/homeassistant/components/yeelight/translations/ru.json b/homeassistant/components/yeelight/translations/ru.json index 221fa3b4738..cbeaad534b4 100644 --- a/homeassistant/components/yeelight/translations/ru.json +++ b/homeassistant/components/yeelight/translations/ru.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {model} ({host})?" + }, "pick_device": { "data": { "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json index fe21b9e535b..b5df81beafd 100644 --- a/homeassistant/components/yeelight/translations/zh-Hant.json +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {model} ({host})\uff1f" + }, "pick_device": { "data": { "device": "\u88dd\u7f6e" diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index 0bad254d00e..86a3f7a7f69 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index f9b34d1be82..dce671771f3 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Kod wymagany do akcji uzbrajania", + "alarm_failed_tries": "Liczba kolejnych nieudanych wpis\u00f3w kodu uruchamiaj\u0105ca alarm", + "alarm_master_code": "Kod g\u0142\u00f3wny panelu (paneli) alarmowego", + "title": "Opcje panelu alarmowego" + }, + "zha_options": { + "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", + "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", + "title": "Opcje og\u00f3lne" + } + }, "device_automation": { "action_type": { "squawk": "squawk", From 1b74359ddbec997a1827499522c65c25a924a00e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 16 May 2021 23:12:23 -0700 Subject: [PATCH 494/852] Disable free-mobile because requirement breaks setuptools (#50749) --- .github/workflows/ci.yaml | 4 ++-- homeassistant/components/free_mobile/manifest.json | 3 ++- homeassistant/components/free_mobile/notify.py | 2 +- requirements_all.txt | 3 --- script/hassfest/model.py | 2 +- script/hassfest/requirements.py | 3 +++ 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a68aaa549c6..fe14e44beed 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -56,7 +56,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U "pip<20.3" "setuptools<56.2" + pip install -U "pip<20.3" setuptools pip install -r requirements.txt -r requirements_test.txt - name: Generate partial pre-commit restore key id: generate-pre-commit-key @@ -580,7 +580,7 @@ jobs: python -m venv venv . venv/bin/activate - pip install -U "pip<20.3" "setuptools<56.2" wheel + pip install -U "pip<20.3" setuptools wheel pip install -r requirements_all.txt pip install -r requirements_test.txt pip install -e . diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index ea6ea921a38..89a21298a38 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/free_mobile", "requirements": ["freesms==0.1.2"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "disabled": "https://github.com/home-assistant/core/pull/50749" } diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index a4351bfe678..2cde2dd135e 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -1,7 +1,7 @@ """Support for Free Mobile SMS platform.""" import logging -from freesms import FreeClient +from freesms import FreeClient # pylint: disable=import-error import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService diff --git a/requirements_all.txt b/requirements_all.txt index 8000472f721..c61196e20c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -623,9 +623,6 @@ fortiosapi==0.10.8 # homeassistant.components.freebox freebox-api==0.0.10 -# homeassistant.components.free_mobile -freesms==0.1.2 - # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 10bc10626a2..59d75be5c4a 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -83,7 +83,7 @@ class Integration: @property def disabled(self) -> str | None: - """List of disabled.""" + """Return if integration is disabled.""" return self.manifest.get("disabled") @property diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index fa5a36c6559..5927824b21f 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -95,6 +95,9 @@ def validate_requirements(integration: Integration): integration_requirements.add(req) integration_packages.add(package) + if integration.disabled: + return + install_ok = install_requirements(integration, integration_requirements) if not install_ok: From 120bf8aed7a09083bcc16aa99b15b43dc92fcbca Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 17 May 2021 08:28:22 +0200 Subject: [PATCH 495/852] fix annotation in actiontec (#50727) --- homeassistant/components/actiontec/device_tracker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index 3783ad881e2..d7e6f5be494 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -31,9 +31,7 @@ PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( ) -def get_scanner( - hass: HomeAssistant, config: ConfigType -) -> ActiontecDeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: """Validate the configuration and return an Actiontec scanner.""" scanner = ActiontecDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None From 663c0374ab2fd38832963a2dc7aaf9eb7e279fd1 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Mon, 17 May 2021 09:12:04 +0200 Subject: [PATCH 496/852] Add full typing to kraken (#50718) * Add full typing to kraken * Let device_info return DeviceInfo * Replace unsub_listeners with entry.async_on_unload * Raise TypeError on end of _try_get_state * Assert Coordinator is not none * Add class SensorType * Add strict typing to kraken * Add changes from code review * Revert typed dict creation --- .strict-typing | 1 + homeassistant/components/kraken/__init__.py | 20 +-- .../components/kraken/config_flow.py | 18 ++- homeassistant/components/kraken/const.py | 16 +- homeassistant/components/kraken/sensor.py | 147 ++++++++++-------- mypy.ini | 14 +- script/hassfest/mypy_config.py | 1 - 7 files changed, 137 insertions(+), 80 deletions(-) diff --git a/.strict-typing b/.strict-typing index 28980c17903..f956ca2f964 100644 --- a/.strict-typing +++ b/.strict-typing @@ -29,6 +29,7 @@ homeassistant.components.hyperion.* homeassistant.components.image_processing.* homeassistant.components.integration.* homeassistant.components.knx.* +homeassistant.components.kraken.* homeassistant.components.light.* homeassistant.components.lock.* homeassistant.components.mailbox.* diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index 057156005e7..d52e0712a0b 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -21,6 +21,7 @@ from .const import ( DEFAULT_TRACKED_ASSET_PAIR, DISPATCH_CONFIG_UPDATED, DOMAIN, + KrakenResponse, ) from .utils import get_tradable_asset_pairs @@ -47,8 +48,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_entry, PLATFORMS ) if unload_ok: - for unsub_listener in hass.data[DOMAIN].unsub_listeners: - unsub_listener() hass.data.pop(DOMAIN) return unload_ok @@ -62,11 +61,10 @@ class KrakenData: self._hass = hass self._config_entry = config_entry self._api = pykrakenapi.KrakenAPI(krakenex.API(), retry=0, crl_sleep=0) - self.tradable_asset_pairs = None - self.coordinator = None - self.unsub_listeners = [] + self.tradable_asset_pairs: dict[str, str] = {} + self.coordinator: DataUpdateCoordinator[KrakenResponse | None] | None = None - async def async_update(self) -> None: + async def async_update(self) -> KrakenResponse | None: """Get the latest data from the Kraken.com REST API. All tradeable asset pairs are retrieved, not the tracked asset pairs @@ -91,8 +89,9 @@ class KrakenData: _LOGGER.warning( "Exceeded the Kraken.com call rate limit. Increase the update interval to prevent this error" ) + return None - def _get_kraken_data(self) -> dict: + def _get_kraken_data(self) -> KrakenResponse: websocket_name_pairs = self._get_websocket_name_asset_pairs() ticker_df = self._api.get_ticker_information(websocket_name_pairs) # Rename columns to their full name @@ -109,7 +108,7 @@ class KrakenData: "o": "opening_price", } ) - response_dict = ticker_df.transpose().to_dict() + response_dict: KrakenResponse = ticker_df.transpose().to_dict() return response_dict async def _async_refresh_tradable_asset_pairs(self) -> None: @@ -140,12 +139,13 @@ class KrakenData: ) await self.coordinator.async_config_entry_first_refresh() - def _get_websocket_name_asset_pairs(self) -> list: + def _get_websocket_name_asset_pairs(self) -> str: return ",".join(wsname for wsname in self.tradable_asset_pairs.values()) def set_update_interval(self, update_interval: int) -> None: """Set the coordinator update_interval to the supplied update_interval.""" - self.coordinator.update_interval = timedelta(seconds=update_interval) + if self.coordinator is not None: + self.coordinator.update_interval = timedelta(seconds=update_interval) async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None: diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py index 87ab2262029..a34bf78557e 100644 --- a/homeassistant/components/kraken/config_flow.py +++ b/homeassistant/components/kraken/config_flow.py @@ -1,5 +1,8 @@ """Config flow for kraken integration.""" +from __future__ import annotations + import logging +from typing import Any import krakenex from pykrakenapi.pykrakenapi import KrakenAPI @@ -8,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import CONF_TRACKED_ASSET_PAIRS, DEFAULT_SCAN_INTERVAL, DOMAIN @@ -24,11 +28,15 @@ class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> KrakenOptionsFlowHandler: """Get the options flow for this handler.""" return KrakenOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" if DOMAIN in self.hass.data: return self.async_abort(reason="already_configured") @@ -44,11 +52,13 @@ class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class KrakenOptionsFlowHandler(config_entries.OptionsFlow): """Handle Kraken client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Kraken options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Kraken options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/kraken/const.py b/homeassistant/components/kraken/const.py index fb3d0aa4dc4..2272d12ead6 100644 --- a/homeassistant/components/kraken/const.py +++ b/homeassistant/components/kraken/const.py @@ -1,5 +1,19 @@ """Constants for the kraken integration.""" +from __future__ import annotations + +from typing import Dict, TypedDict + +KrakenResponse = Dict[str, Dict[str, float]] + + +class SensorType(TypedDict): + """SensorType class.""" + + name: str + enabled_by_default: bool + + DEFAULT_SCAN_INTERVAL = 60 DEFAULT_TRACKED_ASSET_PAIR = "XBT/USD" DISPATCH_CONFIG_UPDATED = "kraken_config_updated" @@ -8,7 +22,7 @@ CONF_TRACKED_ASSET_PAIRS = "tracked_asset_pairs" DOMAIN = "kraken" -SENSOR_TYPES = [ +SENSOR_TYPES: list[SensorType] = [ {"name": "ask", "enabled_by_default": True}, {"name": "ask_volume", "enabled_by_default": False}, {"name": "bid", "enabled_by_default": True}, diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index 1fab821f5dc..bc0a0a21845 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -8,6 +8,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import KrakenData @@ -16,12 +19,17 @@ from .const import ( DISPATCH_CONFIG_UPDATED, DOMAIN, SENSOR_TYPES, + SensorType, ) _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Add kraken entities from a config_entry.""" @callback @@ -59,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_update_sensors(hass, config_entry) - hass.data[DOMAIN].unsub_listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, DISPATCH_CONFIG_UPDATED, @@ -75,9 +83,10 @@ class KrakenSensor(CoordinatorEntity, SensorEntity): self, kraken_data: KrakenData, tracked_asset_pair: str, - sensor_type: dict[str, bool], + sensor_type: SensorType, ) -> None: """Initialize.""" + assert kraken_data.coordinator is not None super().__init__(kraken_data.coordinator) self.tracked_asset_pair_wsname = kraken_data.tradable_asset_pairs[ tracked_asset_pair @@ -100,22 +109,22 @@ class KrakenSensor(CoordinatorEntity, SensorEntity): self._state = None @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 self._enabled_by_default @property - def name(self): + def name(self) -> str: """Return the name.""" return self._name @property - def unique_id(self): + def unique_id(self) -> str: """Set unique_id for sensor.""" return self._name.lower() @property - def state(self): + def state(self) -> StateType: """Return the state.""" return self._state @@ -124,13 +133,76 @@ class KrakenSensor(CoordinatorEntity, SensorEntity): await super().async_added_to_hass() self._update_internal_state() - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: self._update_internal_state() super()._handle_coordinator_update() - def _update_internal_state(self): + def _update_internal_state(self) -> None: try: - self._state = self._try_get_state() + if self._sensor_type == "last_trade_closed": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "last_trade_closed" + ][0] + if self._sensor_type == "ask": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "ask" + ][0] + if self._sensor_type == "ask_volume": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "ask" + ][1] + if self._sensor_type == "bid": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "bid" + ][0] + if self._sensor_type == "bid_volume": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "bid" + ][1] + if self._sensor_type == "volume_today": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "volume" + ][0] + if self._sensor_type == "volume_last_24h": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "volume" + ][1] + if self._sensor_type == "volume_weighted_average_today": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "volume_weighted_average" + ][0] + if self._sensor_type == "volume_weighted_average_last_24h": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "volume_weighted_average" + ][1] + if self._sensor_type == "number_of_trades_today": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "number_of_trades" + ][0] + if self._sensor_type == "number_of_trades_last_24h": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "number_of_trades" + ][1] + if self._sensor_type == "low_today": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "low" + ][0] + if self._sensor_type == "low_last_24h": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "low" + ][1] + if self._sensor_type == "high_today": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "high" + ][0] + if self._sensor_type == "high_last_24h": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "high" + ][1] + if self._sensor_type == "opening_price_today": + self._state = self.coordinator.data[self.tracked_asset_pair_wsname][ + "opening_price" + ] self._received_data_at_least_once = True # Received data at least one time. except TypeError: if self._received_data_at_least_once: @@ -141,55 +213,8 @@ class KrakenSensor(CoordinatorEntity, SensorEntity): ) self._available = False - def _try_get_state(self) -> str: - """Try to get the state or return a TypeError.""" - if self._sensor_type == "last_trade_closed": - return self.coordinator.data[self.tracked_asset_pair_wsname][ - "last_trade_closed" - ][0] - if self._sensor_type == "ask": - return self.coordinator.data[self.tracked_asset_pair_wsname]["ask"][0] - if self._sensor_type == "ask_volume": - return self.coordinator.data[self.tracked_asset_pair_wsname]["ask"][1] - if self._sensor_type == "bid": - return self.coordinator.data[self.tracked_asset_pair_wsname]["bid"][0] - if self._sensor_type == "bid_volume": - return self.coordinator.data[self.tracked_asset_pair_wsname]["bid"][1] - if self._sensor_type == "volume_today": - return self.coordinator.data[self.tracked_asset_pair_wsname]["volume"][0] - if self._sensor_type == "volume_last_24h": - return self.coordinator.data[self.tracked_asset_pair_wsname]["volume"][1] - if self._sensor_type == "volume_weighted_average_today": - return self.coordinator.data[self.tracked_asset_pair_wsname][ - "volume_weighted_average" - ][0] - if self._sensor_type == "volume_weighted_average_last_24h": - return self.coordinator.data[self.tracked_asset_pair_wsname][ - "volume_weighted_average" - ][1] - if self._sensor_type == "number_of_trades_today": - return self.coordinator.data[self.tracked_asset_pair_wsname][ - "number_of_trades" - ][0] - if self._sensor_type == "number_of_trades_last_24h": - return self.coordinator.data[self.tracked_asset_pair_wsname][ - "number_of_trades" - ][1] - if self._sensor_type == "low_today": - return self.coordinator.data[self.tracked_asset_pair_wsname]["low"][0] - if self._sensor_type == "low_last_24h": - return self.coordinator.data[self.tracked_asset_pair_wsname]["low"][1] - if self._sensor_type == "high_today": - return self.coordinator.data[self.tracked_asset_pair_wsname]["high"][0] - if self._sensor_type == "high_last_24h": - return self.coordinator.data[self.tracked_asset_pair_wsname]["high"][1] - if self._sensor_type == "opening_price_today": - return self.coordinator.data[self.tracked_asset_pair_wsname][ - "opening_price" - ] - @property - def icon(self): + def icon(self) -> str: """Return the icon.""" if self._target_asset == "EUR": return "mdi:currency-eur" @@ -204,19 +229,19 @@ class KrakenSensor(CoordinatorEntity, SensorEntity): return "mdi:cash" @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if "number_of" not in self._sensor_type: return self._unit_of_measurement return None @property - def available(self): + def available(self) -> bool: """Could the api be accessed during the last update call.""" return self._available and self.coordinator.last_update_success @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return { diff --git a/mypy.ini b/mypy.ini index ee53f990d78..24dcab95bed 100644 --- a/mypy.ini +++ b/mypy.ini @@ -330,6 +330,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.kraken.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.light.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -982,9 +993,6 @@ ignore_errors = true [mypy-homeassistant.components.kostal_plenticore.*] ignore_errors = true -[mypy-homeassistant.components.kraken.*] -ignore_errors = true - [mypy-homeassistant.components.kulersky.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d6f731c803e..dd9e6c521fa 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -114,7 +114,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.kodi.*", "homeassistant.components.konnected.*", "homeassistant.components.kostal_plenticore.*", - "homeassistant.components.kraken.*", "homeassistant.components.kulersky.*", "homeassistant.components.lifx.*", "homeassistant.components.litejet.*", From 5ea2dd8ce373e5d889d3b30cc1de75432b3d76de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 May 2021 00:26:37 -0700 Subject: [PATCH 497/852] Alexa: Set Equalizer property to retrievable (#50730) --- homeassistant/components/alexa/capabilities.py | 4 ++++ tests/components/alexa/test_smart_home.py | 1 + 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 69acf95e207..1afe65b7bc6 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1920,6 +1920,10 @@ class AlexaEqualizerController(AlexaCapability): """ return [{"name": "mode"}] + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + def get_property(self, name): """Read and return a property.""" if name != "mode": diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index ab884745e95..83abe2326d7 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -3264,6 +3264,7 @@ async def test_media_player_eq_modes(hass): eq_capability = get_capability(capabilities, "Alexa.EqualizerController") assert eq_capability is not None + assert eq_capability["properties"]["retrievable"] assert "modes" in eq_capability["configurations"] eq_modes = eq_capability["configurations"]["modes"] From 636528dd2e6f293fd4db28354fae534464f734fc Mon Sep 17 00:00:00 2001 From: David Nielsen Date: Mon, 17 May 2021 03:37:13 -0400 Subject: [PATCH 498/852] Update bravia-tv to 1.0.11 (#50726) --- homeassistant/components/braviatv/manifest.json | 2 +- homeassistant/components/braviatv/media_player.py | 10 ++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index c3fcf218e9a..f7456c08c13 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["bravia-tv==1.0.8"], + "requirements": ["bravia-tv==1.0.11"], "codeowners": ["@bieniu"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 32a051f4e98..14b47f95101 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -141,6 +141,7 @@ class BraviaTVDevice(MediaPlayerEntity): self._playing = False self._start_date_time = None self._program_media_type = None + self._audio_output = None self._min_volume = None self._max_volume = None self._volume = None @@ -191,9 +192,10 @@ class BraviaTVDevice(MediaPlayerEntity): async def _async_refresh_volume(self): """Refresh volume information.""" volume_info = await self.hass.async_add_executor_job( - self._braviarc.get_volume_info + self._braviarc.get_volume_info, self._audio_output ) if volume_info is not None: + self._audio_output = volume_info.get("target") self._volume = volume_info.get("volume") self._min_volume = volume_info.get("minVolume") self._max_volume = volume_info.get("maxVolume") @@ -305,7 +307,7 @@ class BraviaTVDevice(MediaPlayerEntity): def set_volume_level(self, volume): """Set volume level, range 0..1.""" - self._braviarc.set_volume_level(volume) + self._braviarc.set_volume_level(volume, self._audio_output) async def async_turn_on(self): """Turn the media player on.""" @@ -319,11 +321,11 @@ class BraviaTVDevice(MediaPlayerEntity): def volume_up(self): """Volume up the media player.""" - self._braviarc.volume_up() + self._braviarc.volume_up(self._audio_output) def volume_down(self): """Volume down media player.""" - self._braviarc.volume_down() + self._braviarc.volume_down(self._audio_output) def mute_volume(self, mute): """Send mute command.""" diff --git a/requirements_all.txt b/requirements_all.txt index c61196e20c1..92f89acba78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -390,7 +390,7 @@ boschshcpy==0.2.17 boto3==1.16.52 # homeassistant.components.braviatv -bravia-tv==1.0.8 +bravia-tv==1.0.11 # homeassistant.components.broadlink broadlink==0.17.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54d6e3853c1..384f3460465 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -223,7 +223,7 @@ bond-api==0.1.12 boschshcpy==0.2.17 # homeassistant.components.braviatv -bravia-tv==1.0.8 +bravia-tv==1.0.11 # homeassistant.components.broadlink broadlink==0.17.0 From a414cad3b2a7eef9a478e32bae8b8957fbb3c7cc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 17 May 2021 10:46:37 +0200 Subject: [PATCH 499/852] Upgrade aiodns to 3.0.0 (#50712) --- homeassistant/components/dnsip/manifest.json | 2 +- homeassistant/components/minecraft_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 2254314804b..2a277c3ceeb 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -2,7 +2,7 @@ "domain": "dnsip", "name": "DNS IP", "documentation": "https://www.home-assistant.io/integrations/dnsip", - "requirements": ["aiodns==2.0.0"], + "requirements": ["aiodns==3.0.0"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json index 61860fb163a..0c8df177fec 100644 --- a/homeassistant/components/minecraft_server/manifest.json +++ b/homeassistant/components/minecraft_server/manifest.json @@ -3,7 +3,7 @@ "name": "Minecraft Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/minecraft_server", - "requirements": ["aiodns==2.0.0", "getmac==0.8.2", "mcstatus==5.1.1"], + "requirements": ["aiodns==3.0.0", "getmac==0.8.2", "mcstatus==5.1.1"], "codeowners": ["@elmurato"], "quality_scale": "silver", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 92f89acba78..2053096d1e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -151,7 +151,7 @@ aiodiscover==1.4.0 # homeassistant.components.dnsip # homeassistant.components.minecraft_server -aiodns==2.0.0 +aiodns==3.0.0 # homeassistant.components.eafm aioeafm==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 384f3460465..e0a8aab00dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aiodiscover==1.4.0 # homeassistant.components.dnsip # homeassistant.components.minecraft_server -aiodns==2.0.0 +aiodns==3.0.0 # homeassistant.components.eafm aioeafm==0.1.2 From 059e7c925d7d9e8af14e7ea4669b49ca24a110ef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 17 May 2021 10:46:58 +0200 Subject: [PATCH 500/852] Remove side effects from Watson TTS init (#50716) --- homeassistant/components/watson_tts/tts.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 62ddc917ce9..eeab72b73d0 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -147,6 +147,12 @@ def get_engine(hass, config, discovery_info=None): output_format = config[CONF_OUTPUT_FORMAT] service.set_default_headers({"x-watson-learning-opt-out": "true"}) + if default_voice in DEPRECATED_VOICES: + _LOGGER.warning( + "Watson TTS voice %s is deprecated, it may be removed in the future", + default_voice, + ) + return WatsonTTSProvider(service, supported_languages, default_voice, output_format) @@ -162,12 +168,6 @@ class WatsonTTSProvider(Provider): self.output_format = output_format self.name = "Watson TTS" - if self.default_voice in DEPRECATED_VOICES: - _LOGGER.warning( - "Watson TTS voice %s is deprecated, it may be removed in the future", - self.default_voice, - ) - @property def supported_languages(self): """Return a list of supported languages.""" From 4357d2dc84f1172f0ff4d054a7952866abf380e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 17 May 2021 11:07:53 +0200 Subject: [PATCH 501/852] Update AEMET library to latest version (#50222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index 26f9139aa9e..8f33e9dbf03 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -3,7 +3,7 @@ "name": "AEMET OpenData", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aemet", - "requirements": ["AEMET-OpenData==0.1.8"], + "requirements": ["AEMET-OpenData==0.2.1"], "codeowners": ["@noltari"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 2053096d1e1..d21e9c5b6b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.1.8 +AEMET-OpenData==0.2.1 # homeassistant.components.sht31 Adafruit-GPIO==1.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0a8aab00dc..ee397276f03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.1.8 +AEMET-OpenData==0.2.1 # homeassistant.components.homekit HAP-python==3.4.1 From 2d29959a521684f43a6bbeeac7daf3712bc6a750 Mon Sep 17 00:00:00 2001 From: mountainsandcode Date: Mon, 17 May 2021 11:12:01 +0200 Subject: [PATCH 502/852] Add control of hardware buttons to Sonos (#49977) --- homeassistant/components/sonos/media_player.py | 6 ++++++ homeassistant/components/sonos/services.yaml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 051a9e29e81..06b9c49257a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -119,6 +119,7 @@ ATTR_ENABLED = "enabled" ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" ATTR_MASTER = "master" ATTR_WITH_GROUP = "with_group" +ATTR_BUTTONS_ENABLED = "buttons_enabled" ATTR_NIGHT_SOUND = "night_sound" ATTR_SPEECH_ENHANCE = "speech_enhance" ATTR_QUEUE_POSITION = "queue_position" @@ -229,6 +230,7 @@ async def async_setup_entry( platform.async_register_entity_service( # type: ignore SERVICE_SET_OPTION, { + vol.Optional(ATTR_BUTTONS_ENABLED): cv.boolean, vol.Optional(ATTR_NIGHT_SOUND): cv.boolean, vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean, vol.Optional(ATTR_STATUS_LIGHT): cv.boolean, @@ -605,11 +607,15 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): @soco_error() def set_option( self, + buttons_enabled: bool | None = None, night_sound: bool | None = None, speech_enhance: bool | None = None, status_light: bool | None = None, ) -> None: """Modify playback options.""" + if buttons_enabled is not None: + self.soco.buttons_enabled = buttons_enabled + if night_sound is not None and self.speaker.night_mode is not None: self.soco.night_mode = night_sound diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 0fee089d114..09197fb87ae 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -103,6 +103,12 @@ set_option: device: integration: sonos fields: + buttons_enabled: + name: Buttons enabled + description: Enable control buttons on the device + example: "true" + selector: + boolean: night_sound: name: Night sound description: Enable Night Sound mode From f9c7474a78707539cc9379319bdf9d97a2308c11 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 17 May 2021 11:14:47 +0200 Subject: [PATCH 503/852] Add strict type annotations to ampio (#50699) --- .strict-typing | 1 + homeassistant/components/ampio/air_quality.py | 51 ++++++++++++------- homeassistant/components/ampio/const.py | 7 +++ mypy.ini | 11 ++++ 4 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/ampio/const.py diff --git a/.strict-typing b/.strict-typing index f956ca2f964..12927c8c97b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -8,6 +8,7 @@ homeassistant.components.actiontec.* homeassistant.components.aftership.* homeassistant.components.airly.* homeassistant.components.aladdin_connect.* +homeassistant.components.ampio.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* homeassistant.components.bond.* diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py index c925909a9a8..f8119e9c1b4 100644 --- a/homeassistant/components/ampio/air_quality.py +++ b/homeassistant/components/ampio/air_quality.py @@ -1,28 +1,39 @@ """Support for Ampio Air Quality data.""" -from datetime import timedelta +from __future__ import annotations + import logging +from typing import Final from asmog import AmpioSmog import voluptuous as vol -from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + AirQualityEntity, +) from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle -_LOGGER = logging.getLogger(__name__) +from .const import ATTRIBUTION, CONF_STATION_ID, SCAN_INTERVAL -ATTRIBUTION = "Data provided by Ampio" -CONF_STATION_ID = "station_id" -SCAN_INTERVAL = timedelta(minutes=10) +_LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( {vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME): cv.string} ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the Ampio Smog air quality platform.""" name = config.get(CONF_NAME) @@ -43,38 +54,40 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class AmpioSmogQuality(AirQualityEntity): """Implementation of an Ampio Smog air quality entity.""" - def __init__(self, api, station_id, name): + def __init__( + self, api: AmpioSmogMapData, station_id: str, name: str | None + ) -> None: """Initialize the air quality entity.""" self._ampio = api self._station_id = station_id self._name = name or api.api.name @property - def name(self): + def name(self) -> str: """Return the name of the air quality entity.""" return self._name @property - def unique_id(self): + def unique_id(self) -> str: """Return unique_name.""" return f"ampio_smog_{self._station_id}" @property - def particulate_matter_2_5(self): + def particulate_matter_2_5(self) -> str | None: """Return the particulate matter 2.5 level.""" - return self._ampio.api.pm2_5 + return self._ampio.api.pm2_5 # type: ignore[no-any-return] @property - def particulate_matter_10(self): + def particulate_matter_10(self) -> str | None: """Return the particulate matter 10 level.""" - return self._ampio.api.pm10 + return self._ampio.api.pm10 # type: ignore[no-any-return] @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return ATTRIBUTION - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from the AmpioMap API.""" await self._ampio.async_update() @@ -82,11 +95,11 @@ class AmpioSmogQuality(AirQualityEntity): class AmpioSmogMapData: """Get the latest data and update the states.""" - def __init__(self, api): + def __init__(self, api: AmpioSmog) -> None: """Initialize the data object.""" self.api = api @Throttle(SCAN_INTERVAL) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from AmpioMap.""" await self.api.get_data() diff --git a/homeassistant/components/ampio/const.py b/homeassistant/components/ampio/const.py new file mode 100644 index 00000000000..3162308ff41 --- /dev/null +++ b/homeassistant/components/ampio/const.py @@ -0,0 +1,7 @@ +"""Constants for Ampio Air Quality platform.""" +from datetime import timedelta +from typing import Final + +ATTRIBUTION: Final = "Data provided by Ampio" +CONF_STATION_ID: Final = "station_id" +SCAN_INTERVAL: Final = timedelta(minutes=10) diff --git a/mypy.ini b/mypy.ini index 24dcab95bed..396c51c8ac6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -99,6 +99,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ampio.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.automation.*] check_untyped_defs = true disallow_incomplete_defs = true From 7b18860dcd0e9757db4dd7fb6d523f3b9af68ca9 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 17 May 2021 11:18:13 +0200 Subject: [PATCH 504/852] Update xknx to version 0.18.2 (#50491) * xknx 0.18.2 * individual colors without switch * make `setpoint_shift_mode` optional * Update homeassistant/components/knx/schema.py --- homeassistant/components/knx/factory.py | 2 +- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/schema.py | 47 +++++++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 7c4b7186075..702b6d4f3c1 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -130,7 +130,7 @@ def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: group_address_setpoint_shift_state=config.get( ClimateSchema.CONF_SETPOINT_SHIFT_STATE_ADDRESS ), - setpoint_shift_mode=config[ClimateSchema.CONF_SETPOINT_SHIFT_MODE], + setpoint_shift_mode=config.get(ClimateSchema.CONF_SETPOINT_SHIFT_MODE), setpoint_shift_max=config[ClimateSchema.CONF_SETPOINT_SHIFT_MAX], setpoint_shift_min=config[ClimateSchema.CONF_SETPOINT_SHIFT_MIN], temperature_step=config[ClimateSchema.CONF_TEMPERATURE_STEP], diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index bcca5855bf1..0a722d46162 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.1"], + "requirements": ["xknx==0.18.2"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index dddcabc767b..2404ab37ed3 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -174,9 +174,6 @@ class ClimateSchema: vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional( - CONF_SETPOINT_SHIFT_MODE, default=DEFAULT_SETPOINT_SHIFT_MODE - ): vol.All(vol.Upper, cv.enum(SetpointShiftMode)), vol.Optional( CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX ): vol.All(int, vol.Range(min=0, max=32)), @@ -189,8 +186,21 @@ class ClimateSchema: vol.Required(CONF_TEMPERATURE_ADDRESS): ga_list_validator, vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): ga_list_validator, - vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): ga_list_validator, - vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): ga_list_validator, + vol.Inclusive( + CONF_SETPOINT_SHIFT_ADDRESS, + "setpoint_shift", + msg="'setpoint_shift_address' and 'setpoint_shift_state_address' " + "are required for setpoint_shift configuration", + ): ga_list_validator, + vol.Inclusive( + CONF_SETPOINT_SHIFT_STATE_ADDRESS, + "setpoint_shift", + msg="'setpoint_shift_address' and 'setpoint_shift_state_address' " + "are required for setpoint_shift configuration", + ): ga_list_validator, + vol.Optional(CONF_SETPOINT_SHIFT_MODE): vol.Maybe( + vol.All(vol.Upper, cv.enum(SetpointShiftMode)) + ), vol.Optional(CONF_OPERATION_MODE_ADDRESS): ga_list_validator, vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): ga_list_validator, @@ -377,9 +387,21 @@ class LightSchema: vol.Optional(CONF_BRIGHTNESS_ADDRESS): ga_list_validator, vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_list_validator, vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): { - vol.Inclusive(CONF_RED, "colors"): COLOR_SCHEMA, - vol.Inclusive(CONF_GREEN, "colors"): COLOR_SCHEMA, - vol.Inclusive(CONF_BLUE, "colors"): COLOR_SCHEMA, + vol.Inclusive( + CONF_RED, + "individual_colors", + msg="'red', 'green' and 'blue' are required for individual colors configuration", + ): COLOR_SCHEMA, + vol.Inclusive( + CONF_GREEN, + "individual_colors", + msg="'red', 'green' and 'blue' are required for individual colors configuration", + ): COLOR_SCHEMA, + vol.Inclusive( + CONF_BLUE, + "individual_colors", + msg="'red', 'green' and 'blue' are required for individual colors configuration", + ): COLOR_SCHEMA, vol.Optional(CONF_WHITE): COLOR_SCHEMA, }, vol.Exclusive(CONF_COLOR_ADDRESS, "color"): ga_list_validator, @@ -400,14 +422,11 @@ class LightSchema: } ), vol.Any( - # either global "address" or all addresses for individual colors are required + # either global "address" or "individual_colors" is required vol.Schema( { - vol.Required(CONF_INDIVIDUAL_COLORS): { - vol.Required(CONF_RED): {vol.Required(KNX_ADDRESS): object}, - vol.Required(CONF_GREEN): {vol.Required(KNX_ADDRESS): object}, - vol.Required(CONF_BLUE): {vol.Required(KNX_ADDRESS): object}, - }, + # brightness addresses are required in COLOR_SCHEMA + vol.Required(CONF_INDIVIDUAL_COLORS): object, }, extra=vol.ALLOW_EXTRA, ), diff --git a/requirements_all.txt b/requirements_all.txt index d21e9c5b6b1..295667ed237 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2365,7 +2365,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.1 +xknx==0.18.2 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee397276f03..e8ac0657ab7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1271,7 +1271,7 @@ wolf_smartset==0.1.8 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.1 +xknx==0.18.2 # homeassistant.components.bluesound # homeassistant.components.rest From ff856a9bba1018edcf752a920a981bb4d3c7ef83 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 17 May 2021 11:20:12 +0200 Subject: [PATCH 505/852] Simplify calls to pymodbus (#50717) * simplify pymodbus_call. Do not call with a function object and a check attribute, call instead with CALL_TYPE*. Avoid if call x else call y. Call async_pymodbus_call directly, instead of unpacking/packing. * Declare call type in __init__. * Modbus.py back to 100% test coverage. --- .../components/modbus/binary_sensor.py | 9 +- homeassistant/components/modbus/const.py | 4 + homeassistant/components/modbus/modbus.py | 95 +++++++++++++++---- homeassistant/components/modbus/sensor.py | 11 +-- tests/components/modbus/test_modbus_switch.py | 7 +- 5 files changed, 92 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 14d01535c5b..3605b623c3c 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -151,12 +151,9 @@ class ModbusBinarySensor(BinarySensorEntity): """Update the state of the sensor.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval - if self._input_type == CALL_TYPE_COIL: - result = await self._hub.async_read_coils(self._slave, self._address, 1) - else: - result = await self._hub.async_read_discrete_inputs( - self._slave, self._address, 1 - ) + result = await self._hub.async_pymodbus_call( + self._slave, self._address, 1, self._input_type + ) if result is None: self._available = False self.async_write_ha_state() diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 15de884d5b9..ac32897e857 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -75,6 +75,10 @@ CALL_TYPE_COIL = "coil" CALL_TYPE_DISCRETE = "discrete_input" CALL_TYPE_REGISTER_HOLDING = "holding" CALL_TYPE_REGISTER_INPUT = "input" +CALL_TYPE_WRITE_COIL = "write_coil" +CALL_TYPE_WRITE_COILS = "write_coils" +CALL_TYPE_WRITE_REGISTER = "write_register" +CALL_TYPE_WRITE_REGISTERS = "write_registers" # service calls SERVICE_WRITE_COIL = "write_coil" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 0f4266654a7..5c44580e0df 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -27,6 +27,14 @@ from .const import ( ATTR_STATE, ATTR_UNIT, ATTR_VALUE, + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_WRITE_COIL, + CALL_TYPE_WRITE_COILS, + CALL_TYPE_WRITE_REGISTER, + CALL_TYPE_WRITE_REGISTERS, CONF_BAUDRATE, CONF_BYTESIZE, CONF_CLOSE_COMM_ON_ERROR, @@ -39,6 +47,9 @@ from .const import ( SERVICE_WRITE_REGISTER, ) +ENTRY_FUNC = "func" +ENTRY_ATTR = "attr" + _LOGGER = logging.getLogger(__name__) @@ -145,6 +156,41 @@ class ModbusHub: # network configuration self._config_host = client_config[CONF_HOST] + self._call_type = { + CALL_TYPE_COIL: { + ENTRY_ATTR: "bits", + ENTRY_FUNC: None, + }, + CALL_TYPE_DISCRETE: { + ENTRY_ATTR: "bits", + ENTRY_FUNC: None, + }, + CALL_TYPE_REGISTER_HOLDING: { + ENTRY_ATTR: "registers", + ENTRY_FUNC: None, + }, + CALL_TYPE_REGISTER_INPUT: { + ENTRY_ATTR: "registers", + ENTRY_FUNC: None, + }, + CALL_TYPE_WRITE_COIL: { + ENTRY_ATTR: "value", + ENTRY_FUNC: None, + }, + CALL_TYPE_WRITE_COILS: { + ENTRY_ATTR: "count", + ENTRY_FUNC: None, + }, + CALL_TYPE_WRITE_REGISTER: { + ENTRY_ATTR: "value", + ENTRY_FUNC: None, + }, + CALL_TYPE_WRITE_REGISTERS: { + ENTRY_ATTR: "count", + ENTRY_FUNC: None, + }, + } + @property def name(self): """Return the name of this hub.""" @@ -202,6 +248,25 @@ class ModbusHub: async with self._lock: await self.hass.async_add_executor_job(self._pymodbus_connect) + self._call_type[CALL_TYPE_COIL][ENTRY_FUNC] = self._client.read_coils + self._call_type[CALL_TYPE_DISCRETE][ + ENTRY_FUNC + ] = self._client.read_discrete_inputs + self._call_type[CALL_TYPE_REGISTER_HOLDING][ + ENTRY_FUNC + ] = self._client.read_holding_registers + self._call_type[CALL_TYPE_REGISTER_INPUT][ + ENTRY_FUNC + ] = self._client.read_input_registers + self._call_type[CALL_TYPE_WRITE_COIL][ENTRY_FUNC] = self._client.write_coil + self._call_type[CALL_TYPE_WRITE_COILS][ENTRY_FUNC] = self._client.write_coils + self._call_type[CALL_TYPE_WRITE_REGISTER][ + ENTRY_FUNC + ] = self._client.write_register + self._call_type[CALL_TYPE_WRITE_REGISTERS][ + ENTRY_FUNC + ] = self._client.write_registers + # Start counting down to allow modbus requests. if self._config_delay: self._async_cancel_listener = async_call_later( @@ -239,73 +304,69 @@ class ModbusHub: except ModbusException as exception_error: self._log_error(exception_error, error_state=False) - def _pymodbus_call(self, unit, address, value, check_attr, func): + def _pymodbus_call(self, unit, address, value, use_call): """Call sync. pymodbus.""" kwargs = {"unit": unit} if unit else {} try: - result = func(address, value, **kwargs) + result = self._call_type[use_call][ENTRY_FUNC](address, value, **kwargs) except ModbusException as exception_error: self._log_error(exception_error) result = exception_error - if not hasattr(result, check_attr): + if not hasattr(result, self._call_type[use_call][ENTRY_ATTR]): self._log_error(result) return None self._in_error = False return result - async def async_pymodbus_call(self, unit, address, value, check_attr, func): + async def async_pymodbus_call(self, unit, address, value, use_call): """Convert async to sync pymodbus call.""" if self._config_delay: return None async with self._lock: return await self.hass.async_add_executor_job( - self._pymodbus_call, unit, address, value, check_attr, func + self._pymodbus_call, unit, address, value, use_call ) async def async_read_coils(self, unit, address, count): """Read coils.""" - return await self.async_pymodbus_call( - unit, address, count, "bits", self._client.read_coils - ) + return await self.async_pymodbus_call(unit, address, count, CALL_TYPE_COIL) async def async_read_discrete_inputs(self, unit, address, count): """Read discrete inputs.""" - return await self.async_pymodbus_call( - unit, address, count, "bits", self._client.read_discrete_inputs - ) + return await self.async_pymodbus_call(unit, address, count, CALL_TYPE_DISCRETE) async def async_read_input_registers(self, unit, address, count): """Read input registers.""" return await self.async_pymodbus_call( - unit, address, count, "registers", self._client.read_input_registers + unit, address, count, CALL_TYPE_REGISTER_INPUT ) async def async_read_holding_registers(self, unit, address, count): """Read holding registers.""" return await self.async_pymodbus_call( - unit, address, count, "registers", self._client.read_holding_registers + unit, address, count, CALL_TYPE_REGISTER_HOLDING ) async def async_write_coil(self, unit, address, value) -> bool: """Write coil.""" return await self.async_pymodbus_call( - unit, address, value, "value", self._client.write_coil + unit, address, value, CALL_TYPE_WRITE_COIL ) async def async_write_coils(self, unit, address, values) -> bool: """Write coil.""" return await self.async_pymodbus_call( - unit, address, values, "count", self._client.write_coils + unit, address, values, CALL_TYPE_WRITE_COILS ) async def async_write_register(self, unit, address, value) -> bool: """Write register.""" return await self.async_pymodbus_call( - unit, address, value, "value", self._client.write_register + unit, address, value, CALL_TYPE_WRITE_REGISTER ) async def async_write_registers(self, unit, address, values) -> bool: """Write registers.""" return await self.async_pymodbus_call( - unit, address, values, "count", self._client.write_registers + unit, address, values, CALL_TYPE_WRITE_REGISTERS ) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 7aeb142d1e2..b30aaa3c3d9 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -283,14 +283,9 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): """Update the state of the sensor.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval - if self._register_type == CALL_TYPE_REGISTER_INPUT: - result = await self._hub.async_read_input_registers( - self._slave, self._register, self._count - ) - else: - result = await self._hub.async_read_holding_registers( - self._slave, self._register, self._count - ) + result = await self._hub.async_pymodbus_call( + self._slave, self._register, self._count, self._register_type + ) if result is None: self._available = False self.async_write_ha_state() diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py index f247bb4b148..876259d1fc2 100644 --- a/tests/components/modbus/test_modbus_switch.py +++ b/tests/components/modbus/test_modbus_switch.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components.modbus.const import ( CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, CONF_COILS, @@ -232,11 +233,11 @@ async def test_service_switch_update(hass, mock_pymodbus): CONF_NAME: "test", CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, - CONF_VERIFY: {}, + CONF_VERIFY: {CONF_INPUT_TYPE: CALL_TYPE_DISCRETE}, } ] } - mock_pymodbus.read_coils.return_value = ReadResult([0x01]) + mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) await prepare_service_update( hass, config, @@ -245,7 +246,7 @@ async def test_service_switch_update(hass, mock_pymodbus): "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == STATE_ON - mock_pymodbus.read_coils.return_value = ReadResult([0x00]) + mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x00]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) From ecac574eb0c50a5ecfc85f41d214258b5ffa2e66 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 17 May 2021 11:27:09 +0200 Subject: [PATCH 506/852] Upgrade pyupgrade to v2.16.0 (#50756) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57bc4a85ad1..66e10b18767 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.15.0 + rev: v2.16.0 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 8422dbc338c..e403e05fcfd 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 -pyupgrade==2.15.0 +pyupgrade==2.16.0 yamllint==1.26.1 From 9ee3b771350b91b437350443a073ab15c8e57813 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 17 May 2021 12:14:54 +0200 Subject: [PATCH 507/852] Remove discovery from iCloud (#50751) --- homeassistant/components/icloud/manifest.json | 1 - homeassistant/generated/zeroconf.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 4e07ebd2573..6c40ef6bf03 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -5,6 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/icloud", "requirements": ["pyicloud==0.10.2"], "codeowners": ["@Quentame", "@nzapponi"], - "zeroconf": ["_homekit._tcp.local."], "iot_class": "cloud_polling" } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 00eb5b53170..dce459e2083 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -91,9 +91,6 @@ ZEROCONF = { "_homekit._tcp.local.": [ { "domain": "homekit" - }, - { - "domain": "icloud" } ], "_http._tcp.local.": [ From 1c7242a37a5266f37e59b2d55f5ee78f6b4033c2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 17 May 2021 12:17:19 +0200 Subject: [PATCH 508/852] Create KNX cover entities directly from config (#50707) --- homeassistant/components/knx/cover.py | 54 ++++++++++++++++++------- homeassistant/components/knx/factory.py | 34 +--------------- 2 files changed, 41 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index b0b69c83a31..0ca060e6f22 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import datetime from typing import Any, Callable +from xknx import XKNX from xknx.devices import Cover as XknxCover, Device as XknxDevice from xknx.telegram.address import parse_device_group_address @@ -22,6 +23,7 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -40,24 +42,26 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up cover(s) for KNX platform.""" - _async_migrate_unique_id(hass, discovery_info) + if not discovery_info or not discovery_info["platform_config"]: + return + platform_config = discovery_info["platform_config"] + _async_migrate_unique_id(hass, platform_config) + + xknx: XKNX = hass.data[DOMAIN].xknx + entities = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxCover): - entities.append(KNXCover(device)) + for entity_config in platform_config: + entities.append(KNXCover(xknx, entity_config)) + async_add_entities(entities) @callback def _async_migrate_unique_id( - hass: HomeAssistant, discovery_info: DiscoveryInfoType | None + hass: HomeAssistant, platform_config: list[ConfigType] ) -> None: """Change unique_ids used in 2021.4 to include position_target GA.""" entity_registry = er.async_get(hass) - if not discovery_info or not discovery_info["platform_config"]: - return - - platform_config = discovery_info["platform_config"] for entity_config in platform_config: # normalize group address strings - ga_updown was the old uid but is optional updown_addresses = entity_config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS) @@ -82,12 +86,34 @@ def _async_migrate_unique_id( class KNXCover(KnxEntity, CoverEntity): """Representation of a KNX cover.""" - def __init__(self, device: XknxCover): + def __init__(self, xknx: XKNX, config: ConfigType): """Initialize the cover.""" self._device: XknxCover - super().__init__(device) + super().__init__( + device=XknxCover( + xknx, + name=config[CONF_NAME], + group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS), + group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS), + group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS), + group_address_position_state=config.get( + CoverSchema.CONF_POSITION_STATE_ADDRESS + ), + group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS), + group_address_angle_state=config.get( + CoverSchema.CONF_ANGLE_STATE_ADDRESS + ), + group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS), + travel_time_down=config[CoverSchema.CONF_TRAVELLING_TIME_DOWN], + travel_time_up=config[CoverSchema.CONF_TRAVELLING_TIME_UP], + invert_position=config[CoverSchema.CONF_INVERT_POSITION], + invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], + ) + ) + self._device_class: str | None = config.get(CONF_DEVICE_CLASS) self._unique_id = ( - f"{device.updown.group_address}_{device.position_target.group_address}" + f"{self._device.updown.group_address}_" + f"{self._device.position_target.group_address}" ) self._unsubscribe_auto_updater: Callable[[], None] | None = None @@ -101,8 +127,8 @@ class KNXCover(KnxEntity, CoverEntity): @property def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" - if self._device.device_class in DEVICE_CLASSES: - return self._device.device_class + if self._device_class in DEVICE_CLASSES: + return self._device_class if self._device.supports_angle: return DEVICE_CLASS_BLIND return None diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 702b6d4f3c1..a8092fe03de 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -6,7 +6,6 @@ from xknx.devices import ( BinarySensor as XknxBinarySensor, Climate as XknxClimate, ClimateMode as XknxClimateMode, - Cover as XknxCover, Device as XknxDevice, Sensor as XknxSensor, Weather as XknxWeather, @@ -16,13 +15,7 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE from homeassistant.helpers.typing import ConfigType from .const import SupportedPlatforms -from .schema import ( - BinarySensorSchema, - ClimateSchema, - CoverSchema, - SensorSchema, - WeatherSchema, -) +from .schema import BinarySensorSchema, ClimateSchema, SensorSchema, WeatherSchema def create_knx_device( @@ -31,9 +24,6 @@ def create_knx_device( config: ConfigType, ) -> XknxDevice | None: """Return the requested XKNX device.""" - if platform is SupportedPlatforms.COVER: - return _create_cover(knx_module, config) - if platform is SupportedPlatforms.CLIMATE: return _create_climate(knx_module, config) @@ -49,28 +39,6 @@ def create_knx_device( return None -def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: - """Return a KNX Cover device to be used within XKNX.""" - return XknxCover( - knx_module, - name=config[CONF_NAME], - group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS), - group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS), - group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS), - group_address_position_state=config.get( - CoverSchema.CONF_POSITION_STATE_ADDRESS - ), - group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS), - group_address_angle_state=config.get(CoverSchema.CONF_ANGLE_STATE_ADDRESS), - group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS), - travel_time_down=config[CoverSchema.CONF_TRAVELLING_TIME_DOWN], - travel_time_up=config[CoverSchema.CONF_TRAVELLING_TIME_UP], - invert_position=config[CoverSchema.CONF_INVERT_POSITION], - invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], - device_class=config.get(CONF_DEVICE_CLASS), - ) - - def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: """Return a KNX Climate device to be used within XKNX.""" climate_mode = XknxClimateMode( From 9316f566c93b43031e7bd501d6f875fa23ecce0c Mon Sep 17 00:00:00 2001 From: CantankerousBullMoose Date: Mon, 17 May 2021 03:18:14 -0700 Subject: [PATCH 509/852] Rescan static wemo (#49934) Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- homeassistant/components/wemo/__init__.py | 51 ++++++++++++++++------- tests/components/wemo/test_init.py | 32 ++++++++------ 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 6ae016954f2..d7569a6329f 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,5 +1,6 @@ """Support for WeMo device discovery.""" -import asyncio +from __future__ import annotations + import logging import pywemo @@ -16,9 +17,14 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later +from homeassistant.util.async_ import gather_with_concurrency from .const import DOMAIN +# Max number of devices to initialize at once. This limit is in place to +# avoid tying up too many executor threads with WeMo device setup. +MAX_CONCURRENCY = 3 + # Mapping from Wemo model_name to domain. WEMO_MODEL_DISPATCH = { "Bridge": LIGHT_DOMAIN, @@ -99,9 +105,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Keep track of WeMo device subscriptions for push updates registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry() await hass.async_add_executor_job(registry.start) - + static_conf = config.get(CONF_STATIC, []) wemo_dispatcher = WemoDispatcher(entry) - wemo_discovery = WemoDiscovery(hass, wemo_dispatcher) + wemo_discovery = WemoDiscovery(hass, wemo_dispatcher, static_conf) async def async_stop_wemo(event): """Shutdown Wemo subscriptions and subscription thread on exit.""" @@ -113,17 +119,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_wemo) ) - static_conf = config.get(CONF_STATIC, []) - if static_conf: - _LOGGER.debug("Adding statically configured WeMo devices") - for device in await asyncio.gather( - *[ - hass.async_add_executor_job(validate_static_config, host, port) - for host, port in static_conf - ] - ): - if device: - wemo_dispatcher.async_add_unique_device(hass, device) + # Need to do this at least once in case statics are defined and discovery is disabled + await wemo_discovery.discover_statics() if config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY): await wemo_discovery.async_discover_and_schedule() @@ -183,12 +180,18 @@ class WemoDiscovery: ADDITIONAL_SECONDS_BETWEEN_SCANS = 10 MAX_SECONDS_BETWEEN_SCANS = 300 - def __init__(self, hass: HomeAssistant, wemo_dispatcher: WemoDispatcher) -> None: + def __init__( + self, + hass: HomeAssistant, + wemo_dispatcher: WemoDispatcher, + static_config: list[tuple[[str, str | None]]], + ) -> None: """Initialize the WemoDiscovery.""" self._hass = hass self._wemo_dispatcher = wemo_dispatcher self._stop = None self._scan_delay = 0 + self._static_config = static_config async def async_discover_and_schedule(self, *_) -> None: """Periodically scan the network looking for WeMo devices.""" @@ -198,6 +201,8 @@ class WemoDiscovery: pywemo.discover_devices ): self._wemo_dispatcher.async_add_unique_device(self._hass, device) + await self.discover_statics() + finally: # Run discovery more frequently after hass has just started. self._scan_delay = min( @@ -217,6 +222,22 @@ class WemoDiscovery: self._stop() self._stop = None + async def discover_statics(self): + """Initialize or Re-Initialize connections to statically configured devices.""" + if self._static_config: + _LOGGER.debug("Adding statically configured WeMo devices") + for device in await gather_with_concurrency( + MAX_CONCURRENCY, + *[ + self._hass.async_add_executor_job( + validate_static_config, host, port + ) + for host, port in self._static_config + ], + ): + if device: + self._wemo_dispatcher.async_add_unique_device(self._hass, device) + def validate_static_config(host, port): """Handle a static config.""" diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py index 1164af7cf95..c44bdb659c5 100644 --- a/tests/components/wemo/test_init.py +++ b/tests/components/wemo/test_init.py @@ -117,20 +117,26 @@ async def test_discovery(hass, pywemo_registry): with patch( "pywemo.discover_devices", return_value=pywemo_devices ) as mock_discovery: - assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}} - ) - await pywemo_registry.semaphore.acquire() # Returns after platform setup. - mock_discovery.assert_called() - pywemo_devices.append(create_device(2)) + with patch( + "homeassistant.components.wemo.WemoDiscovery.discover_statics" + ) as mock_discover_statics: + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}} + ) + await pywemo_registry.semaphore.acquire() # Returns after platform setup. + mock_discovery.assert_called() + mock_discover_statics.assert_called() + pywemo_devices.append(create_device(2)) - # Test that discovery runs periodically and the async_dispatcher_send code works. - async_fire_time_changed( - hass, - dt.utcnow() - + timedelta(seconds=WemoDiscovery.ADDITIONAL_SECONDS_BETWEEN_SCANS + 1), - ) - await hass.async_block_till_done() + # Test that discovery runs periodically and the async_dispatcher_send code works. + async_fire_time_changed( + hass, + dt.utcnow() + + timedelta(seconds=WemoDiscovery.ADDITIONAL_SECONDS_BETWEEN_SCANS + 1), + ) + await hass.async_block_till_done() + # Test that discover_statics runs during discovery + assert mock_discover_statics.call_count == 3 # Verify that the expected number of devices were setup. entity_reg = er.async_get(hass) From 74c20cdaa1aee05809d37016d5b6bff4da26f005 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 17 May 2021 12:26:44 +0200 Subject: [PATCH 510/852] Upgrade geopy to 2.1.0 (#50714) --- homeassistant/components/aprs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index 5879c122356..29216e622da 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -3,6 +3,6 @@ "name": "APRS", "documentation": "https://www.home-assistant.io/integrations/aprs", "codeowners": ["@PhilRW"], - "requirements": ["aprslib==0.6.46", "geopy==1.21.0"], + "requirements": ["aprslib==0.6.46", "geopy==2.1.0"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 295667ed237..28d5d9cc138 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -645,7 +645,7 @@ geniushub-client==0.6.30 geojson_client==0.4 # homeassistant.components.aprs -geopy==1.21.0 +geopy==2.1.0 # homeassistant.components.geo_rss_events georss_generic_client==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8ac0657ab7..c49bc65c270 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -351,7 +351,7 @@ garminconnect==0.1.19 geojson_client==0.4 # homeassistant.components.aprs -geopy==1.21.0 +geopy==2.1.0 # homeassistant.components.geo_rss_events georss_generic_client==0.4 From 3ab14d452c63680d811ebf06cf99a0ed0a356286 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 May 2021 12:50:54 +0200 Subject: [PATCH 511/852] Refactor MQTT basic light pt1: Add add_topic helper (#50759) --- .../components/mqtt/light/schema_basic.py | 62 ++++++------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 000ab956911..9f19bc6d70e 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -256,6 +256,15 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): last_state = await self.async_get_last_state() + def add_topic(topic, msg_callback): + """Add a topic.""" + if self._topic[topic] is not None: + topics[topic] = { + "topic": self._topic[topic], + "msg_callback": msg_callback, + "qos": self._config[CONF_QOS], + } + @callback @log_messages(self.hass, self.entity_id) def state_received(msg): @@ -298,13 +307,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._brightness = percent_bright * 255 self.async_write_ha_state() - if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None: - topics[CONF_BRIGHTNESS_STATE_TOPIC] = { - "topic": self._topic[CONF_BRIGHTNESS_STATE_TOPIC], - "msg_callback": brightness_received, - "qos": self._config[CONF_QOS], - } - elif ( + add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) + if ( self._optimistic_brightness and last_state and last_state.attributes.get(ATTR_BRIGHTNESS) @@ -327,12 +331,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._brightness = percent_bright * 255 self.async_write_ha_state() - if self._topic[CONF_RGB_STATE_TOPIC] is not None: - topics[CONF_RGB_STATE_TOPIC] = { - "topic": self._topic[CONF_RGB_STATE_TOPIC], - "msg_callback": rgb_received, - "qos": self._config[CONF_QOS], - } + add_topic(CONF_RGB_STATE_TOPIC, rgb_received) if ( self._optimistic_rgb and last_state @@ -354,12 +353,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._color_temp = int(payload) self.async_write_ha_state() - if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None: - topics[CONF_COLOR_TEMP_STATE_TOPIC] = { - "topic": self._topic[CONF_COLOR_TEMP_STATE_TOPIC], - "msg_callback": color_temp_received, - "qos": self._config[CONF_QOS], - } + add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) if ( self._optimistic_color_temp and last_state @@ -381,12 +375,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._effect = payload self.async_write_ha_state() - if self._topic[CONF_EFFECT_STATE_TOPIC] is not None: - topics[CONF_EFFECT_STATE_TOPIC] = { - "topic": self._topic[CONF_EFFECT_STATE_TOPIC], - "msg_callback": effect_received, - "qos": self._config[CONF_QOS], - } + add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) if ( self._optimistic_effect and last_state @@ -410,12 +399,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): except ValueError: _LOGGER.debug("Failed to parse hs state update: '%s'", payload) - if self._topic[CONF_HS_STATE_TOPIC] is not None: - topics[CONF_HS_STATE_TOPIC] = { - "topic": self._topic[CONF_HS_STATE_TOPIC], - "msg_callback": hs_received, - "qos": self._config[CONF_QOS], - } + add_topic(CONF_HS_STATE_TOPIC, hs_received) if ( self._optimistic_hs and last_state @@ -439,13 +423,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._white_value = percent_white * 255 self.async_write_ha_state() - if self._topic[CONF_WHITE_VALUE_STATE_TOPIC] is not None: - topics[CONF_WHITE_VALUE_STATE_TOPIC] = { - "topic": self._topic[CONF_WHITE_VALUE_STATE_TOPIC], - "msg_callback": white_value_received, - "qos": self._config[CONF_QOS], - } - elif ( + add_topic(CONF_WHITE_VALUE_STATE_TOPIC, white_value_received) + if ( self._optimistic_white_value and last_state and last_state.attributes.get(ATTR_WHITE_VALUE) @@ -465,12 +444,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._hs = color_util.color_xy_to_hs(*xy_color) self.async_write_ha_state() - if self._topic[CONF_XY_STATE_TOPIC] is not None: - topics[CONF_XY_STATE_TOPIC] = { - "topic": self._topic[CONF_XY_STATE_TOPIC], - "msg_callback": xy_received, - "qos": self._config[CONF_QOS], - } + add_topic(CONF_XY_STATE_TOPIC, xy_received) if ( self._optimistic_xy and last_state From a9c73ac264208f7e719db3af1d0185b4fcd6a4ed Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Mon, 17 May 2021 04:18:50 -0700 Subject: [PATCH 512/852] Fix armed_night logic in totalconnect alarm and add tests (#50694) * Fix armed_night, add tests for alarm * end assertions with expected values --- .coveragerc | 1 - .../totalconnect/alarm_control_panel.py | 4 +- tests/components/totalconnect/common.py | 102 ++++++++++++ .../totalconnect/test_alarm_control_panel.py | 147 ++++++++++++++++++ 4 files changed, 251 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6f6fa152fa7..3b94df28fd9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1057,7 +1057,6 @@ omit = homeassistant/components/toon/switch.py homeassistant/components/torque/sensor.py homeassistant/components/totalconnect/__init__.py - homeassistant/components/totalconnect/alarm_control_panel.py homeassistant/components/totalconnect/binary_sensor.py homeassistant/components/totalconnect/const.py homeassistant/components/touchline/climate.py diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index ae999ade9ac..7e88322eca1 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -89,10 +89,10 @@ class TotalConnectAlarm(alarm.AlarmControlPanelEntity): if self._client.locations[self._location_id].is_disarmed(): state = STATE_ALARM_DISARMED - elif self._client.locations[self._location_id].is_armed_home(): - state = STATE_ALARM_ARMED_HOME elif self._client.locations[self._location_id].is_armed_night(): state = STATE_ALARM_ARMED_NIGHT + elif self._client.locations[self._location_id].is_armed_home(): + state = STATE_ALARM_ARMED_HOME elif self._client.locations[self._location_id].is_armed_away(): state = STATE_ALARM_ARMED_AWAY elif self._client.locations[self._location_id].is_armed_custom_bypass(): diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index 6f9fef4b7c0..b092a028c0b 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -59,13 +59,71 @@ PARTITION_ARMED_AWAY = { "ArmingState": TotalConnectClient.TotalConnectLocation.ARMED_AWAY, } +PARTITION_ARMED_CUSTOM = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ARMED_CUSTOM_BYPASS, +} + +PARTITION_ARMED_NIGHT = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ARMED_STAY_NIGHT, +} + +PARTITION_ARMING = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ARMING, +} +PARTITION_DISARMING = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.DISARMING, +} + +PARTITION_TRIGGERED_POLICE = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ALARMING, +} + +PARTITION_TRIGGERED_FIRE = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ALARMING_FIRE_SMOKE, +} + +PARTITION_TRIGGERED_CARBON_MONOXIDE = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ALARMING_CARBON_MONOXIDE, +} + +PARTITION_UNKNOWN = { + "PartitionID": "1", + "ArmingState": "99999", +} + + PARTITION_INFO_DISARMED = {0: PARTITION_DISARMED} PARTITION_INFO_ARMED_STAY = {0: PARTITION_ARMED_STAY} PARTITION_INFO_ARMED_AWAY = {0: PARTITION_ARMED_AWAY} +PARTITION_INFO_ARMED_CUSTOM = {0: PARTITION_ARMED_CUSTOM} +PARTITION_INFO_ARMED_NIGHT = {0: PARTITION_ARMED_NIGHT} +PARTITION_INFO_ARMING = {0: PARTITION_ARMING} +PARTITION_INFO_DISARMING = {0: PARTITION_DISARMING} +PARTITION_INFO_TRIGGERED_POLICE = {0: PARTITION_TRIGGERED_POLICE} +PARTITION_INFO_TRIGGERED_FIRE = {0: PARTITION_TRIGGERED_FIRE} +PARTITION_INFO_TRIGGERED_CARBON_MONOXIDE = {0: PARTITION_TRIGGERED_CARBON_MONOXIDE} +PARTITION_INFO_UNKNOWN = {0: PARTITION_UNKNOWN} PARTITIONS_DISARMED = {"PartitionInfo": PARTITION_INFO_DISARMED} PARTITIONS_ARMED_STAY = {"PartitionInfo": PARTITION_INFO_ARMED_STAY} PARTITIONS_ARMED_AWAY = {"PartitionInfo": PARTITION_INFO_ARMED_AWAY} +PARTITIONS_ARMED_CUSTOM = {"PartitionInfo": PARTITION_INFO_ARMED_CUSTOM} +PARTITIONS_ARMED_NIGHT = {"PartitionInfo": PARTITION_INFO_ARMED_NIGHT} +PARTITIONS_ARMING = {"PartitionInfo": PARTITION_INFO_ARMING} +PARTITIONS_DISARMING = {"PartitionInfo": PARTITION_INFO_DISARMING} +PARTITIONS_TRIGGERED_POLICE = {"PartitionInfo": PARTITION_INFO_TRIGGERED_POLICE} +PARTITIONS_TRIGGERED_FIRE = {"PartitionInfo": PARTITION_INFO_TRIGGERED_FIRE} +PARTITIONS_TRIGGERED_CARBON_MONOXIDE = { + "PartitionInfo": PARTITION_INFO_TRIGGERED_CARBON_MONOXIDE +} +PARTITIONS_UNKNOWN = {"PartitionInfo": PARTITION_INFO_UNKNOWN} ZONE_NORMAL = { "ZoneID": "1", @@ -94,9 +152,53 @@ METADATA_ARMED_STAY["Partitions"] = PARTITIONS_ARMED_STAY METADATA_ARMED_AWAY = METADATA_DISARMED.copy() METADATA_ARMED_AWAY["Partitions"] = PARTITIONS_ARMED_AWAY +METADATA_ARMED_CUSTOM = METADATA_DISARMED.copy() +METADATA_ARMED_CUSTOM["Partitions"] = PARTITIONS_ARMED_CUSTOM + +METADATA_ARMED_NIGHT = METADATA_DISARMED.copy() +METADATA_ARMED_NIGHT["Partitions"] = PARTITIONS_ARMED_NIGHT + +METADATA_ARMING = METADATA_DISARMED.copy() +METADATA_ARMING["Partitions"] = PARTITIONS_ARMING + +METADATA_DISARMING = METADATA_DISARMED.copy() +METADATA_DISARMING["Partitions"] = PARTITIONS_DISARMING + +METADATA_TRIGGERED_POLICE = METADATA_DISARMED.copy() +METADATA_TRIGGERED_POLICE["Partitions"] = PARTITIONS_TRIGGERED_POLICE + +METADATA_TRIGGERED_FIRE = METADATA_DISARMED.copy() +METADATA_TRIGGERED_FIRE["Partitions"] = PARTITIONS_TRIGGERED_FIRE + +METADATA_TRIGGERED_CARBON_MONOXIDE = METADATA_DISARMED.copy() +METADATA_TRIGGERED_CARBON_MONOXIDE["Partitions"] = PARTITIONS_TRIGGERED_CARBON_MONOXIDE + +METADATA_UNKNOWN = METADATA_DISARMED.copy() +METADATA_UNKNOWN["Partitions"] = PARTITIONS_UNKNOWN + RESPONSE_DISARMED = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_DISARMED} RESPONSE_ARMED_STAY = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMED_STAY} RESPONSE_ARMED_AWAY = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMED_AWAY} +RESPONSE_ARMED_CUSTOM = { + "ResultCode": 0, + "PanelMetadataAndStatus": METADATA_ARMED_CUSTOM, +} +RESPONSE_ARMED_NIGHT = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMED_NIGHT} +RESPONSE_ARMING = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMING} +RESPONSE_DISARMING = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_DISARMING} +RESPONSE_TRIGGERED_POLICE = { + "ResultCode": 0, + "PanelMetadataAndStatus": METADATA_TRIGGERED_POLICE, +} +RESPONSE_TRIGGERED_FIRE = { + "ResultCode": 0, + "PanelMetadataAndStatus": METADATA_TRIGGERED_FIRE, +} +RESPONSE_TRIGGERED_CARBON_MONOXIDE = { + "ResultCode": 0, + "PanelMetadataAndStatus": METADATA_TRIGGERED_CARBON_MONOXIDE, +} +RESPONSE_UNKNOWN = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_UNKNOWN} RESPONSE_ARM_SUCCESS = {"ResultCode": TotalConnectClient.TotalConnectClient.ARM_SUCCESS} RESPONSE_ARM_FAILURE = { diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 9d8dbaf0358..a77adea5e27 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -9,10 +9,16 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, + STATE_ALARM_DISARMING, + STATE_ALARM_TRIGGERED, ) from homeassistant.exceptions import HomeAssistantError @@ -21,10 +27,19 @@ from .common import ( RESPONSE_ARM_FAILURE, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY, + RESPONSE_ARMED_CUSTOM, + RESPONSE_ARMED_NIGHT, RESPONSE_ARMED_STAY, + RESPONSE_ARMING, RESPONSE_DISARM_FAILURE, RESPONSE_DISARM_SUCCESS, RESPONSE_DISARMED, + RESPONSE_DISARMING, + RESPONSE_SUCCESS, + RESPONSE_TRIGGERED_CARBON_MONOXIDE, + RESPONSE_TRIGGERED_FIRE, + RESPONSE_TRIGGERED_POLICE, + RESPONSE_UNKNOWN, RESPONSE_USER_CODE_INVALID, setup_platform, ) @@ -197,3 +212,135 @@ async def test_disarm_invalid_usercode(hass): await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect failed to disarm test." assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY + + +async def test_arm_night_success(hass): + """Test arm night method success.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_NIGHT] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True + ) + + await hass.async_block_till_done() + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_NIGHT + + +async def test_arm_night_failure(hass): + """Test arm night method failure.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_DISARMED] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{err.value}" == "TotalConnect failed to arm night test." + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + + +async def test_arming(hass): + """Test arming.""" + responses = [RESPONSE_DISARMED, RESPONSE_SUCCESS, RESPONSE_ARMING] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True + ) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMING + + +async def test_disarming(hass): + """Test disarming.""" + responses = [RESPONSE_ARMED_AWAY, RESPONSE_SUCCESS, RESPONSE_DISARMING] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True + ) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMING + + +async def test_triggered_fire(hass): + """Test triggered by fire.""" + responses = [RESPONSE_TRIGGERED_FIRE] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ALARM_TRIGGERED + assert state.attributes.get("triggered_source") == "Fire/Smoke" + + +async def test_triggered_police(hass): + """Test triggered by police.""" + responses = [RESPONSE_TRIGGERED_POLICE] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ALARM_TRIGGERED + assert state.attributes.get("triggered_source") == "Police/Medical" + + +async def test_triggered_carbon_monoxide(hass): + """Test triggered by carbon monoxide.""" + responses = [RESPONSE_TRIGGERED_CARBON_MONOXIDE] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ALARM_TRIGGERED + assert state.attributes.get("triggered_source") == "Carbon Monoxide" + + +async def test_armed_custom(hass): + """Test armed custom.""" + responses = [RESPONSE_ARMED_CUSTOM] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS + + +async def test_unknown(hass): + """Test unknown arm status.""" + responses = [RESPONSE_UNKNOWN] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == "unknown" From ee4e14e45ef576d621fd522ac91b07a37a3dc2fa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 May 2021 13:51:09 +0200 Subject: [PATCH 513/852] Hoist ATTR_LAST_RESET from utility_meter to SensorEntity (#50757) --- homeassistant/components/sensor/__init__.py | 23 +++++++++++++++---- .../components/utility_meter/sensor.py | 9 +++++--- tests/components/utility_meter/test_sensor.py | 23 +++++++++++-------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index ed88ca55ceb..f3b9a24a15d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -2,9 +2,9 @@ from __future__ import annotations from collections.abc import Mapping -from datetime import timedelta +from datetime import datetime, timedelta import logging -from typing import Any, cast +from typing import Any, cast, final import voluptuous as vol @@ -36,6 +36,7 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) +ATTR_LAST_RESET = "last_reset" ATTR_STATE_CLASS = "state_class" DOMAIN = "sensor" @@ -100,10 +101,24 @@ class SensorEntity(Entity): """Return the state class of this entity, from STATE_CLASSES, if any.""" return None + @property + def last_reset(self) -> datetime | None: + """Return the time when the sensor was last reset, if any.""" + return None + @property def capability_attributes(self) -> Mapping[str, Any] | None: """Return the capability attributes.""" - if self.state_class: - return {ATTR_STATE_CLASS: self.state_class} + if state_class := self.state_class: + return {ATTR_STATE_CLASS: state_class} + + return None + + @final + @property + def state_attributes(self) -> dict[str, Any] | None: + """Return state attributes.""" + if last_reset := self.last_reset: + return {ATTR_LAST_RESET: last_reset} return None diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index b8e7cef111c..1d244c970ff 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -5,7 +5,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ATTR_LAST_RESET, SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -51,7 +51,6 @@ ATTR_SOURCE_ID = "source" ATTR_STATUS = "status" ATTR_PERIOD = "meter_period" ATTR_LAST_PERIOD = "last_period" -ATTR_LAST_RESET = "last_reset" ATTR_TARIFF = "tariff" ICON = "mdi:counter" @@ -331,7 +330,6 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): ATTR_SOURCE_ID: self._sensor_source_id, ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING, ATTR_LAST_PERIOD: self._last_period, - ATTR_LAST_RESET: self._last_reset, } if self._period is not None: state_attr[ATTR_PERIOD] = self._period @@ -343,3 +341,8 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): def icon(self): """Return the icon to use in the frontend, if any.""" return ICON + + @property + def last_reset(self): + """Return the time when the sensor was last reset.""" + return self._last_reset diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 24938b1e818..9157ba738c7 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -161,6 +161,7 @@ async def test_state(hass): async def test_restore_state(hass): """Test utility sensor restore state.""" + last_reset = "2020-12-21T00:00:00.013073+00:00" config = { "utility_meter": { "energy_bill": { @@ -177,7 +178,7 @@ async def test_restore_state(hass): "3", attributes={ ATTR_STATUS: PAUSED, - ATTR_LAST_RESET: "2020-12-21T00:00:00.013073+00:00", + ATTR_LAST_RESET: last_reset, }, ), State( @@ -185,7 +186,7 @@ async def test_restore_state(hass): "6", attributes={ ATTR_STATUS: COLLECTING, - ATTR_LAST_RESET: "2020-12-21T00:00:00.013073+00:00", + ATTR_LAST_RESET: last_reset, }, ), ], @@ -199,10 +200,12 @@ async def test_restore_state(hass): state = hass.states.get("sensor.energy_bill_onpeak") assert state.state == "3" assert state.attributes.get("status") == PAUSED + assert state.attributes.get("last_reset") == dt_util.parse_datetime(last_reset) state = hass.states.get("sensor.energy_bill_offpeak") assert state.state == "6" assert state.attributes.get("status") == COLLECTING + assert state.attributes.get("last_reset") == dt_util.parse_datetime(last_reset) # utility_meter is loaded, now set sensors according to utility_meter: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -304,15 +307,15 @@ def gen_config(cycle, offset=None): async def _test_self_reset(hass, config, start_time, expect_reset=True): """Test energy sensor self reset.""" - assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - entity_id = config[DOMAIN]["energy_bill"]["source"] - now = dt_util.parse_datetime(start_time) with alter_time(now): + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]["energy_bill"]["source"] + async_fire_time_changed(hass, now) hass.states.async_set( entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} @@ -345,10 +348,12 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): state = hass.states.get("sensor.energy_bill") if expect_reset: assert state.attributes.get("last_period") == "2" + assert state.attributes.get("last_reset") == now assert state.state == "3" else: assert state.attributes.get("last_period") == 0 assert state.state == "5" + assert state.attributes.get("last_reset") == dt_util.parse_datetime(start_time) async def test_self_reset_quarter_hourly(hass, legacy_patchable_time): From eccefd154a086b42c02c06fcd4e5ea94511059f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 17 May 2021 14:06:50 +0200 Subject: [PATCH 514/852] Extend targets for entity component services (#50760) --- .../alarm_control_panel/services.yaml | 15 +++++- homeassistant/components/alert/services.yaml | 6 +++ .../components/automation/services.yaml | 8 +++ homeassistant/components/camera/services.yaml | 14 ++++++ .../components/climate/services.yaml | 18 +++++++ .../components/color_extractor/services.yaml | 2 + .../components/counter/services.yaml | 8 +++ homeassistant/components/cover/services.yaml | 20 ++++++++ homeassistant/components/fan/services.yaml | 20 ++++++++ .../components/input_boolean/services.yaml | 6 +++ .../components/input_datetime/services.yaml | 2 + .../components/input_number/services.yaml | 6 +++ .../components/input_select/services.yaml | 12 +++++ .../components/input_text/services.yaml | 2 + homeassistant/components/light/services.yaml | 18 ++++--- .../components/litterrobot/services.yaml | 6 +++ homeassistant/components/lock/services.yaml | 6 +++ .../components/media_player/services.yaml | 50 +++++++++++++++++-- homeassistant/components/number/services.yaml | 2 + homeassistant/components/remote/services.yaml | 12 +++++ homeassistant/components/scene/services.yaml | 2 + homeassistant/components/script/services.yaml | 6 +++ homeassistant/components/switch/services.yaml | 6 +++ homeassistant/components/timer/services.yaml | 8 +++ .../components/utility_meter/services.yaml | 7 ++- homeassistant/components/vacuum/services.yaml | 22 ++++++++ .../components/water_heater/services.yaml | 8 ++- 27 files changed, 276 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index b18f1cfb782..8c148a6a1e0 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -4,6 +4,8 @@ alarm_disarm: name: Disarm description: Send the alarm the command for disarm. target: + entity: + domain: alarm_control_panel fields: code: name: Code @@ -16,11 +18,12 @@ alarm_arm_custom_bypass: name: Arm with custom bypass description: Send arm custom bypass command. target: + entity: + domain: alarm_control_panel fields: code: name: Code - description: - An optional code to arm custom bypass the alarm control panel with. + description: An optional code to arm custom bypass the alarm control panel with. example: "1234" selector: text: @@ -29,6 +32,8 @@ alarm_arm_home: name: Arm home description: Send the alarm the command for arm home. target: + entity: + domain: alarm_control_panel fields: code: name: Code @@ -41,6 +46,8 @@ alarm_arm_away: name: Arm away description: Send the alarm the command for arm away. target: + entity: + domain: alarm_control_panel fields: code: name: Code @@ -53,6 +60,8 @@ alarm_arm_night: name: Arm night description: Send the alarm the command for arm night. target: + entity: + domain: alarm_control_panel fields: code: name: Code @@ -65,6 +74,8 @@ alarm_trigger: name: Trigger description: Send the alarm the command for trigger. target: + entity: + domain: alarm_control_panel fields: code: name: Code diff --git a/homeassistant/components/alert/services.yaml b/homeassistant/components/alert/services.yaml index 5800d642b93..3242a9cedb4 100644 --- a/homeassistant/components/alert/services.yaml +++ b/homeassistant/components/alert/services.yaml @@ -2,13 +2,19 @@ toggle: name: Toggle description: Toggle alert's notifications. target: + entity: + domain: alert turn_off: name: Turn off description: Silence alert's notifications. target: + entity: + domain: alert turn_on: name: Turn on description: Reset alert's notifications. target: + entity: + domain: alert diff --git a/homeassistant/components/automation/services.yaml b/homeassistant/components/automation/services.yaml index 5d399fb253e..dec5793d1e7 100644 --- a/homeassistant/components/automation/services.yaml +++ b/homeassistant/components/automation/services.yaml @@ -3,11 +3,15 @@ turn_on: name: Turn on description: Enable an automation. target: + entity: + domain: automation turn_off: name: Turn off description: Disable an automation. target: + entity: + domain: automation fields: stop_actions: name: Stop actions @@ -21,11 +25,15 @@ toggle: name: Toggle description: Toggle (enable / disable) an automation. target: + entity: + domain: automation trigger: name: Trigger description: Trigger the actions of an automation. target: + entity: + domain: automation fields: skip_condition: name: Skip conditions diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index 3c8e99f001b..61b6e624b12 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -4,26 +4,36 @@ turn_off: name: Turn off description: Turn off camera. target: + entity: + domain: camera turn_on: name: Turn on description: Turn on camera. target: + entity: + domain: camera enable_motion_detection: name: Enable motion detection description: Enable the motion detection in a camera. target: + entity: + domain: camera disable_motion_detection: name: Disable motion detection description: Disable the motion detection in a camera. target: + entity: + domain: camera snapshot: name: Take snapshot description: Take a snapshot from a camera. target: + entity: + domain: camera fields: filename: name: Filename @@ -37,6 +47,8 @@ play_stream: name: Play stream description: Play camera stream on supported media player. target: + entity: + domain: camera fields: media_player: name: Media Player @@ -60,6 +72,8 @@ record: name: Record description: Record live camera feed. target: + entity: + domain: camera fields: filename: name: Filename diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index ca88896c6c2..001f35726ad 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -4,6 +4,8 @@ set_aux_heat: name: Turn on/off auxiliary heater description: Turn auxiliary heater on/off for climate device. target: + entity: + domain: climate fields: aux_heat: name: Auxiliary heating @@ -17,6 +19,8 @@ set_preset_mode: name: Set preset mode description: Set preset mode for climate device. target: + entity: + domain: climate fields: preset_mode: name: Preset mode @@ -30,6 +34,8 @@ set_temperature: name: Set temperature description: Set target temperature of climate device. target: + entity: + domain: climate fields: temperature: name: Temperature @@ -82,6 +88,8 @@ set_humidity: name: Set target humidity description: Set target humidity of climate device. target: + entity: + domain: climate fields: humidity: name: Humidity @@ -100,6 +108,8 @@ set_fan_mode: name: Set fan mode description: Set fan operation for climate device. target: + entity: + domain: climate fields: fan_mode: name: Fan mode @@ -113,6 +123,8 @@ set_hvac_mode: name: Set HVAC mode description: Set HVAC operation mode for climate device. target: + entity: + domain: climate fields: hvac_mode: name: HVAC mode @@ -133,6 +145,8 @@ set_swing_mode: name: Set swing mode description: Set swing operation for climate device. target: + entity: + domain: climate fields: swing_mode: name: Swing mode @@ -146,8 +160,12 @@ turn_on: name: Turn on description: Turn climate device on. target: + entity: + domain: climate turn_off: name: Turn off description: Turn climate device off. target: + entity: + domain: climate diff --git a/homeassistant/components/color_extractor/services.yaml b/homeassistant/components/color_extractor/services.yaml index 00438dc9aa1..be278a59059 100644 --- a/homeassistant/components/color_extractor/services.yaml +++ b/homeassistant/components/color_extractor/services.yaml @@ -4,6 +4,8 @@ turn_on: Set the light RGB to the predominant color found in the image provided by URL or file path. target: + entity: + domain: light fields: color_extract_url: name: URL diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml index 4dd427c1fa1..cc26541def5 100644 --- a/homeassistant/components/counter/services.yaml +++ b/homeassistant/components/counter/services.yaml @@ -4,21 +4,29 @@ decrement: name: Decrement description: Decrement a counter. target: + entity: + domain: counter increment: name: Increment description: Increment a counter. target: + entity: + domain: counter reset: name: Reset description: Reset a counter. target: + entity: + domain: counter configure: name: Configure description: Change counter parameters. target: + entity: + domain: counter fields: minimum: name: Minimum diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml index 1419a5f48ed..f903463bd33 100644 --- a/homeassistant/components/cover/services.yaml +++ b/homeassistant/components/cover/services.yaml @@ -4,21 +4,29 @@ open_cover: name: Open description: Open all or specified cover. target: + entity: + domain: cover close_cover: name: Close description: Close all or specified cover. target: + entity: + domain: cover toggle: name: Toggle description: Toggle a cover open/closed. target: + entity: + domain: cover set_cover_position: name: Set position description: Move to specific position all or specified cover. target: + entity: + domain: cover fields: position: name: Position @@ -37,26 +45,36 @@ stop_cover: name: Stop description: Stop all or specified cover. target: + entity: + domain: cover open_cover_tilt: name: Open tilt description: Open all or specified cover tilt. target: + entity: + domain: cover close_cover_tilt: name: Close tilt description: Close all or specified cover tilt. target: + entity: + domain: cover toggle_cover_tilt: name: Toggle tilt description: Toggle a cover tilt open/closed. target: + entity: + domain: cover set_cover_tilt_position: name: Set tilt position description: Move to specific position all or specified cover tilt. target: + entity: + domain: cover fields: tilt_position: name: Tilt position @@ -75,3 +93,5 @@ stop_cover_tilt: name: Stop tilt description: Stop all or specified cover. target: + entity: + domain: cover diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index f86a32823dc..06245e68395 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -3,6 +3,8 @@ set_speed: name: Set speed description: Set fan speed. target: + entity: + domain: fan fields: speed: name: Speed @@ -16,6 +18,8 @@ set_preset_mode: name: Set preset mode description: Set preset mode for a fan device. target: + entity: + domain: fan fields: preset_mode: name: Preset mode @@ -29,6 +33,8 @@ set_percentage: name: Set speed percentage description: Set fan speed percentage. target: + entity: + domain: fan fields: percentage: name: Percentage @@ -47,6 +53,8 @@ turn_on: name: Turn on description: Turn fan on. target: + entity: + domain: fan fields: speed: name: Speed @@ -74,11 +82,15 @@ turn_off: name: Turn off description: Turn fan off. target: + entity: + domain: fan oscillate: name: Oscillate description: Oscillate the fan. target: + entity: + domain: fan fields: oscillating: name: Oscillating @@ -92,11 +104,15 @@ toggle: name: Toggle description: Toggle the fan on/off. target: + entity: + domain: fan set_direction: name: Set direction description: Set the fan rotation. target: + entity: + domain: fan fields: direction: name: Direction @@ -113,6 +129,8 @@ increase_speed: name: Increase speed description: Increase the speed of the fan by one speed or a percentage_step. target: + entity: + domain: fan fields: percentage_step: advanced: true @@ -131,6 +149,8 @@ decrease_speed: name: Decrease speed description: Decrease the speed of the fan by one speed or a percentage_step. target: + entity: + domain: fan fields: percentage_step: advanced: true diff --git a/homeassistant/components/input_boolean/services.yaml b/homeassistant/components/input_boolean/services.yaml index 68287cc3ff5..d294d61fd4d 100644 --- a/homeassistant/components/input_boolean/services.yaml +++ b/homeassistant/components/input_boolean/services.yaml @@ -2,16 +2,22 @@ toggle: name: Toggle description: Toggle an input boolean target: + entity: + domain: input_boolean turn_off: name: Turn off description: Turn off an input boolean target: + entity: + domain: input_boolean turn_on: name: Turn on description: Turn on an input boolean target: + entity: + domain: input_boolean reload: name: Reload diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index 0243ca9f67d..519b4a085ad 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -2,6 +2,8 @@ set_datetime: name: Set description: This can be used to dynamically set the date and/or time. target: + entity: + domain: input_datetime fields: date: name: Date diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml index 7d388238022..477adbb0dbe 100644 --- a/homeassistant/components/input_number/services.yaml +++ b/homeassistant/components/input_number/services.yaml @@ -2,16 +2,22 @@ decrement: name: Decrement description: Decrement the value of an input number entity by its stepping. target: + entity: + domain: input_number increment: name: Increment description: Increment the value of an input number entity by its stepping. target: + entity: + domain: input_number set_value: name: Set description: Set the value of an input number entity. target: + entity: + domain: input_number fields: value: name: Value diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index f8fbe158aab..b42497e12bf 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -2,6 +2,8 @@ select_next: name: Next description: Select the next options of an input select entity. target: + entity: + domain: input_select fields: cycle: name: Cycle @@ -15,6 +17,8 @@ select_option: name: Select description: Select an option of an input select entity. target: + entity: + domain: input_select fields: option: name: Option @@ -28,6 +32,8 @@ select_previous: name: Previous description: Select the previous options of an input select entity. target: + entity: + domain: input_select fields: cycle: name: Cycle @@ -41,16 +47,22 @@ select_first: name: First description: Select the first option of an input select entity. target: + entity: + domain: input_select select_last: name: Last description: Select the last option of an input select entity. target: + entity: + domain: input_select set_options: name: Set options description: Set the options of an input select entity. target: + entity: + domain: input_select fields: options: name: Options diff --git a/homeassistant/components/input_text/services.yaml b/homeassistant/components/input_text/services.yaml index 5983683ec6d..cf19e15d7ae 100644 --- a/homeassistant/components/input_text/services.yaml +++ b/homeassistant/components/input_text/services.yaml @@ -2,6 +2,8 @@ set_value: name: Set description: Set the value of an input text entity. target: + entity: + domain: input_text fields: value: name: Value diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 34663df0288..8ad01bcdd8c 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -6,6 +6,8 @@ turn_on: Turn on one or more lights and adjust properties of the light, even when they are turned on already. target: + entity: + domain: light fields: transition: name: Transition @@ -198,8 +200,7 @@ turn_on: - "yellowgreen" hs_color: name: Hue/Sat color - description: - Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. advanced: true example: "[300, 70]" selector: @@ -276,8 +277,7 @@ turn_on: mode: slider brightness_step_pct: name: Brightness step - description: - Change brightness by a percentage. Should be between -100..100. + description: Change brightness by a percentage. Should be between -100..100. example: -10 selector: number: @@ -320,6 +320,8 @@ turn_off: name: Turn off description: Turns off one or more lights. target: + entity: + domain: light fields: transition: name: Transition @@ -352,6 +354,8 @@ toggle: Toggles one or more lights, from on to off, or, off to on, based on their current state. target: + entity: + domain: light fields: transition: name: Transition @@ -530,8 +534,7 @@ toggle: - "yellowgreen" hs_color: name: Hue/Sat color - description: - Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. advanced: true example: "[300, 70]" selector: @@ -580,8 +583,7 @@ toggle: mode: slider brightness: name: Brightness value - description: - Number indicating brightness, where 0 turns the light + description: Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. advanced: true diff --git a/homeassistant/components/litterrobot/services.yaml b/homeassistant/components/litterrobot/services.yaml index 5ca25e1b1b8..8caf0fcb73c 100644 --- a/homeassistant/components/litterrobot/services.yaml +++ b/homeassistant/components/litterrobot/services.yaml @@ -4,11 +4,15 @@ reset_waste_drawer: name: Reset waste drawer description: Reset the waste drawer level. target: + entity: + integration: litterrobot set_sleep_mode: name: Set sleep mode description: Set the sleep mode and start time. target: + entity: + integration: litterrobot fields: enabled: name: Enabled @@ -29,6 +33,8 @@ set_wait_time: name: Set wait time description: Set the wait time, in minutes, between when your cat uses the Litter-Robot and when the unit cycles automatically. target: + entity: + integration: litterrobot fields: minutes: name: Minutes diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index f852c82e4e1..5d5e05240e8 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -46,6 +46,8 @@ lock: name: Lock description: Lock all or specified locks. target: + entity: + domain: lock fields: code: name: Code @@ -58,6 +60,8 @@ open: name: Open description: Open all or specified locks. target: + entity: + domain: lock fields: code: name: Code @@ -95,6 +99,8 @@ unlock: name: Unlock description: Unlock all or specified locks. target: + entity: + domain: lock fields: code: name: Code diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 9699fa5f8bb..6136580ff2c 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -4,31 +4,43 @@ turn_on: name: Turn on description: Turn a media player power on. target: + entity: + domain: media_player turn_off: name: Turn off description: Turn a media player power off. target: + entity: + domain: media_player toggle: name: Toggle description: Toggles a media player power state. target: + entity: + domain: media_player volume_up: name: Turn up volume description: Turn a media player volume up. target: + entity: + domain: media_player volume_down: name: Turn down volume description: Turn a media player volume down. target: + entity: + domain: media_player volume_mute: name: Mute volume description: Mute a media player's volume. target: + entity: + domain: media_player fields: is_volume_muted: name: Muted @@ -42,6 +54,8 @@ volume_set: name: Set volume description: Set a media player's volume level. target: + entity: + domain: media_player fields: volume_level: name: Level @@ -59,37 +73,50 @@ media_play_pause: name: Play/Pause description: Toggle media player play/pause state. target: + entity: + domain: media_player media_play: name: Play description: Send the media player the command for play. target: + entity: + domain: media_player media_pause: name: Pause description: Send the media player the command for pause. target: + entity: + domain: media_player media_stop: name: Stop description: Send the media player the stop command. target: + entity: + domain: media_player media_next_track: name: Next description: Send the media player the command for next track. target: + entity: + domain: media_player media_previous_track: name: Previous description: Send the media player the command for previous track. target: + entity: + domain: media_player media_seek: name: Seek - description: - Send the media player the command to seek in current playing media. + description: Send the media player the command to seek in current playing media. target: + entity: + domain: media_player fields: seek_position: name: Position @@ -107,6 +134,8 @@ play_media: name: Play media description: Send the media player the command for playing media. target: + entity: + domain: media_player fields: media_content_id: name: Content ID @@ -130,6 +159,8 @@ select_source: name: Select source description: Send the media player the command to change input source. target: + entity: + domain: media_player fields: source: name: Source @@ -143,6 +174,8 @@ select_sound_mode: name: Select sound mode description: Send the media player the command to change sound mode. target: + entity: + domain: media_player fields: sound_mode: name: Sound mode @@ -155,11 +188,15 @@ clear_playlist: name: Clear playlist description: Send the media player the command to clear players playlist. target: + entity: + domain: media_player shuffle_set: name: Shuffle description: Set shuffling state. target: + entity: + domain: media_player fields: shuffle: name: Shuffle @@ -173,6 +210,8 @@ repeat_set: name: Repeat description: Set repeat mode target: + entity: + domain: media_player fields: repeat: name: Repeat mode @@ -192,11 +231,12 @@ join: Group players together. Only works on platforms with support for player groups. target: + entity: + domain: media_player fields: group_members: name: Group members - description: - The players which will be synced with the target player. + description: The players which will be synced with the target player. example: - "media_player.multiroom_player2" - "media_player.multiroom_player3" @@ -209,3 +249,5 @@ unjoin: player groups. name: Unjoin target: + entity: + domain: media_player diff --git a/homeassistant/components/number/services.yaml b/homeassistant/components/number/services.yaml index a684fef7d5d..2014c4c5221 100644 --- a/homeassistant/components/number/services.yaml +++ b/homeassistant/components/number/services.yaml @@ -4,6 +4,8 @@ set_value: name: Set description: Set the value of a Number entity. target: + entity: + domain: number fields: value: name: Value diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 13459d452bf..a36e33aa77d 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -4,6 +4,8 @@ turn_on: name: Turn On description: Sends the Power On Command. target: + entity: + domain: remote fields: activity: description: Activity ID or Activity Name to start. @@ -15,16 +17,22 @@ toggle: name: Toggle description: Toggles a device. target: + entity: + domain: remote turn_off: name: Turn Off description: Sends the Power Off Command. target: + entity: + domain: remote send_command: name: Send Command description: Sends a command or a list of commands to a device. target: + entity: + domain: remote fields: device: name: Device @@ -75,6 +83,8 @@ learn_command: name: Learn Command description: Learns a command or a list of commands from a device. target: + entity: + domain: remote fields: device: description: Device ID to learn command from. @@ -116,6 +126,8 @@ delete_command: name: Delete Command description: Deletes a command or a list of commands from the database. target: + entity: + domain: remote fields: device: description: Name of the device from which commands will be deleted. diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 9d07460379c..eb7d6bb2ed3 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -4,6 +4,8 @@ turn_on: name: Activate description: Activate a scene. target: + entity: + domain: scene fields: transition: name: Transition diff --git a/homeassistant/components/script/services.yaml b/homeassistant/components/script/services.yaml index b772b80a1d2..1d3c0e8a8a9 100644 --- a/homeassistant/components/script/services.yaml +++ b/homeassistant/components/script/services.yaml @@ -8,13 +8,19 @@ turn_on: name: Turn on description: Turn on script target: + entity: + domain: script turn_off: name: Turn off description: Turn off script target: + entity: + domain: script toggle: name: Toggle description: Toggle script target: + entity: + domain: script diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index 64304fa22e5..33f66070bfb 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -4,13 +4,19 @@ turn_on: name: Turn on description: Turn a switch on target: + entity: + domain: switch turn_off: name: Turn off description: Turn a switch off target: + entity: + domain: switch toggle: name: Toggle description: Toggles a switch state target: + entity: + domain: switch diff --git a/homeassistant/components/timer/services.yaml b/homeassistant/components/timer/services.yaml index 54175de3cf7..37c989544e3 100644 --- a/homeassistant/components/timer/services.yaml +++ b/homeassistant/components/timer/services.yaml @@ -4,6 +4,8 @@ start: name: Start description: Start a timer target: + entity: + domain: timer fields: duration: description: Duration the timer requires to finish. [optional] @@ -16,13 +18,19 @@ pause: name: Pause description: Pause a timer. target: + entity: + domain: timer cancel: name: Cancel description: Cancel a timer. target: + entity: + domain: timer finish: name: Finish description: Finish a timer. target: + entity: + domain: timer diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index b2e2a025c47..c3f95d22175 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -4,16 +4,22 @@ reset: name: Reset description: Resets the counter of a utility meter. target: + entity: + domain: utility_meter next_tariff: name: Next Tariff description: Changes the tariff to the next one. target: + entity: + domain: utility_meter select_tariff: name: Select Tariff description: Selects the current tariff of a utility meter. target: + entity: + domain: utility_meter fields: tariff: name: Tariff @@ -37,4 +43,3 @@ calibrate: required: true selector: text: - diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index e0064bc475b..26c8d745b27 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -4,51 +4,71 @@ turn_on: name: Turn on description: Start a new cleaning task. target: + entity: + domain: vacuum turn_off: name: Turn off description: Stop the current cleaning task and return to home. target: + entity: + domain: vacuum stop: name: Stop description: Stop the current cleaning task. target: + entity: + domain: vacuum locate: name: Locate description: Locate the vacuum cleaner robot. target: + entity: + domain: vacuum start_pause: name: Start/Pause description: Start, pause, or resume the cleaning task. target: + entity: + domain: vacuum start: name: Start description: Start or resume the cleaning task. target: + entity: + domain: vacuum pause: name: Pause description: Pause the cleaning task. target: + entity: + domain: vacuum return_to_base: name: Return to base description: Tell the vacuum cleaner to return to its dock. target: + entity: + domain: vacuum clean_spot: name: Clean spot description: Tell the vacuum cleaner to do a spot clean-up. target: + entity: + domain: vacuum send_command: name: Send command description: Send a raw command to the vacuum cleaner. target: + entity: + domain: vacuum fields: command: name: Command @@ -68,6 +88,8 @@ set_fan_speed: name: Set fan speed description: Set the fan speed of the vacuum cleaner. target: + entity: + domain: vacuum fields: fan_speed: name: Fan speed diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml index 3cbd9446d38..a3b372f219e 100644 --- a/homeassistant/components/water_heater/services.yaml +++ b/homeassistant/components/water_heater/services.yaml @@ -4,6 +4,8 @@ set_away_mode: name: Set away mode description: Turn away mode on/off for water_heater device. target: + entity: + domain: water_heater fields: away_mode: name: Away mode @@ -16,6 +18,8 @@ set_temperature: name: Set temperature description: Set target temperature of water_heater device. target: + entity: + domain: water_heater fields: temperature: name: Temperature @@ -26,7 +30,7 @@ set_temperature: min: 0 max: 100 step: 0.5 - unit_of_measurement: '°' + unit_of_measurement: "°" operation_mode: name: Operation mode description: New value of operation mode. @@ -38,6 +42,8 @@ set_operation_mode: name: Set operation mode description: Set operation mode for water_heater device. target: + entity: + domain: water_heater fields: operation_mode: name: Operation mode From 9e8660295045c5b08c90db54f95ac4cc1b327a42 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 17 May 2021 14:07:01 +0200 Subject: [PATCH 515/852] Fix strings for UPNP (#50762) --- homeassistant/components/upnp/strings.json | 16 +++++++++++----- .../components/upnp/translations/en.json | 12 ++++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index e68a9a9eae5..b0bc476ae61 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -2,15 +2,12 @@ "config": { "flow_title": "{name}", "step": { - "init": { - }, "ssdp_confirm": { "description": "Do you want to set up this UPnP/IGD device?" }, "user": { "data": { - "usn": "Device", - "scan_interval": "Update interval (seconds, minimal 30)" + "unique_id": "Device" } } }, @@ -19,5 +16,14 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "incomplete_discovery": "Incomplete discovery" } - } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update interval (seconds, minimal 30)" + } + } + } +} } diff --git a/homeassistant/components/upnp/translations/en.json b/homeassistant/components/upnp/translations/en.json index 98f6cb88b87..aa22348e308 100644 --- a/homeassistant/components/upnp/translations/en.json +++ b/homeassistant/components/upnp/translations/en.json @@ -12,8 +12,16 @@ }, "user": { "data": { - "scan_interval": "Update interval (seconds, minimal 30)", - "usn": "Device" + "unique_id": "Device" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update interval (seconds, minimal 30)" } } } From df6862a519fdc0a13213aa88ceda6021047d0b94 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 17 May 2021 14:09:52 +0200 Subject: [PATCH 516/852] Add strict type annotations to amazon_polly (#50697) * add strict type annotations * apply suggestions Co-authored-by: Erik Montnemery --- .coveragerc | 2 +- .strict-typing | 1 + .../components/amazon_polly/const.py | 131 +++++++++++ homeassistant/components/amazon_polly/tts.py | 218 +++++++----------- homeassistant/components/tts/__init__.py | 4 +- mypy.ini | 11 + 6 files changed, 226 insertions(+), 141 deletions(-) create mode 100644 homeassistant/components/amazon_polly/const.py diff --git a/.coveragerc b/.coveragerc index 3b94df28fd9..f51217c40a2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -44,7 +44,7 @@ omit = homeassistant/components/alarmdecoder/const.py homeassistant/components/alarmdecoder/sensor.py homeassistant/components/alpha_vantage/sensor.py - homeassistant/components/amazon_polly/tts.py + homeassistant/components/amazon_polly/* homeassistant/components/ambiclimate/climate.py homeassistant/components/ambient_station/* homeassistant/components/amcrest/* diff --git a/.strict-typing b/.strict-typing index 12927c8c97b..8445ff511f5 100644 --- a/.strict-typing +++ b/.strict-typing @@ -8,6 +8,7 @@ homeassistant.components.actiontec.* homeassistant.components.aftership.* homeassistant.components.airly.* homeassistant.components.aladdin_connect.* +homeassistant.components.amazon_polly.* homeassistant.components.ampio.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* diff --git a/homeassistant/components/amazon_polly/const.py b/homeassistant/components/amazon_polly/const.py new file mode 100644 index 00000000000..5ae8c73881d --- /dev/null +++ b/homeassistant/components/amazon_polly/const.py @@ -0,0 +1,131 @@ +"""Constants for the Amazon Polly text to speech service.""" +from __future__ import annotations + +from typing import Final + +CONF_REGION: Final = "region_name" +CONF_ACCESS_KEY_ID: Final = "aws_access_key_id" +CONF_SECRET_ACCESS_KEY: Final = "aws_secret_access_key" + +DEFAULT_REGION: Final = "us-east-1" +SUPPORTED_REGIONS: Final[list[str]] = [ + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", + "ca-central-1", + "eu-west-1", + "eu-central-1", + "eu-west-2", + "eu-west-3", + "ap-southeast-1", + "ap-southeast-2", + "ap-northeast-2", + "ap-northeast-1", + "ap-south-1", + "sa-east-1", +] + +CONF_ENGINE: Final = "engine" +CONF_VOICE: Final = "voice" +CONF_OUTPUT_FORMAT: Final = "output_format" +CONF_SAMPLE_RATE: Final = "sample_rate" +CONF_TEXT_TYPE: Final = "text_type" + +SUPPORTED_VOICES: Final[list[str]] = [ + "Olivia", # Female, Australian, Neural + "Zhiyu", # Chinese + "Mads", + "Naja", # Danish + "Ruben", + "Lotte", # Dutch + "Russell", + "Nicole", # English Australian + "Brian", + "Amy", + "Emma", # English + "Aditi", + "Raveena", # English, Indian + "Joey", + "Justin", + "Matthew", + "Ivy", + "Joanna", + "Kendra", + "Kimberly", + "Salli", # English + "Geraint", # English Welsh + "Mathieu", + "Celine", + "Lea", # French + "Chantal", # French Canadian + "Hans", + "Marlene", + "Vicki", # German + "Aditi", # Hindi + "Karl", + "Dora", # Icelandic + "Giorgio", + "Carla", + "Bianca", # Italian + "Takumi", + "Mizuki", # Japanese + "Seoyeon", # Korean + "Liv", # Norwegian + "Jacek", + "Jan", + "Ewa", + "Maja", # Polish + "Ricardo", + "Vitoria", # Portuguese, Brazilian + "Cristiano", + "Ines", # Portuguese, European + "Carmen", # Romanian + "Maxim", + "Tatyana", # Russian + "Enrique", + "Conchita", + "Lucia", # Spanish European + "Mia", # Spanish Mexican + "Miguel", # Spanish US + "Penelope", # Spanish US + "Lupe", # Spanish US + "Astrid", # Swedish + "Filiz", # Turkish + "Gwyneth", # Welsh +] + +SUPPORTED_OUTPUT_FORMATS: Final[list[str]] = ["mp3", "ogg_vorbis", "pcm"] + +SUPPORTED_ENGINES: Final[list[str]] = ["neural", "standard"] + +SUPPORTED_SAMPLE_RATES: Final[list[str]] = ["8000", "16000", "22050", "24000"] + +SUPPORTED_SAMPLE_RATES_MAP: Final[dict[str, list[str]]] = { + "mp3": ["8000", "16000", "22050", "24000"], + "ogg_vorbis": ["8000", "16000", "22050"], + "pcm": ["8000", "16000"], +} + +SUPPORTED_TEXT_TYPES: Final[list[str]] = ["text", "ssml"] + +CONTENT_TYPE_EXTENSIONS: Final[dict[str, str]] = { + "audio/mpeg": "mp3", + "audio/ogg": "ogg", + "audio/pcm": "pcm", +} + +DEFAULT_ENGINE: Final = "standard" +DEFAULT_VOICE: Final = "Joanna" +DEFAULT_OUTPUT_FORMAT: Final = "mp3" +DEFAULT_TEXT_TYPE: Final = "text" + +DEFAULT_SAMPLE_RATES: Final[dict[str, str]] = { + "mp3": "22050", + "ogg_vorbis": "22050", + "pcm": "16000", +} + +AWS_CONF_CONNECT_TIMEOUT: Final = 10 +AWS_CONF_READ_TIMEOUT: Final = 5 +AWS_CONF_MAX_POOL_CONNECTIONS: Final = 1 diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index bdb46abda9a..7e21b9ac603 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -1,136 +1,54 @@ """Support for the Amazon Polly text to speech service.""" +from __future__ import annotations + import logging +from typing import Final import boto3 import botocore import voluptuous as vol -from homeassistant.components.tts import PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + Provider, + TtsAudioType, +) from homeassistant.const import ATTR_CREDENTIALS, CONF_PROFILE_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -_LOGGER = logging.getLogger(__name__) +from .const import ( + AWS_CONF_CONNECT_TIMEOUT, + AWS_CONF_MAX_POOL_CONNECTIONS, + AWS_CONF_READ_TIMEOUT, + CONF_ACCESS_KEY_ID, + CONF_ENGINE, + CONF_OUTPUT_FORMAT, + CONF_REGION, + CONF_SAMPLE_RATE, + CONF_SECRET_ACCESS_KEY, + CONF_TEXT_TYPE, + CONF_VOICE, + CONTENT_TYPE_EXTENSIONS, + DEFAULT_ENGINE, + DEFAULT_OUTPUT_FORMAT, + DEFAULT_REGION, + DEFAULT_SAMPLE_RATES, + DEFAULT_TEXT_TYPE, + DEFAULT_VOICE, + SUPPORTED_ENGINES, + SUPPORTED_OUTPUT_FORMATS, + SUPPORTED_REGIONS, + SUPPORTED_SAMPLE_RATES, + SUPPORTED_SAMPLE_RATES_MAP, + SUPPORTED_TEXT_TYPES, + SUPPORTED_VOICES, +) -CONF_REGION = "region_name" -CONF_ACCESS_KEY_ID = "aws_access_key_id" -CONF_SECRET_ACCESS_KEY = "aws_secret_access_key" +_LOGGER: Final = logging.getLogger(__name__) -DEFAULT_REGION = "us-east-1" -SUPPORTED_REGIONS = [ - "us-east-1", - "us-east-2", - "us-west-1", - "us-west-2", - "ca-central-1", - "eu-west-1", - "eu-central-1", - "eu-west-2", - "eu-west-3", - "ap-southeast-1", - "ap-southeast-2", - "ap-northeast-2", - "ap-northeast-1", - "ap-south-1", - "sa-east-1", -] - -CONF_ENGINE = "engine" -CONF_VOICE = "voice" -CONF_OUTPUT_FORMAT = "output_format" -CONF_SAMPLE_RATE = "sample_rate" -CONF_TEXT_TYPE = "text_type" - -SUPPORTED_VOICES = [ - "Olivia", # Female, Australian, Neural - "Zhiyu", # Chinese - "Mads", - "Naja", # Danish - "Ruben", - "Lotte", # Dutch - "Russell", - "Nicole", # English Australian - "Brian", - "Amy", - "Emma", # English - "Aditi", - "Raveena", # English, Indian - "Joey", - "Justin", - "Matthew", - "Ivy", - "Joanna", - "Kendra", - "Kimberly", - "Salli", # English - "Geraint", # English Welsh - "Mathieu", - "Celine", - "Lea", # French - "Chantal", # French Canadian - "Hans", - "Marlene", - "Vicki", # German - "Aditi", # Hindi - "Karl", - "Dora", # Icelandic - "Giorgio", - "Carla", - "Bianca", # Italian - "Takumi", - "Mizuki", # Japanese - "Seoyeon", # Korean - "Liv", # Norwegian - "Jacek", - "Jan", - "Ewa", - "Maja", # Polish - "Ricardo", - "Vitoria", # Portuguese, Brazilian - "Cristiano", - "Ines", # Portuguese, European - "Carmen", # Romanian - "Maxim", - "Tatyana", # Russian - "Enrique", - "Conchita", - "Lucia", # Spanish European - "Mia", # Spanish Mexican - "Miguel", # Spanish US - "Penelope", # Spanish US - "Lupe", # Spanish US - "Astrid", # Swedish - "Filiz", # Turkish - "Gwyneth", # Welsh -] - -SUPPORTED_OUTPUT_FORMATS = ["mp3", "ogg_vorbis", "pcm"] - -SUPPORTED_ENGINES = ["neural", "standard"] - -SUPPORTED_SAMPLE_RATES = ["8000", "16000", "22050", "24000"] - -SUPPORTED_SAMPLE_RATES_MAP = { - "mp3": ["8000", "16000", "22050", "24000"], - "ogg_vorbis": ["8000", "16000", "22050"], - "pcm": ["8000", "16000"], -} - -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" - -DEFAULT_SAMPLE_RATES = {"mp3": "22050", "ogg_vorbis": "22050", "pcm": "16000"} - -AWS_CONF_CONNECT_TIMEOUT = 10 -AWS_CONF_READ_TIMEOUT = 5 -AWS_CONF_MAX_POOL_CONNECTIONS = 1 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(SUPPORTED_REGIONS), vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, @@ -151,11 +69,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_engine(hass, config, discovery_info=None): +def get_engine( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> Provider | None: """Set up Amazon Polly speech component.""" output_format = config[CONF_OUTPUT_FORMAT] sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format]) - if sample_rate not in SUPPORTED_SAMPLE_RATES_MAP.get(output_format): + if sample_rate not in SUPPORTED_SAMPLE_RATES_MAP[output_format]: _LOGGER.error( "%s is not a valid sample rate for %s", sample_rate, output_format ) @@ -163,7 +85,7 @@ def get_engine(hass, config, discovery_info=None): config[CONF_SAMPLE_RATE] = sample_rate - profile = config.get(CONF_PROFILE_NAME) + profile: str | None = config.get(CONF_PROFILE_NAME) if profile is not None: boto3.setup_default_session(profile_name=profile) @@ -185,16 +107,20 @@ def get_engine(hass, config, discovery_info=None): polly_client = boto3.client("polly", **aws_config) - supported_languages = [] + supported_languages: list[str] = [] - all_voices = {} + all_voices: dict[str, dict[str, str]] = {} all_voices_req = polly_client.describe_voices() - for voice in all_voices_req.get("Voices"): - all_voices[voice.get("Id")] = voice - if voice.get("LanguageCode") not in supported_languages: - supported_languages.append(voice.get("LanguageCode")) + for voice in all_voices_req.get("Voices", []): + voice_id: str | None = voice.get("Id") + if voice_id is None: + continue + all_voices[voice_id] = voice + language_code: str | None = voice.get("LanguageCode") + if language_code is not None and language_code not in supported_languages: + supported_languages.append(language_code) return AmazonPollyProvider(polly_client, config, supported_languages, all_voices) @@ -202,39 +128,53 @@ def get_engine(hass, config, discovery_info=None): class AmazonPollyProvider(Provider): """Amazon Polly speech api provider.""" - def __init__(self, polly_client, config, supported_languages, all_voices): + def __init__( + self, + polly_client: boto3.client, + config: ConfigType, + supported_languages: list[str], + all_voices: dict[str, dict[str, str]], + ) -> None: """Initialize Amazon Polly provider for TTS.""" self.client = polly_client self.config = config self.supported_langs = supported_languages self.all_voices = all_voices - self.default_voice = self.config[CONF_VOICE] + self.default_voice: str = self.config[CONF_VOICE] self.name = "Amazon Polly" @property - def supported_languages(self): + def supported_languages(self) -> list[str]: """Return a list of supported languages.""" return self.supported_langs @property - def default_language(self): + def default_language(self) -> str | None: """Return the default language.""" - return self.all_voices.get(self.default_voice).get("LanguageCode") + return self.all_voices.get(self.default_voice, {}).get("LanguageCode") @property - def default_options(self): + def default_options(self) -> dict[str, str]: """Return dict include default options.""" return {CONF_VOICE: self.default_voice} @property - def supported_options(self): + def supported_options(self) -> list[str]: """Return a list of supported options.""" return [CONF_VOICE] - def get_tts_audio(self, message, language=None, options=None): + def get_tts_audio( + self, + message: str, + language: str | None = None, + options: dict[str, str] | None = None, + ) -> TtsAudioType: """Request TTS file from Polly.""" + if options is None or language is None: + _LOGGER.debug("language and/or options were missing") + return None, None voice_id = options.get(CONF_VOICE, self.default_voice) - voice_in_dict = self.all_voices.get(voice_id) + voice_in_dict = self.all_voices[voice_id] if language != voice_in_dict.get("LanguageCode"): _LOGGER.error("%s does not support the %s language", voice_id, language) return None, None diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index f2d72dbe4ad..be38eb6ec09 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -9,7 +9,7 @@ import logging import mimetypes import os import re -from typing import cast +from typing import Optional, Tuple, cast from aiohttp import web import mutagen @@ -47,6 +47,8 @@ from homeassistant.util.yaml import load_yaml _LOGGER = logging.getLogger(__name__) +TtsAudioType = Tuple[Optional[str], Optional[bytes]] + ATTR_CACHE = "cache" ATTR_LANGUAGE = "language" ATTR_MESSAGE = "message" diff --git a/mypy.ini b/mypy.ini index 396c51c8ac6..009112c1e0a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -99,6 +99,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.amazon_polly.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ampio.*] check_untyped_defs = true disallow_incomplete_defs = true From 0fac3ccebc25e24ba3661f4799752248034e97e4 Mon Sep 17 00:00:00 2001 From: nikito7 <45373783+nikito7@users.noreply.github.com> Date: Mon, 17 May 2021 13:12:17 +0100 Subject: [PATCH 517/852] Change Modbus error message to bytes (#50725) --- homeassistant/components/modbus/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index b30aaa3c3d9..614925b79a6 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -142,11 +142,13 @@ async def async_setup_platform( _LOGGER.error("Error in sensor %s structure: %s", entry[CONF_NAME], err) continue - if entry[CONF_COUNT] * 2 != size: + bytecount = entry[CONF_COUNT] * 2 + if bytecount != size: _LOGGER.error( - "Structure size (%d bytes) mismatch registers count (%d words)", + "Structure request %d bytes, but %d registers have a size of %d bytes", size, entry[CONF_COUNT], + bytecount, ) continue From 97559087b58489bf0c5d9709f3d4ef764043536f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 17 May 2021 14:13:01 +0200 Subject: [PATCH 518/852] Allow some failures before setting Xiaomi Miio MIOT air purifiers unavailable (#50755) --- homeassistant/components/xiaomi_miio/fan.py | 28 +++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 6d18131cdeb..a485654e638 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -551,7 +551,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if model in MODELS_PURIFIER_MIOT: air_purifier = AirPurifierMiot(host, token) - entity = XiaomiAirPurifierMiot(name, air_purifier, config_entry, unique_id) + entity = XiaomiAirPurifierMiot( + name, air_purifier, config_entry, unique_id, allowed_failures=2 + ) elif model.startswith("zhimi.airpurifier."): air_purifier = AirPurifier(host, token) entity = XiaomiAirPurifier(name, air_purifier, config_entry, unique_id) @@ -769,9 +771,11 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): class XiaomiAirPurifier(XiaomiGenericDevice): """Representation of a Xiaomi Air Purifier.""" - def __init__(self, name, device, entry, unique_id): + def __init__(self, name, device, entry, unique_id, allowed_failures=0): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id) + self._allowed_failures = allowed_failures + self._failure = 0 if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO @@ -822,10 +826,24 @@ class XiaomiAirPurifier(XiaomiGenericDevice): } ) + self._failure = 0 + except DeviceException as ex: - if self._available: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) + self._failure += 1 + if self._failure < self._allowed_failures: + _LOGGER.info( + "Got exception while fetching the state: %s, failure: %d", + ex, + self._failure, + ) + else: + if self._available: + self._available = False + _LOGGER.error( + "Got exception while fetching the state: %s, failure: %d", + ex, + self._failure, + ) @property def speed_list(self) -> list: From 6b34ba012cffa0ed5430e3e76f8a7c4606c3eb5b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 17 May 2021 14:20:51 +0200 Subject: [PATCH 519/852] Fix missing await in modbus platforms (followup on async PR) (#50710) --- homeassistant/components/modbus/climate.py | 2 +- homeassistant/components/modbus/cover.py | 4 ++-- homeassistant/components/modbus/switch.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 43c0f0d05db..41843fd4929 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -213,7 +213,7 @@ class ModbusThermostat(ClimateEntity): self._target_temperature_register, register_value, ) - self.async_update() + await self.async_update() @property def available(self) -> bool: diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 48dc08a18b9..bfe4ce1fb51 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -174,7 +174,7 @@ class ModbusCover(CoverEntity, RestoreEntity): else: await self._async_write_register(self._state_open) - self.async_update() + await self.async_update() async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" @@ -183,7 +183,7 @@ class ModbusCover(CoverEntity, RestoreEntity): else: await self._async_write_register(self._state_closed) - self.async_update() + await self.async_update() async def async_update(self, now=None): """Update the state of the cover.""" diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 4495a72c63a..9c8f2d1d12d 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -134,7 +134,7 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): else: self._available = True if self._verify_active: - self.async_update() + await self.async_update() else: self._is_on = True self.async_write_ha_state() @@ -150,7 +150,7 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): else: self._available = True if self._verify_active: - self.async_update() + await self.async_update() else: self._is_on = False self.async_write_ha_state() From add594a44b82be756598679b7ea5be4feebdbd8b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 17 May 2021 14:34:58 +0200 Subject: [PATCH 520/852] Clean up smhi redundant code (#50765) --- homeassistant/components/smhi/__init__.py | 6 ------ homeassistant/components/smhi/config_flow.py | 2 +- homeassistant/components/smhi/const.py | 1 - 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 70ee0aaa386..418c9ac32f1 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -2,12 +2,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -# Have to import for config_flow to work even if they are not used here -from .config_flow import smhi_locations # noqa: F401 -from .const import DOMAIN # noqa: F401 - -DEFAULT_NAME = "smhi" - PLATFORMS = ["weather"] diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 5c3572dd2fd..16be0c9fda4 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -21,7 +21,7 @@ from .const import DOMAIN, HOME_LOCATION_NAME def smhi_locations(hass: HomeAssistant) -> set[str]: """Return configurations of SMHI component.""" return { - (slugify(entry.data[CONF_NAME])) + slugify(entry.data[CONF_NAME]) for entry in hass.config_entries.async_entries(DOMAIN) } diff --git a/homeassistant/components/smhi/const.py b/homeassistant/components/smhi/const.py index 03b583b77ec..26eb506fb67 100644 --- a/homeassistant/components/smhi/const.py +++ b/homeassistant/components/smhi/const.py @@ -12,4 +12,3 @@ DOMAIN = "smhi" HOME_LOCATION_NAME = "Home" ENTITY_ID_SENSOR_FORMAT = WEATHER_DOMAIN + ".smhi_{}" -ENTITY_ID_SENSOR_FORMAT_HOME = ENTITY_ID_SENSOR_FORMAT.format(HOME_LOCATION_NAME) From b36021b4fd68e2015ba6b2511574a2652e84c218 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 May 2021 14:53:48 +0200 Subject: [PATCH 521/852] Deduplicate code in MQTT basic light pt2: Add restore_state helper (#50766) * Refactor MQTT basic light pt2: Add restore_state helper * Update homeassistant/components/mqtt/light/schema_basic.py Co-authored-by: Shay Levy Co-authored-by: Shay Levy --- .../components/mqtt/light/schema_basic.py | 74 ++++++------------- 1 file changed, 22 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 9f19bc6d70e..658531ca12d 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -155,11 +155,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT light.""" - self._state = False self._brightness = None - self._hs = None self._color_temp = None self._effect = None + self._hs_color = None + self._state = False self._white_value = None self._topic = None @@ -265,6 +265,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): "qos": self._config[CONF_QOS], } + def restore_state(attribute, condition): + """Restore a state attribute.""" + if condition and last_state and last_state.attributes.get(attribute): + setattr(self, f"_{attribute}", last_state.attributes[attribute]) + @callback @log_messages(self.hass, self.entity_id) def state_received(msg): @@ -308,12 +313,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.async_write_ha_state() add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) - if ( - self._optimistic_brightness - and last_state - and last_state.attributes.get(ATTR_BRIGHTNESS) - ): - self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) + restore_state(ATTR_BRIGHTNESS, self._optimistic_brightness) @callback @log_messages(self.hass, self.entity_id) @@ -325,19 +325,14 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return rgb = [int(val) for val in payload.split(",")] - self._hs = color_util.color_RGB_to_hs(*rgb) + self._hs_color = color_util.color_RGB_to_hs(*rgb) if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: percent_bright = float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0 self._brightness = percent_bright * 255 self.async_write_ha_state() add_topic(CONF_RGB_STATE_TOPIC, rgb_received) - if ( - self._optimistic_rgb - and last_state - and last_state.attributes.get(ATTR_HS_COLOR) - ): - self._hs = last_state.attributes.get(ATTR_HS_COLOR) + restore_state(ATTR_HS_COLOR, self._optimistic_rgb) @callback @log_messages(self.hass, self.entity_id) @@ -354,12 +349,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.async_write_ha_state() add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) - if ( - self._optimistic_color_temp - and last_state - and last_state.attributes.get(ATTR_COLOR_TEMP) - ): - self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + restore_state(ATTR_COLOR_TEMP, self._optimistic_color_temp) @callback @log_messages(self.hass, self.entity_id) @@ -376,12 +366,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.async_write_ha_state() add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) - if ( - self._optimistic_effect - and last_state - and last_state.attributes.get(ATTR_EFFECT) - ): - self._effect = last_state.attributes.get(ATTR_EFFECT) + restore_state(ATTR_EFFECT, self._optimistic_effect) @callback @log_messages(self.hass, self.entity_id) @@ -394,18 +379,13 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): try: hs_color = [float(val) for val in payload.split(",", 2)] - self._hs = hs_color + self._hs_color = hs_color self.async_write_ha_state() except ValueError: _LOGGER.debug("Failed to parse hs state update: '%s'", payload) add_topic(CONF_HS_STATE_TOPIC, hs_received) - if ( - self._optimistic_hs - and last_state - and last_state.attributes.get(ATTR_HS_COLOR) - ): - self._hs = last_state.attributes.get(ATTR_HS_COLOR) + restore_state(ATTR_HS_COLOR, self._optimistic_hs) @callback @log_messages(self.hass, self.entity_id) @@ -424,12 +404,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.async_write_ha_state() add_topic(CONF_WHITE_VALUE_STATE_TOPIC, white_value_received) - if ( - self._optimistic_white_value - and last_state - and last_state.attributes.get(ATTR_WHITE_VALUE) - ): - self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) + restore_state(ATTR_WHITE_VALUE, self._optimistic_white_value) @callback @log_messages(self.hass, self.entity_id) @@ -441,16 +416,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return xy_color = [float(val) for val in payload.split(",")] - self._hs = color_util.color_xy_to_hs(*xy_color) + self._hs_color = color_util.color_xy_to_hs(*xy_color) self.async_write_ha_state() add_topic(CONF_XY_STATE_TOPIC, xy_received) - if ( - self._optimistic_xy - and last_state - and last_state.attributes.get(ATTR_HS_COLOR) - ): - self._hs = last_state.attributes.get(ATTR_HS_COLOR) + restore_state(ATTR_HS_COLOR, self._optimistic_xy) self._sub_state = await subscription.async_subscribe_topics( self.hass, self._sub_state, topics @@ -469,7 +439,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): """Return the hs color value.""" if self._white_value: return None - return self._hs + return self._hs_color @property def color_temp(self): @@ -607,7 +577,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) if self._optimistic_rgb: - self._hs = kwargs[ATTR_HS_COLOR] + self._hs_color = kwargs[ATTR_HS_COLOR] should_update = True if ATTR_HS_COLOR in kwargs and self._topic[CONF_HS_COMMAND_TOPIC] is not None: @@ -622,7 +592,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) if self._optimistic_hs: - self._hs = kwargs[ATTR_HS_COLOR] + self._hs_color = kwargs[ATTR_HS_COLOR] should_update = True if ATTR_HS_COLOR in kwargs and self._topic[CONF_XY_COMMAND_TOPIC] is not None: @@ -637,7 +607,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) if self._optimistic_xy: - self._hs = kwargs[ATTR_HS_COLOR] + self._hs_color = kwargs[ATTR_HS_COLOR] should_update = True if ( @@ -667,7 +637,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): and ATTR_HS_COLOR not in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None ): - hs_color = self._hs if self._hs is not None else (0, 0) + hs_color = self._hs_color if self._hs_color is not None else (0, 0) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], kwargs[ATTR_BRIGHTNESS] / 255 * 100 ) From 8c6f4a8c7127cb2c03fd425ee0c2fcbeb933fbb6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 May 2021 14:54:19 +0200 Subject: [PATCH 522/852] Refactor MQTT basic light pt3: Add publish helper (#50767) --- .../components/mqtt/light/schema_basic.py | 82 ++++--------------- 1 file changed, 16 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 658531ca12d..372cc76becc 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -531,14 +531,18 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): should_update = False on_command_type = self._config[CONF_ON_COMMAND_TYPE] - if on_command_type == "first": + def publish(topic, payload): + """Publish an MQTT message.""" mqtt.async_publish( self.hass, - self._topic[CONF_COMMAND_TOPIC], - self._payload["on"], + self._topic[topic], + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], ) + + if on_command_type == "first": + publish(CONF_COMMAND_TOPIC, self._payload["on"]) should_update = True # If brightness is being used instead of an on command, make sure @@ -568,13 +572,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): else: rgb_color_str = f"{rgb[0]},{rgb[1]},{rgb[2]}" - mqtt.async_publish( - self.hass, - self._topic[CONF_RGB_COMMAND_TOPIC], - rgb_color_str, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + publish(CONF_RGB_COMMAND_TOPIC, rgb_color_str) if self._optimistic_rgb: self._hs_color = kwargs[ATTR_HS_COLOR] @@ -583,13 +581,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if ATTR_HS_COLOR in kwargs and self._topic[CONF_HS_COMMAND_TOPIC] is not None: hs_color = kwargs[ATTR_HS_COLOR] - mqtt.async_publish( - self.hass, - self._topic[CONF_HS_COMMAND_TOPIC], - f"{hs_color[0]},{hs_color[1]}", - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") if self._optimistic_hs: self._hs_color = kwargs[ATTR_HS_COLOR] @@ -598,13 +590,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if ATTR_HS_COLOR in kwargs and self._topic[CONF_XY_COMMAND_TOPIC] is not None: xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) - mqtt.async_publish( - self.hass, - self._topic[CONF_XY_COMMAND_TOPIC], - f"{xy_color[0]},{xy_color[1]}", - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") if self._optimistic_xy: self._hs_color = kwargs[ATTR_HS_COLOR] @@ -621,13 +607,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) # Make sure the brightness is not rounded down to 0 device_brightness = max(device_brightness, 1) - mqtt.async_publish( - self.hass, - self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC], - device_brightness, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + publish(CONF_BRIGHTNESS_COMMAND_TOPIC, device_brightness) if self._optimistic_brightness: self._brightness = kwargs[ATTR_BRIGHTNESS] @@ -647,13 +627,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): else: rgb_color_str = f"{rgb[0]},{rgb[1]},{rgb[2]}" - mqtt.async_publish( - self.hass, - self._topic[CONF_RGB_COMMAND_TOPIC], - rgb_color_str, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + publish(CONF_RGB_COMMAND_TOPIC, rgb_color_str) if self._optimistic_brightness: self._brightness = kwargs[ATTR_BRIGHTNESS] @@ -669,13 +643,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if tpl: color_temp = tpl({"value": color_temp}) - mqtt.async_publish( - self.hass, - self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC], - color_temp, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp) if self._optimistic_color_temp: self._color_temp = kwargs[ATTR_COLOR_TEMP] @@ -684,13 +652,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if ATTR_EFFECT in kwargs and self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: effect = kwargs[ATTR_EFFECT] if effect in self._config.get(CONF_EFFECT_LIST): - mqtt.async_publish( - self.hass, - self._topic[CONF_EFFECT_COMMAND_TOPIC], - effect, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + publish(CONF_EFFECT_COMMAND_TOPIC, effect) if self._optimistic_effect: self._effect = kwargs[ATTR_EFFECT] @@ -703,26 +665,14 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): percent_white = float(kwargs[ATTR_WHITE_VALUE]) / 255 white_scale = self._config[CONF_WHITE_VALUE_SCALE] device_white_value = min(round(percent_white * white_scale), white_scale) - mqtt.async_publish( - self.hass, - self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC], - device_white_value, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + publish(CONF_WHITE_VALUE_COMMAND_TOPIC, device_white_value) if self._optimistic_white_value: self._white_value = kwargs[ATTR_WHITE_VALUE] should_update = True if on_command_type == "last": - mqtt.async_publish( - self.hass, - self._topic[CONF_COMMAND_TOPIC], - self._payload["on"], - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + publish(CONF_COMMAND_TOPIC, self._payload["on"]) should_update = True if self._optimistic: From 9afa7df3c1b201ac6b574e5ce6926ad711e82332 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 17 May 2021 15:06:36 +0200 Subject: [PATCH 523/852] Upgrade apprise to 0.9.3 (#50769) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index f9e6305678a..3e87d38ff69 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==0.8.9"], + "requirements": ["apprise==0.9.3"], "codeowners": ["@caronc"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 28d5d9cc138..3f7529169df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -272,7 +272,7 @@ apcaccess==0.0.13 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.9 +apprise==0.9.3 # homeassistant.components.aprs aprslib==0.6.46 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c49bc65c270..d6244443a9a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,7 +176,7 @@ androidtv[async]==0.0.59 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.9 +apprise==0.9.3 # homeassistant.components.aprs aprslib==0.6.46 From ac6d99d434e13688467987db8b358ca788a99b3f Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 17 May 2021 15:33:09 +0200 Subject: [PATCH 524/852] Create KNX binary_sensor entities directly from config (#50708) --- homeassistant/components/knx/binary_sensor.py | 37 +++++++++++++++---- homeassistant/components/knx/factory.py | 25 +------------ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 9a271ec965f..3b2f4b7a75a 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -3,9 +3,11 @@ from __future__ import annotations from typing import Any +from xknx import XKNX from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -13,6 +15,7 @@ from homeassistant.util import dt from .const import ATTR_COUNTER, ATTR_LAST_KNX_UPDATE, ATTR_SOURCE, DOMAIN from .knx_entity import KnxEntity +from .schema import BinarySensorSchema async def async_setup_platform( @@ -22,27 +25,47 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up binary sensor(s) for KNX platform.""" + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + entities = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxBinarySensor): - entities.append(KNXBinarySensor(device)) + for entity_config in platform_config: + entities.append(KNXBinarySensor(xknx, entity_config)) + async_add_entities(entities) class KNXBinarySensor(KnxEntity, BinarySensorEntity): """Representation of a KNX binary sensor.""" - def __init__(self, device: XknxBinarySensor) -> None: + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX binary sensor.""" self._device: XknxBinarySensor - super().__init__(device) + super().__init__( + device=XknxBinarySensor( + xknx, + name=config[CONF_NAME], + group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS], + invert=config[BinarySensorSchema.CONF_INVERT], + sync_state=config[BinarySensorSchema.CONF_SYNC_STATE], + ignore_internal_state=config[ + BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE + ], + context_timeout=config.get(BinarySensorSchema.CONF_CONTEXT_TIMEOUT), + reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), + ) + ) + self._device_class: str | None = config.get(CONF_DEVICE_CLASS) self._unique_id = f"{self._device.remote_value.group_address_state}" @property def device_class(self) -> str | None: """Return the class of this sensor.""" - if self._device.device_class in DEVICE_CLASSES: - return self._device.device_class + if self._device_class in DEVICE_CLASSES: + return self._device_class return None @property diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index a8092fe03de..3c1ef0fdae4 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -3,7 +3,6 @@ from __future__ import annotations from xknx import XKNX from xknx.devices import ( - BinarySensor as XknxBinarySensor, Climate as XknxClimate, ClimateMode as XknxClimateMode, Device as XknxDevice, @@ -11,11 +10,11 @@ from xknx.devices import ( Weather as XknxWeather, ) -from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE +from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.helpers.typing import ConfigType from .const import SupportedPlatforms -from .schema import BinarySensorSchema, ClimateSchema, SensorSchema, WeatherSchema +from .schema import ClimateSchema, SensorSchema, WeatherSchema def create_knx_device( @@ -30,9 +29,6 @@ def create_knx_device( if platform is SupportedPlatforms.SENSOR: return _create_sensor(knx_module, config) - if platform is SupportedPlatforms.BINARY_SENSOR: - return _create_binary_sensor(knx_module, config) - if platform is SupportedPlatforms.WEATHER: return _create_weather(knx_module, config) @@ -126,23 +122,6 @@ def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor: ) -def _create_binary_sensor(knx_module: XKNX, config: ConfigType) -> XknxBinarySensor: - """Return a KNX binary sensor to be used within XKNX.""" - device_name = config[CONF_NAME] - - return XknxBinarySensor( - knx_module, - name=device_name, - group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS], - invert=config[BinarySensorSchema.CONF_INVERT], - sync_state=config[BinarySensorSchema.CONF_SYNC_STATE], - device_class=config.get(CONF_DEVICE_CLASS), - ignore_internal_state=config[BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE], - context_timeout=config.get(BinarySensorSchema.CONF_CONTEXT_TIMEOUT), - reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), - ) - - def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather: """Return a KNX weather device to be used within XKNX.""" return XknxWeather( From 2f10f5971788687792b9d9753925ff1fa2225284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 17 May 2021 15:48:41 +0200 Subject: [PATCH 525/852] Block custom integrations with missing or invalid version (#49916) --- homeassistant/loader.py | 114 ++++++------------ homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- script/hassfest/manifest.py | 27 +++-- setup.py | 2 +- .../alarm_control_panel/test_device_action.py | 10 +- tests/components/analytics/test_analytics.py | 2 +- .../binary_sensor/test_device_condition.py | 6 +- .../binary_sensor/test_device_trigger.py | 8 +- .../components/config/test_config_entries.py | 4 +- tests/components/cover/test_device_action.py | 32 +++-- .../components/cover/test_device_condition.py | 26 ++-- tests/components/cover/test_device_trigger.py | 26 ++-- .../components/device_automation/test_init.py | 14 ++- .../device_sun_light_trigger/test_init.py | 4 +- .../device_tracker/test_entities.py | 2 +- tests/components/device_tracker/test_init.py | 38 ++++-- tests/components/flux/test_switch.py | 56 ++++++--- .../generic_thermostat/test_climate.py | 2 +- tests/components/group/test_light.py | 18 +-- .../components/image_processing/test_init.py | 2 + tests/components/light/test_device_action.py | 2 +- .../components/light/test_device_condition.py | 4 +- tests/components/light/test_device_trigger.py | 6 +- tests/components/light/test_init.py | 40 +++--- tests/components/lock/test_device_action.py | 8 +- tests/components/remote/test_device_action.py | 2 +- .../remote/test_device_condition.py | 4 +- .../components/remote/test_device_trigger.py | 6 +- tests/components/scene/test_init.py | 6 +- .../sensor/test_device_condition.py | 20 +-- .../components/sensor/test_device_trigger.py | 24 ++-- tests/components/switch/test_device_action.py | 2 +- .../switch/test_device_condition.py | 4 +- .../components/switch/test_device_trigger.py | 6 +- tests/components/switch/test_init.py | 6 +- tests/components/trace/test_websocket_api.py | 5 +- tests/components/webhook/test_init.py | 4 +- tests/hassfest/test_version.py | 7 +- tests/helpers/test_translation.py | 23 +--- tests/test_loader.py | 92 ++++---------- .../custom_components/test/manifest.json | 3 +- .../test_embedded/manifest.json | 9 ++ .../test_package/manifest.json | 3 +- 44 files changed, 369 insertions(+), 314 deletions(-) create mode 100644 tests/testing_config/custom_components/test_embedded/manifest.json diff --git a/homeassistant/loader.py b/homeassistant/loader.py index cdf9a831450..abc5e533df5 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -17,7 +17,11 @@ import sys from types import ModuleType from typing import TYPE_CHECKING, Any, Callable, Dict, TypedDict, TypeVar, cast -from awesomeversion import AwesomeVersion, AwesomeVersionStrategy +from awesomeversion import ( + AwesomeVersion, + AwesomeVersionException, + AwesomeVersionStrategy, +) from homeassistant.generated.dhcp import DHCP from homeassistant.generated.mqtt import MQTT @@ -48,17 +52,7 @@ CUSTOM_WARNING = ( "cause stability problems, be sure to disable it if you " "experience issues with Home Assistant" ) -CUSTOM_WARNING_VERSION_MISSING = ( - "No 'version' key in the manifest file for " - "custom integration '%s'. As of Home Assistant " - "2021.6, this integration will no longer be " - "loaded. Please report this to the maintainer of '%s'" -) -CUSTOM_WARNING_VERSION_TYPE = ( - "'%s' is not a valid version for " - "custom integration '%s'. " - "Please report this to the maintainer of '%s'" -) + _UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency MAX_LOAD_CONCURRENTLY = 4 @@ -297,29 +291,14 @@ class Integration: continue return cls( - hass, f"{root_module.__name__}.{domain}", manifest_path.parent, manifest + hass, + f"{root_module.__name__}.{domain}", + manifest_path.parent, + manifest, ) return None - @classmethod - def resolve_legacy(cls, hass: HomeAssistant, domain: str) -> Integration | None: - """Resolve legacy component. - - Will create a stub manifest. - """ - comp = _load_file(hass, domain, _lookup_path(hass)) - - if comp is None: - return None - - return cls( - hass, - comp.__name__, - pathlib.Path(comp.__file__).parent, - manifest_from_legacy_module(domain, comp), - ) - def __init__( self, hass: HomeAssistant, @@ -531,7 +510,8 @@ async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration # components to find the integration. integration = (await async_get_custom_components(hass)).get(domain) if integration is not None: - custom_integration_warning(integration) + validate_custom_integration_version(integration) + _LOGGER.warning(CUSTOM_WARNING, integration.domain) cache[domain] = integration event.set() return integration @@ -541,25 +521,15 @@ async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration integration = await hass.async_add_executor_job( Integration.resolve_from_root, hass, components, domain ) - - if integration is not None: - cache[domain] = integration - event.set() - return integration - - integration = Integration.resolve_legacy(hass, domain) - if integration is not None: - custom_integration_warning(integration) - cache[domain] = integration - else: - # Remove event from cache. - cache.pop(domain) - event.set() if not integration: + # Remove event from cache. + cache.pop(domain) raise IntegrationNotFound(domain) + cache[domain] = integration + return integration @@ -772,33 +742,29 @@ def _lookup_path(hass: HomeAssistant) -> list[str]: return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] -def validate_custom_integration_version(version: str) -> bool: - """Validate the version of custom integrations.""" - return AwesomeVersion(version).strategy in ( - AwesomeVersionStrategy.CALVER, - AwesomeVersionStrategy.SEMVER, - AwesomeVersionStrategy.SIMPLEVER, - AwesomeVersionStrategy.BUILDVER, - AwesomeVersionStrategy.PEP440, - ) +def validate_custom_integration_version(integration: Integration) -> None: + """ + Validate the version of custom integrations. - -def custom_integration_warning(integration: Integration) -> None: - """Create logs for custom integrations.""" - if not integration.pkg_path.startswith(PACKAGE_CUSTOM_COMPONENTS): - return None - - _LOGGER.warning(CUSTOM_WARNING, integration.domain) - - if integration.manifest.get("version") is None: - _LOGGER.error( - CUSTOM_WARNING_VERSION_MISSING, integration.domain, integration.domain + Raises IntegrationNotFound when version is missing or not valid + """ + try: + AwesomeVersion( + integration.version, + [ + AwesomeVersionStrategy.CALVER, + AwesomeVersionStrategy.SEMVER, + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, + ], ) - else: - if not validate_custom_integration_version(integration.manifest["version"]): - _LOGGER.error( - CUSTOM_WARNING_VERSION_TYPE, - integration.manifest["version"], - integration.domain, - integration.domain, - ) + except AwesomeVersionException: + _LOGGER.error( + "The custom integration '%s' does not have a " + "valid version key (%s) in the manifest file and was blocked from loading. " + "See https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions for more details", + integration.domain, + integration.version, + ) + raise IntegrationNotFound(integration.domain) from None diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 18421821d2a..d957f9c7cd9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ astral==2.2 async-upnp-client==0.17.0 async_timeout==3.0.1 attrs==21.2.0 -awesomeversion==21.2.3 +awesomeversion==21.4.0 backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2020.12.5 diff --git a/requirements.txt b/requirements.txt index 39a30934d4c..4661a23a70f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ aiohttp==3.7.4.post0 astral==2.2 async_timeout==3.0.1 attrs==21.2.0 -awesomeversion==21.2.3 +awesomeversion==21.4.0 backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2020.12.5 diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 016e3a0a322..a8e1858cad3 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -4,11 +4,14 @@ from __future__ import annotations from pathlib import Path from urllib.parse import urlparse +from awesomeversion import ( + AwesomeVersion, + AwesomeVersionException, + AwesomeVersionStrategy, +) import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant.loader import validate_custom_integration_version - from .model import Config, Integration DOCUMENTATION_URL_SCHEMA = "https" @@ -142,10 +145,19 @@ def verify_uppercase(value: str): def verify_version(value: str): """Verify the version.""" - if not validate_custom_integration_version(value): - raise vol.Invalid( - f"'{value}' is not a valid version. This will cause a future version of Home Assistant to block this integration.", + try: + AwesomeVersion( + value, + [ + AwesomeVersionStrategy.CALVER, + AwesomeVersionStrategy.SEMVER, + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, + ], ) + except AwesomeVersionException: + raise vol.Invalid(f"'{value}' is not a valid version.") return value @@ -221,10 +233,7 @@ def validate_version(integration: Integration): Will be removed when the version key is no longer optional for custom integrations. """ if not integration.manifest.get("version"): - integration.add_error( - "manifest", - "No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration.", - ) + integration.add_error("manifest", "No 'version' key in the manifest file.") return diff --git a/setup.py b/setup.py index 43a8107743e..d987f4671b4 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ REQUIRES = [ "astral==2.2", "async_timeout==3.0.1", "attrs==21.2.0", - "awesomeversion==21.2.3", + "awesomeversion==21.4.0", 'backports.zoneinfo;python_version<"3.9"', "bcrypt==3.1.7", "certifi>=2020.12.5", diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index 2b50f83fd8d..a4ec802f3f5 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -116,7 +116,9 @@ async def test_get_actions_arm_night_only(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_action_capabilities(hass, device_reg, entity_reg): +async def test_get_action_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -154,7 +156,9 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities[action["type"]] -async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg): +async def test_get_action_capabilities_arm_code( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -198,7 +202,7 @@ async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg): assert capabilities == expected_capabilities[action["type"]] -async def test_action(hass): +async def test_action(hass, enable_custom_integrations): """Test for turn_on and turn_off actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index ea871081f02..09f82e37fba 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -354,7 +354,7 @@ async def test_reusing_uuid(hass, aioclient_mock): assert analytics.uuid == "NOT_MOCK_UUID" -async def test_custom_integrations(hass, aioclient_mock): +async def test_custom_integrations(hass, aioclient_mock, enable_custom_integrations): """Test sending custom integrations.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index d8c9e1ca894..5d8673825fc 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -41,7 +41,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_conditions(hass, device_reg, entity_reg): +async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected conditions from a binary_sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -100,7 +100,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_state(hass, calls): +async def test_if_state(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -173,7 +173,7 @@ async def test_if_state(hass, calls): assert calls[1].data["some"] == "is_off event - test_event2" -async def test_if_fires_on_for_condition(hass, calls): +async def test_if_fires_on_for_condition(hass, calls, enable_custom_integrations): """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 9b50d52b785..0e5cbcc1d70 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -41,7 +41,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg): +async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected triggers from a binary_sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -100,7 +100,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_fires_on_state_change(hass, calls): +async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations): """Test for on and off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -184,7 +184,9 @@ async def test_if_fires_on_state_change(hass, calls): ) -async def test_if_fires_on_state_change_with_for(hass, calls): +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations +): """Test for triggers firing with delay.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 2763e5912fa..1ad2bd978fc 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -287,7 +287,7 @@ async def test_abort(hass, client): } -async def test_create_account(hass, client): +async def test_create_account(hass, client, enable_custom_integrations): """Test a flow that creates an account.""" mock_entity_platform(hass, "config_flow.test", None) @@ -337,7 +337,7 @@ async def test_create_account(hass, client): } -async def test_two_step_flow(hass, client): +async def test_two_step_flow(hass, client, enable_custom_integrations): """Test we can finish a two step flow.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 5cec3d901e1..60bb4e5401b 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -31,7 +31,7 @@ def entity_reg(hass): return mock_registry(hass) -async def test_get_actions(hass, device_reg, entity_reg): +async def test_get_actions(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected actions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -73,7 +73,9 @@ async def test_get_actions(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_actions_tilt(hass, device_reg, entity_reg): +async def test_get_actions_tilt( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected actions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -127,7 +129,9 @@ async def test_get_actions_tilt(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_actions_set_pos(hass, device_reg, entity_reg): +async def test_get_actions_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected actions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -157,7 +161,9 @@ async def test_get_actions_set_pos(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_actions_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_actions_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected actions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -205,7 +211,9 @@ async def test_get_actions_set_tilt_pos(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_action_capabilities(hass, device_reg, entity_reg): +async def test_get_action_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover action.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -233,7 +241,9 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): assert capabilities == {"extra_fields": []} -async def test_get_action_capabilities_set_pos(hass, device_reg, entity_reg): +async def test_get_action_capabilities_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover action.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -276,7 +286,9 @@ async def test_get_action_capabilities_set_pos(hass, device_reg, entity_reg): assert capabilities == {"extra_fields": []} -async def test_get_action_capabilities_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_action_capabilities_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover action.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -319,7 +331,7 @@ async def test_get_action_capabilities_set_tilt_pos(hass, device_reg, entity_reg assert capabilities == {"extra_fields": []} -async def test_action(hass): +async def test_action(hass, enable_custom_integrations): """Test for cover actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -385,7 +397,7 @@ async def test_action(hass): assert len(stop_calls) == 1 -async def test_action_tilt(hass): +async def test_action_tilt(hass, enable_custom_integrations): """Test for cover tilt actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -438,7 +450,7 @@ async def test_action_tilt(hass): assert len(close_calls) == 1 -async def test_action_set_position(hass): +async def test_action_set_position(hass, enable_custom_integrations): """Test for cover set position actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 04415661515..8e8d92e5a1c 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -43,7 +43,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_conditions(hass, device_reg, entity_reg): +async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected conditions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -94,7 +94,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert_lists_same(conditions, expected_conditions) -async def test_get_conditions_set_pos(hass, device_reg, entity_reg): +async def test_get_conditions_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected conditions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -152,7 +154,9 @@ async def test_get_conditions_set_pos(hass, device_reg, entity_reg): assert_lists_same(conditions, expected_conditions) -async def test_get_conditions_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_conditions_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected conditions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -210,7 +214,9 @@ async def test_get_conditions_set_tilt_pos(hass, device_reg, entity_reg): assert_lists_same(conditions, expected_conditions) -async def test_get_condition_capabilities(hass, device_reg, entity_reg): +async def test_get_condition_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -237,7 +243,9 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == {"extra_fields": []} -async def test_get_condition_capabilities_set_pos(hass, device_reg, entity_reg): +async def test_get_condition_capabilities_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -287,7 +295,9 @@ async def test_get_condition_capabilities_set_pos(hass, device_reg, entity_reg): assert capabilities == {"extra_fields": []} -async def test_get_condition_capabilities_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_condition_capabilities_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -449,7 +459,7 @@ async def test_if_state(hass, calls): assert calls[3].data["some"] == "is_closing - event - test_event4" -async def test_if_position(hass, calls): +async def test_if_position(hass, calls, enable_custom_integrations): """Test for position conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -553,7 +563,7 @@ async def test_if_position(hass, calls): assert calls[4].data["some"] == "is_pos_gt_45 - event - test_event1" -async def test_if_tilt_position(hass, calls): +async def test_if_tilt_position(hass, calls, enable_custom_integrations): """Test for tilt position conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 7ff5a434e5b..8732fcc8020 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -47,7 +47,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg): +async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected triggers from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -98,7 +98,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): assert_lists_same(triggers, expected_triggers) -async def test_get_triggers_set_pos(hass, device_reg, entity_reg): +async def test_get_triggers_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected triggers from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -156,7 +158,9 @@ async def test_get_triggers_set_pos(hass, device_reg, entity_reg): assert_lists_same(triggers, expected_triggers) -async def test_get_triggers_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_triggers_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected triggers from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -214,7 +218,9 @@ async def test_get_triggers_set_tilt_pos(hass, device_reg, entity_reg): assert_lists_same(triggers, expected_triggers) -async def test_get_trigger_capabilities(hass, device_reg, entity_reg): +async def test_get_trigger_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -245,7 +251,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): } -async def test_get_trigger_capabilities_set_pos(hass, device_reg, entity_reg): +async def test_get_trigger_capabilities_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -303,7 +311,9 @@ async def test_get_trigger_capabilities_set_pos(hass, device_reg, entity_reg): } -async def test_get_trigger_capabilities_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_trigger_capabilities_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -538,7 +548,7 @@ async def test_if_fires_on_state_change_with_for(hass, calls): ) -async def test_if_fires_on_position(hass, calls): +async def test_if_fires_on_position(hass, calls, enable_custom_integrations): """Test for position triggers.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -665,7 +675,7 @@ async def test_if_fires_on_position(hass, calls): ) -async def test_if_fires_on_tilt_position(hass, calls): +async def test_if_fires_on_tilt_position(hass, calls, enable_custom_integrations): """Test for tilt position triggers.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index a2f042bcd73..7c16d067eff 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -490,7 +490,9 @@ 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): +async def test_automation_with_integration_without_device_action( + hass, caplog, enable_custom_integrations +): """Test automation with integration without device action support.""" assert await async_setup_component( hass, @@ -509,7 +511,9 @@ async def test_automation_with_integration_without_device_action(hass, caplog): ) -async def test_automation_with_integration_without_device_condition(hass, caplog): +async def test_automation_with_integration_without_device_condition( + hass, caplog, enable_custom_integrations +): """Test automation with integration without device condition support.""" assert await async_setup_component( hass, @@ -534,7 +538,9 @@ async def test_automation_with_integration_without_device_condition(hass, caplog ) -async def test_automation_with_integration_without_device_trigger(hass, caplog): +async def test_automation_with_integration_without_device_trigger( + hass, caplog, enable_custom_integrations +): """Test automation with integration without device trigger support.""" assert await async_setup_component( hass, @@ -615,7 +621,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_automation_with_sub_condition(hass, calls): +async def test_automation_with_sub_condition(hass, calls, enable_custom_integrations): """Test automation with device condition under and/or conditions.""" DOMAIN = "light" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 7d478e7d8d1..f7d835427a1 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -29,7 +29,7 @@ from tests.common import async_fire_time_changed @pytest.fixture -def scanner(hass): +def scanner(hass, enable_custom_integrations): """Initialize components.""" scanner = getattr(hass.components, "test.device_tracker").get_scanner(None, None) @@ -100,7 +100,7 @@ async def test_lights_on_when_sun_sets(hass, scanner): ) -async def test_lights_turn_off_when_everyone_leaves(hass): +async def test_lights_turn_off_when_everyone_leaves(hass, enable_custom_integrations): """Test lights turn off when everyone leaves the house.""" assert await async_setup_component( hass, "light", {light.DOMAIN: {CONF_PLATFORM: "test"}} diff --git a/tests/components/device_tracker/test_entities.py b/tests/components/device_tracker/test_entities.py index 1bc058a1449..88e1dccdb34 100644 --- a/tests/components/device_tracker/test_entities.py +++ b/tests/components/device_tracker/test_entities.py @@ -18,7 +18,7 @@ from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_HOME, STATE_NOT_HOME from tests.common import MockConfigEntry -async def test_scanner_entity_device_tracker(hass): +async def test_scanner_entity_device_tracker(hass, enable_custom_integrations): """Test ScannerEntity based device tracker.""" config_entry = MockConfigEntry(domain="test") config_entry.add_to_hass(hass) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 6155ed7d1db..bf72cb34119 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -94,7 +94,7 @@ async def test_reading_broken_yaml_config(hass): assert res[0].dev_id == "my_device" -async def test_reading_yaml_config(hass, yaml_devices): +async def test_reading_yaml_config(hass, yaml_devices, enable_custom_integrations): """Test the rendering of the YAML configuration.""" dev_id = "test" device = legacy.Device( @@ -161,7 +161,7 @@ async def test_duplicate_mac_dev_id(mock_warning, hass): assert "Duplicate device IDs" in args[0], "Duplicate device IDs warning expected" -async def test_setup_without_yaml_file(hass): +async def test_setup_without_yaml_file(hass, enable_custom_integrations): """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -224,7 +224,7 @@ async def test_discover_platform(mock_demo_setup_scanner, mock_see, hass): ) -async def test_update_stale(hass, mock_device_tracker_conf): +async def test_update_stale(hass, mock_device_tracker_conf, enable_custom_integrations): """Test stalled update.""" scanner = getattr(hass.components, "test.device_tracker").SCANNER @@ -265,7 +265,9 @@ async def test_update_stale(hass, mock_device_tracker_conf): assert hass.states.get("device_tracker.dev1").state == STATE_NOT_HOME -async def test_entity_attributes(hass, mock_device_tracker_conf): +async def test_entity_attributes( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test the entity attributes.""" devices = mock_device_tracker_conf dev_id = "test_entity" @@ -297,7 +299,7 @@ async def test_entity_attributes(hass, mock_device_tracker_conf): @patch("homeassistant.components.device_tracker.legacy." "DeviceTracker.async_see") -async def test_see_service(mock_see, hass): +async def test_see_service(mock_see, hass, enable_custom_integrations): """Test the see service with a unicode dev_id and NO MAC.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -324,7 +326,9 @@ async def test_see_service(mock_see, hass): assert mock_see.call_args == call(**params) -async def test_see_service_guard_config_entry(hass, mock_device_tracker_conf): +async def test_see_service_guard_config_entry( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test the guard if the device is registered in the entity registry.""" mock_entry = Mock() dev_id = "test" @@ -340,7 +344,9 @@ async def test_see_service_guard_config_entry(hass, mock_device_tracker_conf): assert not devices -async def test_new_device_event_fired(hass, mock_device_tracker_conf): +async def test_new_device_event_fired( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test that the device tracker will fire an event.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -370,7 +376,9 @@ async def test_new_device_event_fired(hass, mock_device_tracker_conf): } -async def test_duplicate_yaml_keys(hass, mock_device_tracker_conf): +async def test_duplicate_yaml_keys( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test that the device tracker will not generate invalid YAML.""" devices = mock_device_tracker_conf with assert_setup_component(1, device_tracker.DOMAIN): @@ -385,7 +393,9 @@ async def test_duplicate_yaml_keys(hass, mock_device_tracker_conf): assert devices[0].dev_id != devices[1].dev_id -async def test_invalid_dev_id(hass, mock_device_tracker_conf): +async def test_invalid_dev_id( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test that the device tracker will not allow invalid dev ids.""" devices = mock_device_tracker_conf with assert_setup_component(1, device_tracker.DOMAIN): @@ -397,7 +407,7 @@ async def test_invalid_dev_id(hass, mock_device_tracker_conf): assert not devices -async def test_see_state(hass, yaml_devices): +async def test_see_state(hass, yaml_devices, enable_custom_integrations): """Test device tracker see records state correctly.""" assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -433,7 +443,9 @@ async def test_see_state(hass, yaml_devices): assert attrs["number"] == 1 -async def test_see_passive_zone_state(hass, mock_device_tracker_conf): +async def test_see_passive_zone_state( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test that the device tracker sets gps for passive trackers.""" now = dt_util.utcnow() @@ -562,7 +574,9 @@ async def test_bad_platform(hass): assert f"{device_tracker.DOMAIN}.bad_platform" not in hass.config.components -async def test_adding_unknown_device_to_config(mock_device_tracker_conf, hass): +async def test_adding_unknown_device_to_config( + mock_device_tracker_conf, hass, enable_custom_integrations +): """Test the adding of unknown devices to configuration file.""" scanner = getattr(hass.components, "test.device_tracker").SCANNER scanner.reset() diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index 7438b690ab5..e331a788e9c 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -127,7 +127,9 @@ async def test_invalid_config_no_lights(hass): await hass.async_block_till_done() -async def test_flux_when_switch_is_off(hass, legacy_patchable_time): +async def test_flux_when_switch_is_off( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch when it is off.""" platform = getattr(hass.components, "test.light") platform.init() @@ -178,7 +180,9 @@ async def test_flux_when_switch_is_off(hass, legacy_patchable_time): assert not turn_on_calls -async def test_flux_before_sunrise(hass, legacy_patchable_time): +async def test_flux_before_sunrise( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch before sunrise.""" platform = getattr(hass.components, "test.light") platform.init() @@ -237,7 +241,9 @@ async def test_flux_before_sunrise(hass, legacy_patchable_time): assert call.data[light.ATTR_XY_COLOR] == [0.606, 0.379] -async def test_flux_before_sunrise_known_location(hass, legacy_patchable_time): +async def test_flux_before_sunrise_known_location( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch before sunrise.""" platform = getattr(hass.components, "test.light") platform.init() @@ -296,7 +302,9 @@ async def test_flux_before_sunrise_known_location(hass, legacy_patchable_time): # pylint: disable=invalid-name -async def test_flux_after_sunrise_before_sunset(hass, legacy_patchable_time): +async def test_flux_after_sunrise_before_sunset( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch after sunrise and before sunset.""" platform = getattr(hass.components, "test.light") platform.init() @@ -355,7 +363,9 @@ async def test_flux_after_sunrise_before_sunset(hass, legacy_patchable_time): # pylint: disable=invalid-name -async def test_flux_after_sunset_before_stop(hass, legacy_patchable_time): +async def test_flux_after_sunset_before_stop( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch after sunset and before stop.""" platform = getattr(hass.components, "test.light") platform.init() @@ -415,7 +425,9 @@ async def test_flux_after_sunset_before_stop(hass, legacy_patchable_time): # pylint: disable=invalid-name -async def test_flux_after_stop_before_sunrise(hass, legacy_patchable_time): +async def test_flux_after_stop_before_sunrise( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch after stop and before sunrise.""" platform = getattr(hass.components, "test.light") platform.init() @@ -474,7 +486,9 @@ async def test_flux_after_stop_before_sunrise(hass, legacy_patchable_time): # pylint: disable=invalid-name -async def test_flux_with_custom_start_stop_times(hass, legacy_patchable_time): +async def test_flux_with_custom_start_stop_times( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux with custom start and stop times.""" platform = getattr(hass.components, "test.light") platform.init() @@ -534,7 +548,9 @@ async def test_flux_with_custom_start_stop_times(hass, legacy_patchable_time): assert call.data[light.ATTR_XY_COLOR] == [0.504, 0.385] -async def test_flux_before_sunrise_stop_next_day(hass, legacy_patchable_time): +async def test_flux_before_sunrise_stop_next_day( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch before sunrise. This test has the stop_time on the next day (after midnight). @@ -598,7 +614,7 @@ async def test_flux_before_sunrise_stop_next_day(hass, legacy_patchable_time): # pylint: disable=invalid-name async def test_flux_after_sunrise_before_sunset_stop_next_day( - hass, legacy_patchable_time + hass, legacy_patchable_time, enable_custom_integrations ): """ Test the flux switch after sunrise and before sunset. @@ -665,7 +681,7 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day( # pylint: disable=invalid-name @pytest.mark.parametrize("x", [0, 1]) async def test_flux_after_sunset_before_midnight_stop_next_day( - hass, legacy_patchable_time, x + hass, legacy_patchable_time, x, enable_custom_integrations ): """Test the flux switch after sunset and before stop. @@ -730,7 +746,7 @@ async def test_flux_after_sunset_before_midnight_stop_next_day( # pylint: disable=invalid-name async def test_flux_after_sunset_after_midnight_stop_next_day( - hass, legacy_patchable_time + hass, legacy_patchable_time, enable_custom_integrations ): """Test the flux switch after sunset and before stop. @@ -795,7 +811,7 @@ async def test_flux_after_sunset_after_midnight_stop_next_day( # pylint: disable=invalid-name async def test_flux_after_stop_before_sunrise_stop_next_day( - hass, legacy_patchable_time + hass, legacy_patchable_time, enable_custom_integrations ): """Test the flux switch after stop and before sunrise. @@ -859,7 +875,9 @@ async def test_flux_after_stop_before_sunrise_stop_next_day( # pylint: disable=invalid-name -async def test_flux_with_custom_colortemps(hass, legacy_patchable_time): +async def test_flux_with_custom_colortemps( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux with custom start and stop colortemps.""" platform = getattr(hass.components, "test.light") platform.init() @@ -921,7 +939,9 @@ async def test_flux_with_custom_colortemps(hass, legacy_patchable_time): # pylint: disable=invalid-name -async def test_flux_with_custom_brightness(hass, legacy_patchable_time): +async def test_flux_with_custom_brightness( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux with custom start and stop colortemps.""" platform = getattr(hass.components, "test.light") platform.init() @@ -981,7 +1001,9 @@ async def test_flux_with_custom_brightness(hass, legacy_patchable_time): assert call.data[light.ATTR_XY_COLOR] == [0.506, 0.385] -async def test_flux_with_multiple_lights(hass, legacy_patchable_time): +async def test_flux_with_multiple_lights( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch with multiple light entities.""" platform = getattr(hass.components, "test.light") platform.init() @@ -1064,7 +1086,7 @@ async def test_flux_with_multiple_lights(hass, legacy_patchable_time): assert call.data[light.ATTR_XY_COLOR] == [0.46, 0.376] -async def test_flux_with_mired(hass, legacy_patchable_time): +async def test_flux_with_mired(hass, legacy_patchable_time, enable_custom_integrations): """Test the flux switch´s mode mired.""" platform = getattr(hass.components, "test.light") platform.init() @@ -1121,7 +1143,7 @@ async def test_flux_with_mired(hass, legacy_patchable_time): assert call.data[light.ATTR_COLOR_TEMP] == 269 -async def test_flux_with_rgb(hass, legacy_patchable_time): +async def test_flux_with_rgb(hass, legacy_patchable_time, enable_custom_integrations): """Test the flux switch´s mode rgb.""" platform = getattr(hass.components, "test.light") platform.init() diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index a7f42fffbd8..4b9fbca41e2 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -125,7 +125,7 @@ async def test_heater_input_boolean(hass, setup_comp_1): assert hass.states.get(heater_switch).state == STATE_ON -async def test_heater_switch(hass, setup_comp_1): +async def test_heater_switch(hass, setup_comp_1, enable_custom_integrations): """Test heater switching test switch.""" platform = getattr(hass.components, "test.switch") platform.init() diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index b409786dc07..14489450610 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -114,7 +114,7 @@ async def test_state_reporting(hass): assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE -async def test_brightness(hass): +async def test_brightness(hass, enable_custom_integrations): """Test brightness reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -183,7 +183,7 @@ async def test_brightness(hass): assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] -async def test_color_hs(hass): +async def test_color_hs(hass, enable_custom_integrations): """Test hs color reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -251,7 +251,7 @@ async def test_color_hs(hass): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_color_rgbw(hass): +async def test_color_rgbw(hass, enable_custom_integrations): """Test rgbw color reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -322,7 +322,7 @@ async def test_color_rgbw(hass): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_color_rgbww(hass): +async def test_color_rgbww(hass, enable_custom_integrations): """Test rgbww color reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -434,7 +434,7 @@ async def test_white_value(hass): assert state.attributes[ATTR_WHITE_VALUE] == 100 -async def test_color_temp(hass): +async def test_color_temp(hass, enable_custom_integrations): """Test color temp reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -501,7 +501,7 @@ async def test_color_temp(hass): assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] -async def test_emulated_color_temp_group(hass): +async def test_emulated_color_temp_group(hass, enable_custom_integrations): """Test emulated color temperature in a group.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -564,7 +564,7 @@ async def test_emulated_color_temp_group(hass): assert state.attributes[ATTR_HS_COLOR] == (27.001, 19.243) -async def test_min_max_mireds(hass): +async def test_min_max_mireds(hass, enable_custom_integrations): """Test min/max mireds reporting. min/max mireds is reported both when light is on and off @@ -739,7 +739,7 @@ async def test_effect(hass): assert state.attributes[ATTR_EFFECT] == "Random" -async def test_supported_color_modes(hass): +async def test_supported_color_modes(hass, enable_custom_integrations): """Test supported_color_modes reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -784,7 +784,7 @@ async def test_supported_color_modes(hass): } -async def test_color_mode(hass): +async def test_color_mode(hass, enable_custom_integrations): """Test color_mode reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 3e6a3cc960f..55c76273ad7 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -6,6 +6,7 @@ import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.loader import DATA_CUSTOM_COMPONENTS from homeassistant.setup import setup_component from tests.common import ( @@ -50,6 +51,7 @@ class TestImageProcessing: def setup_method(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.data.pop(DATA_CUSTOM_COMPONENTS) setup_component( self.hass, diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 440cae8f0fc..5628861b72d 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -256,7 +256,7 @@ async def test_get_action_capabilities_features( assert capabilities == expected -async def test_action(hass, calls): +async def test_action(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index d529c82bfa5..b174a312cd9 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -91,7 +91,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_state(hass, calls): +async def test_if_state(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -165,7 +165,7 @@ async def test_if_state(hass, calls): assert calls[1].data["some"] == "is_off event - test_event2" -async def test_if_fires_on_for_condition(hass, calls): +async def test_if_fires_on_for_condition(hass, calls, enable_custom_integrations): """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index 1c9f6cf1454..3217eb461b0 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -91,7 +91,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_fires_on_state_change(hass, calls): +async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -178,7 +178,9 @@ async def test_if_fires_on_state_change(hass, calls): ) -async def test_if_fires_on_state_change_with_for(hass, calls): +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations +): """Test for triggers firing with delay.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 432959b67e4..71764eec186 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -106,7 +106,7 @@ async def test_methods(hass): assert call.data[light.ATTR_TRANSITION] == "transition_val" -async def test_services(hass, mock_light_profiles): +async def test_services(hass, mock_light_profiles, enable_custom_integrations): """Test the provided services.""" platform = getattr(hass.components, "test.light") @@ -491,7 +491,12 @@ async def test_services(hass, mock_light_profiles): ), ) async def test_light_profiles( - hass, mock_light_profiles, profile_name, expected_data, last_call + hass, + mock_light_profiles, + profile_name, + expected_data, + last_call, + enable_custom_integrations, ): """Test light profiles.""" platform = getattr(hass.components, "test.light") @@ -535,7 +540,9 @@ async def test_light_profiles( assert data == expected_data -async def test_default_profiles_group(hass, mock_light_profiles): +async def test_default_profiles_group( + hass, mock_light_profiles, enable_custom_integrations +): """Test default turn-on light profile for all lights.""" platform = getattr(hass.components, "test.light") platform.init() @@ -635,6 +642,7 @@ async def test_default_profiles_light( hass, mock_light_profiles, extra_call_params, + enable_custom_integrations, expected_params_state_was_off, expected_params_state_was_on, ): @@ -694,7 +702,7 @@ async def test_default_profiles_light( } -async def test_light_context(hass, hass_admin_user): +async def test_light_context(hass, hass_admin_user, enable_custom_integrations): """Test that light context works.""" platform = getattr(hass.components, "test.light") platform.init() @@ -718,7 +726,7 @@ async def test_light_context(hass, hass_admin_user): assert state2.context.user_id == hass_admin_user.id -async def test_light_turn_on_auth(hass, hass_admin_user): +async def test_light_turn_on_auth(hass, hass_admin_user, enable_custom_integrations): """Test that light context works.""" platform = getattr(hass.components, "test.light") platform.init() @@ -740,7 +748,7 @@ async def test_light_turn_on_auth(hass, hass_admin_user): ) -async def test_light_brightness_step(hass): +async def test_light_brightness_step(hass, enable_custom_integrations): """Test that light context works.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -802,7 +810,7 @@ async def test_light_brightness_step(hass): assert entity0.state == "off" # 126 - 126; brightness is 0, light should turn off -async def test_light_brightness_pct_conversion(hass): +async def test_light_brightness_pct_conversion(hass, enable_custom_integrations): """Test that light brightness percent conversion.""" platform = getattr(hass.components, "test.light") platform.init() @@ -967,7 +975,9 @@ invalid_no_brightness_no_color_no_transition,,, @pytest.mark.parametrize("light_state", (STATE_ON, STATE_OFF)) -async def test_light_backwards_compatibility_supported_color_modes(hass, light_state): +async def test_light_backwards_compatibility_supported_color_modes( + hass, light_state, enable_custom_integrations +): """Test supported_color_modes if not implemented by the entity.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1072,7 +1082,9 @@ async def test_light_backwards_compatibility_supported_color_modes(hass, light_s assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN -async def test_light_backwards_compatibility_color_mode(hass): +async def test_light_backwards_compatibility_color_mode( + hass, enable_custom_integrations +): """Test color_mode if not implemented by the entity.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1148,7 +1160,7 @@ async def test_light_backwards_compatibility_color_mode(hass): assert state.attributes["color_mode"] == light.COLOR_MODE_HS -async def test_light_service_call_rgbw(hass): +async def test_light_service_call_rgbw(hass, enable_custom_integrations): """Test backwards compatibility for rgbw functionality in service calls.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1193,7 +1205,7 @@ async def test_light_service_call_rgbw(hass): assert data == {"brightness": 255, "rgbw_color": (10, 20, 30, 40)} -async def test_light_state_rgbw(hass): +async def test_light_state_rgbw(hass, enable_custom_integrations): """Test rgbw color conversion in state updates.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1251,7 +1263,7 @@ async def test_light_state_rgbw(hass): } -async def test_light_state_rgbww(hass): +async def test_light_state_rgbww(hass, enable_custom_integrations): """Test rgbww color conversion in state updates.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1284,7 +1296,7 @@ async def test_light_state_rgbww(hass): } -async def test_light_service_call_color_conversion(hass): +async def test_light_service_call_color_conversion(hass, enable_custom_integrations): """Test color conversion in service calls.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1552,7 +1564,7 @@ async def test_light_service_call_color_conversion(hass): assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)} -async def test_light_state_color_conversion(hass): +async def test_light_state_color_conversion(hass, enable_custom_integrations): """Test color conversion in state updates.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index 7d484ae96aa..a84555bdd42 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -30,7 +30,9 @@ def entity_reg(hass): return mock_registry(hass) -async def test_get_actions_support_open(hass, device_reg, entity_reg): +async def test_get_actions_support_open( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected actions from a lock which supports open.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -74,7 +76,9 @@ async def test_get_actions_support_open(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_actions_not_support_open(hass, device_reg, entity_reg): +async def test_get_actions_not_support_open( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected actions from a lock which doesn't support open.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index 7cd5a632982..1193764da3a 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -70,7 +70,7 @@ async def test_get_actions(hass, device_reg, entity_reg): assert actions == expected_actions -async def test_action(hass, calls): +async def test_action(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index 12cf0e05493..6f3c0e1c0a2 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -91,7 +91,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_state(hass, calls): +async def test_if_state(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -165,7 +165,7 @@ async def test_if_state(hass, calls): assert calls[1].data["some"] == "is_off event - test_event2" -async def test_if_fires_on_for_condition(hass, calls): +async def test_if_fires_on_for_condition(hass, calls, enable_custom_integrations): """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 616c356936c..3afa731cf59 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -91,7 +91,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_fires_on_state_change(hass, calls): +async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -176,7 +176,9 @@ async def test_if_fires_on_state_change(hass, calls): ) -async def test_if_fires_on_state_change_with_for(hass, calls): +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations +): """Test for triggers firing with delay.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index a3d50d0214f..8263b9c3006 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -19,7 +19,7 @@ def entities(hass): yield platform.ENTITIES[0:2] -async def test_config_yaml_alias_anchor(hass, entities): +async def test_config_yaml_alias_anchor(hass, entities, enable_custom_integrations): """Test the usage of YAML aliases and anchors. The following test scene configuration is equivalent to: @@ -64,7 +64,7 @@ async def test_config_yaml_alias_anchor(hass, entities): assert light_2.last_call("turn_on")[1].get("brightness") == 100 -async def test_config_yaml_bool(hass, entities): +async def test_config_yaml_bool(hass, entities, enable_custom_integrations): """Test parsing of booleans in yaml config.""" light_1, light_2 = await setup_lights(hass, entities) @@ -91,7 +91,7 @@ async def test_config_yaml_bool(hass, entities): assert light_2.last_call("turn_on")[1].get("brightness") == 100 -async def test_activate_scene(hass, entities): +async def test_activate_scene(hass, entities, enable_custom_integrations): """Test active scene.""" light_1, light_2 = await setup_lights(hass, entities) diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 2de95d44eb1..6cad21c5bde 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -41,7 +41,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_conditions(hass, device_reg, entity_reg): +async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected conditions from a sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -80,7 +80,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions -async def test_get_condition_capabilities(hass, device_reg, entity_reg): +async def test_get_condition_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -126,7 +128,9 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_get_condition_capabilities_none(hass, device_reg, entity_reg): +async def test_get_condition_capabilities_none( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -162,7 +166,9 @@ async def test_get_condition_capabilities_none(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_state_not_above_below(hass, calls, caplog): +async def test_if_state_not_above_below( + hass, calls, caplog, enable_custom_integrations +): """Test for bad value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -196,7 +202,7 @@ async def test_if_state_not_above_below(hass, calls, caplog): assert "must contain at least one of below, above" in caplog.text -async def test_if_state_above(hass, calls): +async def test_if_state_above(hass, calls, enable_custom_integrations): """Test for value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -254,7 +260,7 @@ async def test_if_state_above(hass, calls): assert calls[0].data["some"] == "event - test_event1" -async def test_if_state_below(hass, calls): +async def test_if_state_below(hass, calls, enable_custom_integrations): """Test for value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -312,7 +318,7 @@ async def test_if_state_below(hass, calls): assert calls[0].data["some"] == "event - test_event1" -async def test_if_state_between(hass, calls): +async def test_if_state_between(hass, calls, enable_custom_integrations): """Test for value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 4c65eff34ab..9da93510523 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -45,7 +45,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg): +async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected triggers from a sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -85,7 +85,9 @@ 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): +async def test_get_trigger_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -132,7 +134,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg): +async def test_get_trigger_capabilities_none( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -168,7 +172,9 @@ async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_fires_not_on_above_below(hass, calls, caplog): +async def test_if_fires_not_on_above_below( + hass, calls, caplog, enable_custom_integrations +): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -198,7 +204,7 @@ async def test_if_fires_not_on_above_below(hass, calls, caplog): assert "must contain at least one of below, above" in caplog.text -async def test_if_fires_on_state_above(hass, calls): +async def test_if_fires_on_state_above(hass, calls, enable_custom_integrations): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -256,7 +262,7 @@ async def test_if_fires_on_state_above(hass, calls): ) -async def test_if_fires_on_state_below(hass, calls): +async def test_if_fires_on_state_below(hass, calls, enable_custom_integrations): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -314,7 +320,7 @@ async def test_if_fires_on_state_below(hass, calls): ) -async def test_if_fires_on_state_between(hass, calls): +async def test_if_fires_on_state_between(hass, calls, enable_custom_integrations): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -384,7 +390,9 @@ async def test_if_fires_on_state_between(hass, calls): ) -async def test_if_fires_on_state_change_with_for(hass, calls): +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations +): """Test for triggers firing with delay.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 2a98cd2fad4..9f8d821e74b 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -70,7 +70,7 @@ async def test_get_actions(hass, device_reg, entity_reg): assert actions == expected_actions -async def test_action(hass, calls): +async def test_action(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index 9273610dee9..e2102976f8d 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -91,7 +91,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_state(hass, calls): +async def test_if_state(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -165,7 +165,7 @@ async def test_if_state(hass, calls): assert calls[1].data["some"] == "is_off event - test_event2" -async def test_if_fires_on_for_condition(hass, calls): +async def test_if_fires_on_for_condition(hass, calls, enable_custom_integrations): """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index d958dd21911..e871bf6f645 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -91,7 +91,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_fires_on_state_change(hass, calls): +async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -176,7 +176,9 @@ async def test_if_fires_on_state_change(hass, calls): ) -async def test_if_fires_on_state_change_with_for(hass, calls): +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations +): """Test for triggers firing with delay.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index cf2933282ea..44302ec311b 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -17,7 +17,7 @@ def entities(hass): yield platform.ENTITIES -async def test_methods(hass, entities): +async def test_methods(hass, entities, enable_custom_integrations): """Test is_on, turn_on, turn_off methods.""" switch_1, switch_2, switch_3 = entities assert await async_setup_component( @@ -49,7 +49,9 @@ async def test_methods(hass, entities): assert switch.is_on(hass, switch_3.entity_id) -async def test_switch_context(hass, entities, hass_admin_user): +async def test_switch_context( + hass, entities, hass_admin_user, enable_custom_integrations +): """Test that switch context works.""" assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}}) diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 4c6ff88fa1b..2660fa86879 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -108,6 +108,7 @@ async def test_get_trace( trigger, context_key, condition_results, + enable_custom_integrations, ): """Test tracing a script or automation.""" id = 1 @@ -1227,7 +1228,9 @@ async def test_script_mode_2(hass, hass_ws_client, script_mode, script_execution assert trace["script_execution"] == "finished" -async def test_trace_blueprint_automation(hass, hass_ws_client): +async def test_trace_blueprint_automation( + hass, hass_ws_client, enable_custom_integrations +): """Test trace of blueprint automation.""" id = 1 diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index 44c45c1e9d8..63d4a6e134d 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -141,7 +141,9 @@ async def test_webhook_head(hass, mock_client): assert hooks[0][2].method == "HEAD" -async def test_listing_webhook(hass, hass_ws_client, hass_access_token): +async def test_listing_webhook( + hass, hass_ws_client, hass_access_token, enable_custom_integrations +): """Test unregistering a webhook.""" assert await async_setup_component(hass, "webhook", {}) client = await hass_ws_client(hass, hass_access_token) diff --git a/tests/hassfest/test_version.py b/tests/hassfest/test_version.py index f99ee911a69..7f12fb83fd7 100644 --- a/tests/hassfest/test_version.py +++ b/tests/hassfest/test_version.py @@ -25,10 +25,9 @@ def integration(): def test_validate_version_no_key(integration: Integration): """Test validate version with no key.""" validate_version(integration) - assert ( - "No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration." - in [x.error for x in integration.errors] - ) + assert "No 'version' key in the manifest file." in [ + x.error for x in integration.errors + ] def test_validate_custom_integration_manifest(integration: Integration): diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 8f555914682..3a08c423d76 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -33,25 +33,18 @@ def test_recursive_flatten(): } -async def test_component_translation_path(hass): +async def test_component_translation_path(hass, enable_custom_integrations): """Test the component translation file function.""" assert await async_setup_component( hass, "switch", {"switch": [{"platform": "test"}, {"platform": "test_embedded"}]}, ) - assert await async_setup_component(hass, "test_standalone", {"test_standalone"}) assert await async_setup_component(hass, "test_package", {"test_package"}) - ( - int_test, - int_test_embedded, - int_test_standalone, - int_test_package, - ) = await asyncio.gather( + (int_test, int_test_embedded, int_test_package,) = await asyncio.gather( async_get_integration(hass, "test"), async_get_integration(hass, "test_embedded"), - async_get_integration(hass, "test_standalone"), async_get_integration(hass, "test_package"), ) @@ -71,13 +64,6 @@ async def test_component_translation_path(hass): ) ) - assert ( - translation.component_translation_path( - "test_standalone", "en", int_test_standalone - ) - is None - ) - assert path.normpath( translation.component_translation_path("test_package", "en", int_test_package) ) == path.normpath( @@ -105,7 +91,7 @@ def test_load_translations_files(hass): } -async def test_get_translations(hass, mock_config_flows): +async def test_get_translations(hass, mock_config_flows, enable_custom_integrations): """Test the get translations helper.""" translations = await translation.async_get_translations(hass, "en", "state") assert translations == {} @@ -376,9 +362,8 @@ async def test_caching(hass): assert len(mock_build.mock_calls) > 1 -async def test_custom_component_translations(hass): +async def test_custom_component_translations(hass, enable_custom_integrations): """Test getting translation from custom components.""" - hass.config.components.add("test_standalone") hass.config.components.add("test_embedded") hass.config.components.add("test_package") assert await translation.async_get_translations(hass, "en", "state") == {} diff --git a/tests/test_loader.py b/tests/test_loader.py index 8acc8a7de4f..c2c176b62ab 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,5 +1,5 @@ """Test to verify that we can load components.""" -from unittest.mock import ANY, patch +from unittest.mock import patch import pytest @@ -97,18 +97,13 @@ async def test_helpers_wrapper(hass): assert result == ["hello"] -async def test_custom_component_name(hass): +async def test_custom_component_name(hass, enable_custom_integrations): """Test the name attribute of custom components.""" - integration = await loader.async_get_integration(hass, "test_standalone") - int_comp = integration.get_component() - assert int_comp.__name__ == "custom_components.test_standalone" - assert int_comp.__package__ == "custom_components" - - comp = hass.components.test_standalone - assert comp.__name__ == "custom_components.test_standalone" - assert comp.__package__ == "custom_components" + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_standalone") integration = await loader.async_get_integration(hass, "test_package") + int_comp = integration.get_component() assert int_comp.__name__ == "custom_components.test_package" assert int_comp.__package__ == "custom_components.test_package" @@ -128,67 +123,39 @@ async def test_custom_component_name(hass): assert TEST == 5 -async def test_log_warning_custom_component(hass, caplog): +async def test_log_warning_custom_component(hass, caplog, enable_custom_integrations): """Test that we log a warning when loading a custom component.""" - await loader.async_get_integration(hass, "test_standalone") - assert "You are using a custom integration test_standalone" in caplog.text + + await loader.async_get_integration(hass, "test_package") + assert "You are using a custom integration test_package" in caplog.text await loader.async_get_integration(hass, "test") assert "You are using a custom integration test " in caplog.text -async def test_custom_integration_missing_version(hass, caplog): - """Test that we log a warning when custom integrations are missing a version.""" - test_integration_1 = loader.Integration( - hass, "custom_components.test1", None, {"domain": "test1"} - ) - test_integration_2 = loader.Integration( - hass, - "custom_components.test2", - None, - loader.manifest_from_legacy_module("test2", "custom_components.test2"), - ) - - with patch("homeassistant.loader.async_get_custom_components") as mock_get: - mock_get.return_value = { - "test1": test_integration_1, - "test2": test_integration_2, - } - - await loader.async_get_integration(hass, "test1") - assert ( - "No 'version' key in the manifest file for custom integration 'test1'." - in caplog.text - ) - - await loader.async_get_integration(hass, "test2") - assert ( - "No 'version' key in the manifest file for custom integration 'test2'." - in caplog.text - ) - - -async def test_no_version_warning_for_none_custom_integrations(hass, caplog): - """Test that we do not log a warning when core integrations are missing a version.""" - await loader.async_get_integration(hass, "hue") - assert ( - "No 'version' key in the manifest file for custom integration 'hue'." - not in caplog.text - ) - - async def test_custom_integration_version_not_valid(hass, caplog): """Test that we log a warning when custom integrations have a invalid version.""" - test_integration = loader.Integration( - hass, "custom_components.test", None, {"domain": "test", "version": "test"} + test_integration1 = loader.Integration( + hass, "custom_components.test", None, {"domain": "test1", "version": "test"} + ) + test_integration2 = loader.Integration( + hass, "custom_components.test", None, {"domain": "test2"} ) with patch("homeassistant.loader.async_get_custom_components") as mock_get: - mock_get.return_value = {"test": test_integration} + mock_get.return_value = {"test1": test_integration1, "test2": test_integration2} - await loader.async_get_integration(hass, "test") + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test1") assert ( - "'test' is not a valid version for custom integration 'test'." + "The custom integration 'test1' does not have a valid version key (test) in the manifest file and was blocked from loading." + in caplog.text + ) + + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test2") + assert ( + "The custom integration 'test2' does not have a valid version key (None) in the manifest file and was blocked from loading." in caplog.text ) @@ -200,7 +167,7 @@ async def test_get_integration(hass): assert hue_light == integration.get_platform("light") -async def test_get_integration_legacy(hass): +async def test_get_integration_legacy(hass, enable_custom_integrations): """Test resolving integration.""" integration = await loader.async_get_integration(hass, "test_embedded") assert integration.get_component().DOMAIN == "test_embedded" @@ -319,13 +286,6 @@ async def test_integrations_only_once(hass): assert await int_1 is await int_2 -async def test_get_custom_components_internal(hass): - """Test that we can a list of custom components.""" - # pylint: disable=protected-access - integrations = await loader._async_get_custom_components(hass) - assert integrations == {"test": ANY, "test_package": ANY} - - def _get_test_integration(hass, name, config_flow): """Return a generated test integration.""" return loader.Integration( diff --git a/tests/testing_config/custom_components/test/manifest.json b/tests/testing_config/custom_components/test/manifest.json index 70882fece05..125136e70b5 100644 --- a/tests/testing_config/custom_components/test/manifest.json +++ b/tests/testing_config/custom_components/test/manifest.json @@ -4,5 +4,6 @@ "documentation": "http://example.com", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "version": "1.2.3" } diff --git a/tests/testing_config/custom_components/test_embedded/manifest.json b/tests/testing_config/custom_components/test_embedded/manifest.json new file mode 100644 index 00000000000..72206594d0c --- /dev/null +++ b/tests/testing_config/custom_components/test_embedded/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "test_embedded", + "name": "Test Embedded", + "documentation": "http://test-package.io", + "requirements": [], + "dependencies": [], + "codeowners": [], + "version": "1.2.3" +} diff --git a/tests/testing_config/custom_components/test_package/manifest.json b/tests/testing_config/custom_components/test_package/manifest.json index 320d2768d27..660d0aef1a5 100644 --- a/tests/testing_config/custom_components/test_package/manifest.json +++ b/tests/testing_config/custom_components/test_package/manifest.json @@ -4,5 +4,6 @@ "documentation": "http://test-package.io", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "version": "1.2.3" } From 848ab5c2bc5741041bbaee3dae074a68916bb45e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 May 2021 16:28:30 +0200 Subject: [PATCH 526/852] Deduplicate code in MQTT basic light pt4: Add set_optimistic helper (#50774) --- .../components/mqtt/light/schema_basic.py | 91 +++++++++---------- 1 file changed, 42 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 372cc76becc..b96a5e4a815 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -8,7 +8,9 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_RGB_COLOR, ATTR_WHITE_VALUE, + ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -167,13 +169,13 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._command_templates = None self._value_templates = None self._optimistic = False - self._optimistic_rgb = False + self._optimistic_rgb_color = False self._optimistic_brightness = False self._optimistic_color_temp = False self._optimistic_effect = False - self._optimistic_hs = False + self._optimistic_hs_color = False self._optimistic_white_value = False - self._optimistic_xy = False + self._optimistic_xy_color = False MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -228,7 +230,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): optimistic = config[CONF_OPTIMISTIC] self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None - self._optimistic_rgb = optimistic or topic[CONF_RGB_STATE_TOPIC] is None + self._optimistic_rgb_color = optimistic or topic[CONF_RGB_STATE_TOPIC] is None self._optimistic_brightness = ( optimistic or ( @@ -244,11 +246,15 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None ) self._optimistic_effect = optimistic or topic[CONF_EFFECT_STATE_TOPIC] is None - self._optimistic_hs = optimistic or topic[CONF_HS_STATE_TOPIC] is None + self._optimistic_hs_color = optimistic or topic[CONF_HS_STATE_TOPIC] is None self._optimistic_white_value = ( optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None ) - self._optimistic_xy = optimistic or topic[CONF_XY_STATE_TOPIC] is None + self._optimistic_xy_color = optimistic or topic[CONF_XY_STATE_TOPIC] is None + + def _is_optimistic(self, attribute): + """Return True if the attribute is optimistically updated.""" + return getattr(self, f"_optimistic_{attribute}") async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" @@ -265,9 +271,12 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): "qos": self._config[CONF_QOS], } - def restore_state(attribute, condition): + def restore_state(attribute, condition_attribute=None): """Restore a state attribute.""" - if condition and last_state and last_state.attributes.get(attribute): + if condition_attribute is None: + condition_attribute = attribute + optimistic = self._is_optimistic(condition_attribute) + if optimistic and last_state and last_state.attributes.get(attribute): setattr(self, f"_{attribute}", last_state.attributes[attribute]) @callback @@ -313,7 +322,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.async_write_ha_state() add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) - restore_state(ATTR_BRIGHTNESS, self._optimistic_brightness) + restore_state(ATTR_BRIGHTNESS) @callback @log_messages(self.hass, self.entity_id) @@ -332,7 +341,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.async_write_ha_state() add_topic(CONF_RGB_STATE_TOPIC, rgb_received) - restore_state(ATTR_HS_COLOR, self._optimistic_rgb) + restore_state(ATTR_HS_COLOR, ATTR_RGB_COLOR) @callback @log_messages(self.hass, self.entity_id) @@ -349,7 +358,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.async_write_ha_state() add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) - restore_state(ATTR_COLOR_TEMP, self._optimistic_color_temp) + restore_state(ATTR_COLOR_TEMP) @callback @log_messages(self.hass, self.entity_id) @@ -366,7 +375,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.async_write_ha_state() add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) - restore_state(ATTR_EFFECT, self._optimistic_effect) + restore_state(ATTR_EFFECT) @callback @log_messages(self.hass, self.entity_id) @@ -385,7 +394,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): _LOGGER.debug("Failed to parse hs state update: '%s'", payload) add_topic(CONF_HS_STATE_TOPIC, hs_received) - restore_state(ATTR_HS_COLOR, self._optimistic_hs) + restore_state(ATTR_HS_COLOR) @callback @log_messages(self.hass, self.entity_id) @@ -404,7 +413,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.async_write_ha_state() add_topic(CONF_WHITE_VALUE_STATE_TOPIC, white_value_received) - restore_state(ATTR_WHITE_VALUE, self._optimistic_white_value) + restore_state(ATTR_WHITE_VALUE) @callback @log_messages(self.hass, self.entity_id) @@ -420,7 +429,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.async_write_ha_state() add_topic(CONF_XY_STATE_TOPIC, xy_received) - restore_state(ATTR_HS_COLOR, self._optimistic_xy) + restore_state(ATTR_HS_COLOR, ATTR_XY_COLOR) self._sub_state = await subscription.async_subscribe_topics( self.hass, self._sub_state, topics @@ -523,7 +532,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return supported_features - async def async_turn_on(self, **kwargs): # noqa: C901 + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -541,6 +550,15 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._config[CONF_RETAIN], ) + def set_optimistic(attribute, value, condition_attribute=None): + """Optimistically update a state attribute.""" + if condition_attribute is None: + condition_attribute = attribute + if not self._is_optimistic(condition_attribute): + return False + setattr(self, f"_{attribute}", value) + return True + if on_command_type == "first": publish(CONF_COMMAND_TOPIC, self._payload["on"]) should_update = True @@ -573,28 +591,18 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): rgb_color_str = f"{rgb[0]},{rgb[1]},{rgb[2]}" publish(CONF_RGB_COMMAND_TOPIC, rgb_color_str) - - if self._optimistic_rgb: - self._hs_color = kwargs[ATTR_HS_COLOR] - should_update = True + should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ATTR_RGB_COLOR) if ATTR_HS_COLOR in kwargs and self._topic[CONF_HS_COMMAND_TOPIC] is not None: - hs_color = kwargs[ATTR_HS_COLOR] publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") - - if self._optimistic_hs: - self._hs_color = kwargs[ATTR_HS_COLOR] - should_update = True + should_update |= set_optimistic(ATTR_HS_COLOR, hs_color) if ATTR_HS_COLOR in kwargs and self._topic[CONF_XY_COMMAND_TOPIC] is not None: xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") - - if self._optimistic_xy: - self._hs_color = kwargs[ATTR_HS_COLOR] - should_update = True + should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ATTR_XY_COLOR) if ( ATTR_BRIGHTNESS in kwargs @@ -608,10 +616,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): # Make sure the brightness is not rounded down to 0 device_brightness = max(device_brightness, 1) publish(CONF_BRIGHTNESS_COMMAND_TOPIC, device_brightness) - - if self._optimistic_brightness: - self._brightness = kwargs[ATTR_BRIGHTNESS] - should_update = True + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR not in kwargs @@ -628,10 +633,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): rgb_color_str = f"{rgb[0]},{rgb[1]},{rgb[2]}" publish(CONF_RGB_COMMAND_TOPIC, rgb_color_str) - - if self._optimistic_brightness: - self._brightness = kwargs[ATTR_BRIGHTNESS] - should_update = True + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) if ( ATTR_COLOR_TEMP in kwargs @@ -644,19 +646,13 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): color_temp = tpl({"value": color_temp}) publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp) - - if self._optimistic_color_temp: - self._color_temp = kwargs[ATTR_COLOR_TEMP] - should_update = True + should_update |= set_optimistic(ATTR_COLOR_TEMP, kwargs[ATTR_COLOR_TEMP]) if ATTR_EFFECT in kwargs and self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: effect = kwargs[ATTR_EFFECT] if effect in self._config.get(CONF_EFFECT_LIST): publish(CONF_EFFECT_COMMAND_TOPIC, effect) - - if self._optimistic_effect: - self._effect = kwargs[ATTR_EFFECT] - should_update = True + should_update |= set_optimistic(ATTR_EFFECT, effect) if ( ATTR_WHITE_VALUE in kwargs @@ -666,10 +662,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): white_scale = self._config[CONF_WHITE_VALUE_SCALE] device_white_value = min(round(percent_white * white_scale), white_scale) publish(CONF_WHITE_VALUE_COMMAND_TOPIC, device_white_value) - - if self._optimistic_white_value: - self._white_value = kwargs[ATTR_WHITE_VALUE] - should_update = True + should_update |= set_optimistic(ATTR_WHITE_VALUE, kwargs[ATTR_WHITE_VALUE]) if on_command_type == "last": publish(CONF_COMMAND_TOPIC, self._payload["on"]) From 8dc8e885c86d96ea0efd38b2cb6112e18aa2bb24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 May 2021 16:43:18 +0200 Subject: [PATCH 527/852] Bump home-assistant/builder from 2021.04.2 to 2021.05.0 (#50754) Bumps [home-assistant/builder](https://github.com/home-assistant/builder) from 2021.04.2 to 2021.05.0. - [Release notes](https://github.com/home-assistant/builder/releases) - [Commits](https://github.com/home-assistant/builder/compare/2021.04.2...2021.05.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 657f8b7f15a..190c449cf3c 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -115,7 +115,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.04.2 + uses: home-assistant/builder@2021.05.0 with: args: | $BUILD_ARGS \ @@ -167,7 +167,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.04.2 + uses: home-assistant/builder@2021.05.0 with: args: | $BUILD_ARGS \ From c8486879ae6cfde7fdd96fa06dd1856d9b959403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 17 May 2021 16:54:14 +0200 Subject: [PATCH 528/852] Update devcontainer to Python 3.9 (#50778) --- Dockerfile.dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index 68188f16f01..6dd789761e6 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8 +FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9 SHELL ["/bin/bash", "-o", "pipefail", "-c"] From 56774a9f63dddc826ed3f6be236f18ac839baa3a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 May 2021 08:07:25 -0700 Subject: [PATCH 529/852] Hue: unique ID for groups + remote events (#50748) --- homeassistant/components/hue/bridge.py | 19 +++++--- homeassistant/components/hue/hue_event.py | 10 ++++- homeassistant/components/hue/light.py | 6 ++- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/conftest.py | 8 ++-- tests/components/hue/test_bridge.py | 7 ++- tests/components/hue/test_light.py | 8 ++++ tests/components/hue/test_sensor_base.py | 51 ++++++++++++++-------- 10 files changed, 76 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index c01dc771f3e..8f027aee033 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -1,4 +1,6 @@ """Code to handle a Hue bridge.""" +from __future__ import annotations + import asyncio from functools import partial import logging @@ -241,7 +243,8 @@ class HueBridge: key = (updated_object.ITEM_TYPE, updated_object.id) if key in self._update_callbacks: - self._update_callbacks[key]() + for callback in self._update_callbacks[key]: + callback() except GeneratorExit: pass @@ -249,18 +252,20 @@ class HueBridge: @core.callback def listen_updates(self, item_type, item_id, update_callback): """Listen to updates.""" - callbacks = self._update_callbacks key = (item_type, item_id) + callbacks: list[core.CALLBACK_TYPE] | None = self._update_callbacks.get(key) - if key in callbacks: - _LOGGER.warning("Overwriting update callback for %s", key) + if callbacks is None: + callbacks = self._update_callbacks[key] = [] - callbacks[key] = update_callback + callbacks.append(update_callback) @core.callback def unsub(): - if callbacks.get(key) == update_callback: - callbacks.pop(key) + try: + callbacks.remove(update_callback) + except ValueError: + pass return unsub diff --git a/homeassistant/components/hue/hue_event.py b/homeassistant/components/hue/hue_event.py index f2edc129f10..7c0163f8a16 100644 --- a/homeassistant/components/hue/hue_event.py +++ b/homeassistant/components/hue/hue_event.py @@ -5,7 +5,7 @@ from aiohue.sensors import TYPE_ZGP_SWITCH, TYPE_ZLL_ROTARY, TYPE_ZLL_SWITCH from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback -from homeassistant.util import slugify +from homeassistant.util import dt as dt_util, slugify from .sensor_device import GenericHueDevice @@ -48,7 +48,13 @@ class HueEvent(GenericHueDevice): @callback def async_update_callback(self): """Fire the event if reason is that state is updated.""" - if self.sensor.state == self._last_state: + if ( + self.sensor.state == self._last_state + or + # Filter out old states. Can happen when events fire while refreshing + dt_util.parse_datetime(self.sensor.state["lastupdated"]) + <= dt_util.parse_datetime(self._last_state["lastupdated"]) + ): return # Extract the press code as state diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 18c8444ce65..345156de7d7 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -306,7 +306,11 @@ class HueLight(CoordinatorEntity, LightEntity): @property def unique_id(self): """Return the unique ID of this Hue light.""" - return self.light.uniqueid + unique_id = self.light.uniqueid + if not unique_id and self.is_group and self.light.room: + unique_id = self.light.room["id"] + + return unique_id @property def device_id(self): diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 2a46da9c52b..b61635cb408 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.4.2"], + "requirements": ["aiohue==2.5.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 3f7529169df..5b84ddc46df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.4.2 +aiohue==2.5.0 # homeassistant.components.imap aioimaplib==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6244443a9a..15a8c0af298 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.4.2 +aiohue==2.5.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index ed31f00d9cc..3aecacac58d 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -81,10 +81,10 @@ def create_mock_api(hass): logger = logging.getLogger(__name__) api.config.apiversion = "9.9.9" - api.lights = Lights(logger, {}, mock_request) - api.groups = Groups(logger, {}, mock_request) - api.sensors = Sensors(logger, {}, mock_request) - api.scenes = Scenes(logger, {}, mock_request) + api.lights = Lights(logger, {}, [], mock_request) + api.groups = Groups(logger, {}, [], mock_request) + api.sensors = Sensors(logger, {}, [], mock_request) + api.scenes = Scenes(logger, {}, [], mock_request) return api diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index ee980c6bffe..eb5c93862fe 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -351,12 +351,11 @@ async def test_event_updates(hass, caplog): unsub = hue_bridge.listen_updates("lights", "2", obj_updated) unsub_false = hue_bridge.listen_updates("lights", "2", obj_updated_false) - assert "Overwriting update callback" in caplog.text - events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) await wait_empty_queue() - assert len(calls) == 2 + assert len(calls) == 3 + assert calls[-2] is True assert calls[-1] is False # Also call multiple times to make sure that works. @@ -368,7 +367,7 @@ async def test_event_updates(hass, caplog): events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) await wait_empty_queue() - assert len(calls) == 2 + assert len(calls) == 3 events.put_nowait(None) await subscription_task diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 105fd2a7271..5efb74d015f 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -269,6 +269,10 @@ async def test_groups(hass, mock_bridge): mock_bridge.allow_groups = True mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge.api.groups._v2_resources = [ + {"id_v1": "/groups/1", "id": "group-1-mock-id", "type": "room"}, + {"id_v1": "/groups/2", "id": "group-2-mock-id", "type": "room"}, + ] await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 2 @@ -285,6 +289,10 @@ async def test_groups(hass, mock_bridge): assert lamp_2 is not None assert lamp_2.state == "on" + ent_reg = er.async_get(hass) + assert ent_reg.async_get("light.group_1").unique_id == "group-1-mock-id" + assert ent_reg.async_get("light.group_2").unique_id == "group-2-mock-id" + async def test_new_group_discovered(hass, mock_bridge): """Test if 2nd update has a new group.""" diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index eb7ece241c3..46237c510f7 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -8,6 +8,8 @@ from homeassistant.components.hue.hue_event import CONF_HUE_EVENT from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge +from tests.common import async_capture_events + PRESENCE_SENSOR_1_PRESENT = { "state": {"presence": True, "lastupdated": "2019-01-01T01:00:00"}, "swupdate": {"state": "noupdates", "lastinstall": "2019-01-01T00:00:00"}, @@ -435,18 +437,17 @@ async def test_hue_events(hass, mock_bridge): """Test that hue remotes fire events when pressed.""" mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) - mock_listener = Mock() - unsub = hass.bus.async_listen(CONF_HUE_EVENT, mock_listener) + events = async_capture_events(hass, CONF_HUE_EVENT) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 assert len(hass.states.async_all()) == 7 - assert len(mock_listener.mock_calls) == 0 + assert len(events) == 0 new_sensor_response = dict(SENSOR_RESPONSE) new_sensor_response["7"]["state"] = { "buttonevent": 18, - "lastupdated": "2019-12-28T22:58:02", + "lastupdated": "2019-12-28T22:58:03", } mock_bridge.mock_sensor_responses.append(new_sensor_response) @@ -456,18 +457,18 @@ async def test_hue_events(hass, mock_bridge): assert len(mock_bridge.mock_requests) == 2 assert len(hass.states.async_all()) == 7 - assert len(mock_listener.mock_calls) == 1 - assert mock_listener.mock_calls[0][1][0].data == { + assert len(events) == 1 + assert events[-1].data == { "id": "hue_tap", "unique_id": "00:00:00:00:00:44:23:08-f2", "event": 18, - "last_updated": "2019-12-28T22:58:02", + "last_updated": "2019-12-28T22:58:03", } new_sensor_response = dict(new_sensor_response) new_sensor_response["8"]["state"] = { "buttonevent": 3002, - "lastupdated": "2019-12-28T22:58:01", + "lastupdated": "2019-12-28T22:58:03", } mock_bridge.mock_sensor_responses.append(new_sensor_response) @@ -477,14 +478,30 @@ async def test_hue_events(hass, mock_bridge): assert len(mock_bridge.mock_requests) == 3 assert len(hass.states.async_all()) == 7 - assert len(mock_listener.mock_calls) == 2 - assert mock_listener.mock_calls[1][1][0].data == { + assert len(events) == 2 + assert events[-1].data == { "id": "hue_dimmer_switch_1", "unique_id": "00:17:88:01:10:3e:3a:dc-02-fc00", "event": 3002, - "last_updated": "2019-12-28T22:58:01", + "last_updated": "2019-12-28T22:58:03", } + # Fire old event, it should be ignored + new_sensor_response = dict(new_sensor_response) + new_sensor_response["8"]["state"] = { + "buttonevent": 18, + "lastupdated": "2019-12-28T22:58:02", + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + await mock_bridge.sensor_manager.coordinator.async_refresh() + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 4 + assert len(hass.states.async_all()) == 7 + assert len(events) == 2 + # Add a new remote. In discovery the new event is registered **but not fired** new_sensor_response = dict(new_sensor_response) new_sensor_response["21"] = { @@ -524,9 +541,9 @@ async def test_hue_events(hass, mock_bridge): await mock_bridge.sensor_manager.coordinator.async_refresh() await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge.mock_requests) == 5 assert len(hass.states.async_all()) == 8 - assert len(mock_listener.mock_calls) == 2 + assert len(events) == 2 # A new press fires the event new_sensor_response["21"]["state"]["lastupdated"] = "2020-01-31T15:57:19" @@ -536,14 +553,12 @@ async def test_hue_events(hass, mock_bridge): await mock_bridge.sensor_manager.coordinator.async_refresh() await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 5 + assert len(mock_bridge.mock_requests) == 6 assert len(hass.states.async_all()) == 8 - assert len(mock_listener.mock_calls) == 3 - assert mock_listener.mock_calls[2][1][0].data == { + assert len(events) == 3 + assert events[-1].data == { "id": "lutron_aurora_1", "unique_id": "ff:ff:00:0f:e7:fd:bc:b7-01-fc00-0014", "event": 2, "last_updated": "2020-01-31T15:57:19", } - - unsub() From 72dfa8606e3cee921fa5888c33b010b50309a06a Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Mon, 17 May 2021 16:20:05 +0100 Subject: [PATCH 530/852] Enable strict typing for air_quality component (#50722) --- .strict-typing | 1 + .../components/air_quality/__init__.py | 83 ++++++++++--------- mypy.ini | 11 +++ 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/.strict-typing b/.strict-typing index 8445ff511f5..6d8b22493b6 100644 --- a/.strict-typing +++ b/.strict-typing @@ -6,6 +6,7 @@ homeassistant.components homeassistant.components.acer_projector.* homeassistant.components.actiontec.* homeassistant.components.aftership.* +homeassistant.components.air_quality.* homeassistant.components.airly.* homeassistant.components.aladdin_connect.* homeassistant.components.amazon_polly.* diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index d69a02f83bd..1b8ab5f9c30 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -1,40 +1,45 @@ """Component for handling Air Quality data for your location.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import final +from typing import Final, final +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType, StateType -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -ATTR_AQI = "air_quality_index" -ATTR_CO2 = "carbon_dioxide" -ATTR_CO = "carbon_monoxide" -ATTR_N2O = "nitrogen_oxide" -ATTR_NO = "nitrogen_monoxide" -ATTR_NO2 = "nitrogen_dioxide" -ATTR_OZONE = "ozone" -ATTR_PM_0_1 = "particulate_matter_0_1" -ATTR_PM_10 = "particulate_matter_10" -ATTR_PM_2_5 = "particulate_matter_2_5" -ATTR_SO2 = "sulphur_dioxide" +ATTR_AQI: Final = "air_quality_index" +ATTR_CO2: Final = "carbon_dioxide" +ATTR_CO: Final = "carbon_monoxide" +ATTR_N2O: Final = "nitrogen_oxide" +ATTR_NO: Final = "nitrogen_monoxide" +ATTR_NO2: Final = "nitrogen_dioxide" +ATTR_OZONE: Final = "ozone" +ATTR_PM_0_1: Final = "particulate_matter_0_1" +ATTR_PM_10: Final = "particulate_matter_10" +ATTR_PM_2_5: Final = "particulate_matter_2_5" +ATTR_SO2: Final = "sulphur_dioxide" -DOMAIN = "air_quality" +DOMAIN: Final = "air_quality" -ENTITY_ID_FORMAT = DOMAIN + ".{}" +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL: Final = timedelta(seconds=30) -PROP_TO_ATTR = { +PROP_TO_ATTR: Final[dict[str, str]] = { "air_quality_index": ATTR_AQI, "attribution": ATTR_ATTRIBUTION, "carbon_dioxide": ATTR_CO2, @@ -50,7 +55,7 @@ PROP_TO_ATTR = { } -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the air quality component.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -59,84 +64,86 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class AirQualityEntity(Entity): """ABC for air quality data.""" @property - def particulate_matter_2_5(self): + def particulate_matter_2_5(self) -> StateType: """Return the particulate matter 2.5 level.""" raise NotImplementedError() @property - def particulate_matter_10(self): + def particulate_matter_10(self) -> StateType: """Return the particulate matter 10 level.""" return None @property - def particulate_matter_0_1(self): + def particulate_matter_0_1(self) -> StateType: """Return the particulate matter 0.1 level.""" return None @property - def air_quality_index(self): + def air_quality_index(self) -> StateType: """Return the Air Quality Index (AQI).""" return None @property - def ozone(self): + def ozone(self) -> StateType: """Return the O3 (ozone) level.""" return None @property - def carbon_monoxide(self): + def carbon_monoxide(self) -> StateType: """Return the CO (carbon monoxide) level.""" return None @property - def carbon_dioxide(self): + def carbon_dioxide(self) -> StateType: """Return the CO2 (carbon dioxide) level.""" return None @property - def attribution(self): + def attribution(self) -> StateType: """Return the attribution.""" return None @property - def sulphur_dioxide(self): + def sulphur_dioxide(self) -> StateType: """Return the SO2 (sulphur dioxide) level.""" return None @property - def nitrogen_oxide(self): + def nitrogen_oxide(self) -> StateType: """Return the N2O (nitrogen oxide) level.""" return None @property - def nitrogen_monoxide(self): + def nitrogen_monoxide(self) -> StateType: """Return the NO (nitrogen monoxide) level.""" return None @property - def nitrogen_dioxide(self): + def nitrogen_dioxide(self) -> StateType: """Return the NO2 (nitrogen dioxide) level.""" return None @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, str | int | float]: """Return the state attributes.""" - data = {} + data: dict[str, str | int | float] = {} for prop, attr in PROP_TO_ATTR.items(): value = getattr(self, prop) @@ -146,11 +153,11 @@ class AirQualityEntity(Entity): return data @property - def state(self): + def state(self) -> StateType: """Return the current state.""" return self.particulate_matter_2_5 @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity.""" return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER diff --git a/mypy.ini b/mypy.ini index 009112c1e0a..b2590348431 100644 --- a/mypy.ini +++ b/mypy.ini @@ -77,6 +77,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.air_quality.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.airly.*] check_untyped_defs = true disallow_incomplete_defs = true From 3554316f3f8120293462c52fb86cf6ce366e5b5b Mon Sep 17 00:00:00 2001 From: Michael Klamminger <6277211+m1ch@users.noreply.github.com> Date: Mon, 17 May 2021 19:31:11 +0200 Subject: [PATCH 531/852] Update MQTT cover template handling (#50236) * flake 8 * Implement feedback from PR * update warning message * added and updated tests * remove _has_tilt_topic variable * flake 8 * Implement feedback from PR * update warning message * added and updated tests * remove _has_tilt_topic variable * renamed _tilt_message_received to _tilt_payload_received * merged with latesed upstream/dev * converted if to try except for type check * Implemented the suggestions of @emontnemery * Tweak tests * logger info to debug Co-authored-by: Shay Levy * cast tilt payload as int; combine exceptions to one line * Add test for JSONDecodeError * Update homeassistant/components/mqtt/cover.py Co-authored-by: Erik Montnemery Co-authored-by: Shay Levy --- homeassistant/components/mqtt/cover.py | 162 +++++++--- tests/components/mqtt/test_cover.py | 420 ++++++++++++++++++++++++- 2 files changed, 532 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 3bcc7d4f2a9..eb7c5a79da9 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -1,5 +1,6 @@ """Support for MQTT cover devices.""" import functools +from json import JSONDecodeError, loads as json_loads import logging import voluptuous as vol @@ -252,7 +253,7 @@ class MqttCover(MqttEntity, CoverEntity): if tilt_status_template is not None: tilt_status_template.hass = self.hass - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -261,45 +262,36 @@ class MqttCover(MqttEntity, CoverEntity): def tilt_message_received(msg): """Handle tilt updates.""" payload = msg.payload - tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) - if tilt_status_template is not None: - payload = tilt_status_template.async_render_with_possible_json_value( - payload + template = self._config.get(CONF_TILT_STATUS_TEMPLATE) + if template is not None: + variables = { + "entity_id": self.entity_id, + "position_open": self._config[CONF_POSITION_OPEN], + "position_closed": self._config[CONF_POSITION_CLOSED], + "tilt_min": self._config[CONF_TILT_MIN], + "tilt_max": self._config[CONF_TILT_MAX], + } + payload = template.async_render_with_possible_json_value( + payload, variables=variables ) if not payload: _LOGGER.debug("Ignoring empty tilt message from '%s'", msg.topic) return - if not payload.isnumeric(): - _LOGGER.warning("Payload '%s' is not numeric", payload) - elif ( - self._config[CONF_TILT_MIN] - <= int(payload) - <= self._config[CONF_TILT_MAX] - or self._config[CONF_TILT_MAX] - <= int(payload) - <= self._config[CONF_TILT_MIN] - ): - level = self.find_percentage_in_range(float(payload)) - self._tilt_value = level - self.async_write_ha_state() - else: - _LOGGER.warning( - "Payload '%s' is out of range, must be between '%s' and '%s' inclusive", - payload, - self._config[CONF_TILT_MIN], - self._config[CONF_TILT_MAX], - ) + self.tilt_payload_received(payload) @callback @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - payload = value_template.async_render_with_possible_json_value(payload) + template = self._config.get(CONF_VALUE_TEMPLATE) + if template is not None: + variables = {"entity_id": self.entity_id} + payload = template.async_render_with_possible_json_value( + payload, variables=variables + ) if not payload: _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) @@ -347,26 +339,57 @@ class MqttCover(MqttEntity, CoverEntity): template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: - payload = template.async_render_with_possible_json_value(payload) + variables = { + "entity_id": self.entity_id, + "position_open": self._config[CONF_POSITION_OPEN], + "position_closed": self._config[CONF_POSITION_CLOSED], + "tilt_min": self._config[CONF_TILT_MIN], + "tilt_max": self._config[CONF_TILT_MAX], + } + payload = template.async_render_with_possible_json_value( + payload, variables=variables + ) - if not payload: - _LOGGER.debug("Ignoring empty position message from '%s'", msg.topic) - return + if not payload: + _LOGGER.debug( + "Ignoring empty position message from '%s'", msg.topic + ) + return - if payload.isnumeric(): + try: + payload = json_loads(payload) + except JSONDecodeError: + pass + + if isinstance(payload, dict): + if "position" not in payload: + _LOGGER.warning( + "Template (position_template) returned JSON without position attribute" + ) + return + if "tilt_position" in payload: + if not self._config.get(CONF_TILT_STATE_OPTIMISTIC): + # reset forced set tilt optimistic + self._tilt_optimistic = False + self.tilt_payload_received(payload["tilt_position"]) + payload = payload["position"] + + try: percentage_payload = self.find_percentage_in_range( float(payload), COVER_PAYLOAD ) - self._position = percentage_payload - if self._config.get(CONF_STATE_TOPIC) is None: - self._state = ( - STATE_CLOSED - if percentage_payload == DEFAULT_POSITION_CLOSED - else STATE_OPEN - ) - else: + except ValueError: _LOGGER.warning("Payload '%s' is not numeric", payload) return + + self._position = percentage_payload + if self._config.get(CONF_STATE_TOPIC) is None: + self._state = ( + STATE_CLOSED + if percentage_payload == DEFAULT_POSITION_CLOSED + else STATE_OPEN + ) + self.async_write_ha_state() if self._config.get(CONF_GET_POSITION_TOPIC): @@ -391,6 +414,7 @@ class MqttCover(MqttEntity, CoverEntity): self._optimistic = True if self._config.get(CONF_TILT_STATUS_TOPIC) is None: + # Force into optimistic tilt mode. self._tilt_optimistic = True else: self._tilt_value = STATE_UNKNOWN @@ -550,12 +574,21 @@ class MqttCover(MqttEntity, CoverEntity): async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE) + template = self._config.get(CONF_TILT_COMMAND_TEMPLATE) tilt = kwargs[ATTR_TILT_POSITION] percentage_tilt = tilt tilt = self.find_in_range_from_percent(tilt) - if set_tilt_template is not None: - tilt = set_tilt_template.async_render(parse_result=False, **kwargs) + # Handover the tilt after calculated from percent would make it more consistent with receiving templates + if template is not None: + variables = { + "tilt_position": percentage_tilt, + "entity_id": self.entity_id, + "position_open": self._config[CONF_POSITION_OPEN], + "position_closed": self._config[CONF_POSITION_CLOSED], + "tilt_min": self._config[CONF_TILT_MIN], + "tilt_max": self._config[CONF_TILT_MAX], + } + tilt = template.async_render(parse_result=False, variables=variables) mqtt.async_publish( self.hass, @@ -565,17 +598,26 @@ class MqttCover(MqttEntity, CoverEntity): self._config[CONF_RETAIN], ) if self._tilt_optimistic: + _LOGGER.debug("Set tilt value optimistic") self._tilt_value = percentage_tilt self.async_write_ha_state() async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE) + template = self._config.get(CONF_SET_POSITION_TEMPLATE) position = kwargs[ATTR_POSITION] percentage_position = position position = self.find_in_range_from_percent(position, COVER_PAYLOAD) - if set_position_template is not None: - position = set_position_template.async_render(parse_result=False, **kwargs) + if template is not None: + variables = { + "position": percentage_position, + "entity_id": self.entity_id, + "position_open": self._config[CONF_POSITION_OPEN], + "position_closed": self._config[CONF_POSITION_CLOSED], + "tilt_min": self._config[CONF_TILT_MIN], + "tilt_max": self._config[CONF_TILT_MAX], + } + position = template.async_render(parse_result=False, variables=variables) mqtt.async_publish( self.hass, @@ -650,3 +692,29 @@ class MqttCover(MqttEntity, CoverEntity): if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE): position = max_range - position + offset return position + + def tilt_payload_received(self, _payload): + """Set the tilt value.""" + + try: + payload = int(round(float(_payload))) + except ValueError: + _LOGGER.warning("Payload '%s' is not numeric", _payload) + return + + if ( + self._config[CONF_TILT_MIN] <= int(payload) <= self._config[CONF_TILT_MAX] + or self._config[CONF_TILT_MAX] + <= int(payload) + <= self._config[CONF_TILT_MIN] + ): + level = self.find_percentage_in_range(payload) + self._tilt_value = level + self.async_write_ha_state() + else: + _LOGGER.warning( + "Payload '%s' is out of range, must be between '%s' and '%s' inclusive", + payload, + self._config[CONF_TILT_MIN], + self._config[CONF_TILT_MAX], + ) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 8d729ca9dde..d0665fba318 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -260,6 +260,45 @@ async def test_state_via_template(hass, mqtt_mock): assert state.state == STATE_CLOSED +async def test_state_via_template_and_entity_id(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "value_template": '\ + {% if value == "open" or value == "closed" %}\ + {{ value }}\ + {% else %}\ + {{ states(entity_id) }}\ + {% endif %}', + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "state-topic", "open") + async_fire_mqtt_message(hass, "state-topic", "invalid") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "state-topic", "closed") + async_fire_mqtt_message(hass, "state-topic", "invalid") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): """Test the controlling state via topic with JSON value.""" assert await async_setup_component( @@ -336,6 +375,47 @@ async def test_position_via_template(hass, mqtt_mock): assert state.state == STATE_CLOSED +async def test_position_via_template_and_entity_id(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "qos": 0, + "position_template": '\ + {% if state_attr(entity_id, "current_position") == None %}\ + {{ value }}\ + {% else %}\ + {{ state_attr(entity_id, "current_position") + value | int }}\ + {% endif %}', + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "get-position-topic", "10") + + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position == 10 + + async_fire_mqtt_message(hass, "get-position-topic", "10") + + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position == 20 + + async def test_optimistic_state_change(hass, mqtt_mock): """Test changing state optimistically.""" assert await async_setup_component( @@ -712,7 +792,13 @@ async def test_position_update(hass, mqtt_mock): assert current_cover_position == 22 -async def test_set_position_templated(hass, mqtt_mock): +@pytest.mark.parametrize( + "pos_template,pos_call,pos_message", + [("{{position-1}}", 43, "42"), ("{{100-62}}", 100, "38")], +) +async def test_set_position_templated( + hass, mqtt_mock, pos_template, pos_call, pos_message +): """Test setting cover position via template.""" assert await async_setup_component( hass, @@ -726,7 +812,51 @@ async def test_set_position_templated(hass, mqtt_mock): "position_open": 100, "position_closed": 0, "set_position_topic": "set-position-topic", - "set_position_template": "{{100-62}}", + "set_position_template": pos_template, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_POSITION: pos_call}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "set-position-topic", pos_message, 0, False + ) + + +async def test_set_position_templated_and_attributes(hass, mqtt_mock): + """Test setting cover position via template and using entities attributes.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": '\ + {% if position > 99 %}\ + {% if state_attr(entity_id, "current_position") == None %}\ + {{ 5 }}\ + {% else %}\ + {{ 23 }}\ + {% endif %}\ + {% else %}\ + {{ 42 }}\ + {% endif %}', "payload_open": "OPEN", "payload_close": "CLOSE", "payload_stop": "STOP", @@ -742,8 +872,85 @@ async def test_set_position_templated(hass, mqtt_mock): blocking=True, ) + mqtt_mock.async_publish.assert_called_once_with("set-position-topic", "5", 0, False) + + +async def test_set_tilt_templated(hass, mqtt_mock): + """Test setting cover tilt position via template.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": "{{position-1}}", + "tilt_command_template": "{{tilt_position+1}}", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_TILT_POSITION: 41}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with( - "set-position-topic", "38", 0, False + "tilt-command-topic", "42", 0, False + ) + + +async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): + """Test setting cover tilt position via template and using entities attributes.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": "{{position-1}}", + "tilt_command_template": '\ + {% if state_attr(entity_id, "friendly_name") != "test" %}\ + {{ 5 }}\ + {% else %}\ + {{ 23 }}\ + {% endif %}', + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_TILT_POSITION: 99}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "tilt-command-topic", "23", 0, False ) @@ -2508,6 +2715,187 @@ async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, ) in caplog.text +async def test_position_template_with_entity_id(hass, mqtt_mock): + """Test position by updating status via position template.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '\ + {% if state_attr(entity_id, "current_position") != None %}\ + {{ value | int + state_attr(entity_id, "current_position") }} \ + {% else %} \ + {{ value }} \ + {% endif %}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "10") + + current_cover_position_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position_position == 10 + + async_fire_mqtt_message(hass, "get-position-topic", "10") + + current_cover_position_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position_position == 20 + + +async def test_position_via_position_topic_template_return_json(hass, mqtt_mock): + """Test position by updating status via position template and returning json.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"position" : value} | tojson }}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "55") + + current_cover_position_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position_position == 55 + + +async def test_position_via_position_topic_template_return_json_warning( + hass, caplog, mqtt_mock +): + """Test position by updating status via position template returning json without position attribute.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"pos" : value} | tojson }}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "55") + + assert ( + "Template (position_template) returned JSON without position attribute" + in caplog.text + ) + + +async def test_position_and_tilt_via_position_topic_template_return_json( + hass, mqtt_mock +): + """Test position and tilt by updating the position via position template.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '\ + {{ {"position" : value, "tilt_position" : (value | int / 2)| int } | tojson }}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "0") + + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + current_tilt_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_TILT_POSITION + ] + assert current_cover_position == 0 and current_tilt_position == 0 + + async_fire_mqtt_message(hass, "get-position-topic", "99") + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + current_tilt_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_TILT_POSITION + ] + assert current_cover_position == 99 and current_tilt_position == 49 + + +async def test_position_via_position_topic_template_all_variables(hass, mqtt_mock): + """Test position by updating status via position template.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 99, + "position_closed": 1, + "tilt_min": 11, + "tilt_max": 22, + "position_template": "\ + {% if value | int < tilt_max %}\ + {{ tilt_min }}\ + {% endif %}\ + {% if value | int > position_closed %}\ + {{ position_open }}\ + {% endif %}", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "0") + + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position == 10 + + async_fire_mqtt_message(hass, "get-position-topic", "55") + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position == 100 + + async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): """Test the controlling state via stopped state when no position topic.""" assert await async_setup_component( @@ -2555,3 +2943,29 @@ async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): state = hass.states.get("cover.test") assert state.state == STATE_CLOSED + + +async def test_position_via_position_topic_template_return_invalid_json( + hass, caplog, mqtt_mock +): + """Test position by updating status via position template and returning invalid json.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"position" : invalid_json} }}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "55") + + assert ("Payload '{'position': Undefined}' is not numeric") in caplog.text From 72288710cad1814a5e27d2700605b9ab927e2755 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 May 2021 13:42:12 -0400 Subject: [PATCH 532/852] Increase the sqlite cache size from ~2MiB to 8MiB (#50747) --- homeassistant/components/recorder/__init__.py | 13 ++++++----- homeassistant/components/recorder/util.py | 23 ++++++++++--------- tests/components/recorder/test_util.py | 14 ++++++++--- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index d7358e96100..091bff8445f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -331,7 +331,7 @@ class Recorder(threading.Thread): self._pending_expunge = [] self.event_session = None self.get_session = None - self._completed_database_setup = None + self._completed_first_database_setup = None self._event_listener = None self.async_migration_event = asyncio.Event() self.migration_in_progress = False @@ -837,15 +837,16 @@ class Recorder(threading.Thread): def _setup_connection(self): """Ensure database is ready to fly.""" kwargs = {} - self._completed_database_setup = False + self._completed_first_database_setup = False def setup_recorder_connection(dbapi_connection, connection_record): """Dbapi specific connection settings.""" - if self._completed_database_setup: - return - self._completed_database_setup = setup_connection_for_dialect( - self.engine.dialect.name, dbapi_connection + setup_connection_for_dialect( + self.engine.dialect.name, + dbapi_connection, + not self._completed_first_database_setup, ) + self._completed_first_database_setup = True if self.db_url == SQLITE_URL_PREFIX or ":memory:" in self.db_url: kwargs["connect_args"] = {"check_same_thread": False} diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 6231f493cc2..186aad4fe9e 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -254,26 +254,27 @@ def execute_on_connection(dbapi_connection, statement): cursor.close() -def setup_connection_for_dialect(dialect_name, dbapi_connection): +def setup_connection_for_dialect(dialect_name, dbapi_connection, first_connection): """Execute statements needed for dialect connection.""" # Returns False if the the connection needs to be setup # on the next connection, returns True if the connection # never needs to be setup again. if dialect_name == "sqlite": - old_isolation = dbapi_connection.isolation_level - dbapi_connection.isolation_level = None - execute_on_connection(dbapi_connection, "PRAGMA journal_mode=WAL") - dbapi_connection.isolation_level = old_isolation - # WAL mode only needs to be setup once - # instead of every time we open the sqlite connection - # as its persistent and isn't free to call every time. - return True + if first_connection: + old_isolation = dbapi_connection.isolation_level + dbapi_connection.isolation_level = None + execute_on_connection(dbapi_connection, "PRAGMA journal_mode=WAL") + dbapi_connection.isolation_level = old_isolation + # WAL mode only needs to be setup once + # instead of every time we open the sqlite connection + # as its persistent and isn't free to call every time. + + # approximately 8MiB of memory + execute_on_connection(dbapi_connection, "PRAGMA cache_size = -8192") if dialect_name == "mysql": execute_on_connection(dbapi_connection, "SET session wait_timeout=28800") - return False - def end_incomplete_runs(session, start_time): """End any incomplete recorder runs.""" diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index b5c5b68fe3f..0a9f90be83e 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -152,7 +152,7 @@ def test_setup_connection_for_dialect_mysql(): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - assert util.setup_connection_for_dialect("mysql", dbapi_connection) is False + util.setup_connection_for_dialect("mysql", dbapi_connection, True) assert execute_mock.call_args[0][0] == "SET session wait_timeout=28800" @@ -167,9 +167,17 @@ def test_setup_connection_for_dialect_sqlite(): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - assert util.setup_connection_for_dialect("sqlite", dbapi_connection) is True + util.setup_connection_for_dialect("sqlite", dbapi_connection, True) - assert execute_mock.call_args[0][0] == "PRAGMA journal_mode=WAL" + assert len(execute_mock.call_args_list) == 2 + assert execute_mock.call_args_list[0][0][0] == "PRAGMA journal_mode=WAL" + assert execute_mock.call_args_list[1][0][0] == "PRAGMA cache_size = -8192" + + execute_mock.reset_mock() + util.setup_connection_for_dialect("sqlite", dbapi_connection, False) + + assert len(execute_mock.call_args_list) == 1 + assert execute_mock.call_args_list[0][0][0] == "PRAGMA cache_size = -8192" def test_basic_sanity_check(hass_recorder): From 1e107724976f34884359e1bf1ff1a3190176c797 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 May 2021 11:06:42 -0700 Subject: [PATCH 533/852] Add support for local push channels to mobile_app (#50750) --- .../components/mobile_app/__init__.py | 60 ++++++++++++- homeassistant/components/mobile_app/const.py | 1 + .../components/mobile_app/manifest.json | 2 +- homeassistant/components/mobile_app/notify.py | 14 ++- .../components/websocket_api/commands.py | 1 - tests/components/mobile_app/test_notify.py | 85 +++++++++++++++++-- 6 files changed, 150 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 0fe1386d7ce..951c6f3beaf 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,13 +1,15 @@ """Integrates Native Apps to Home Assistant.""" from contextlib import suppress -from homeassistant.components import cloud, notify as hass_notify +import voluptuous as vol + +from homeassistant.components import cloud, notify as hass_notify, websocket_api from homeassistant.components.webhook import ( async_register as webhook_register, async_unregister as webhook_unregister, ) from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, discovery from homeassistant.helpers.typing import ConfigType @@ -17,9 +19,11 @@ from .const import ( ATTR_MODEL, ATTR_OS_VERSION, CONF_CLOUDHOOK_URL, + CONF_USER_ID, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_DEVICES, + DATA_PUSH_CHANNEL, DATA_STORE, DOMAIN, STORAGE_KEY, @@ -46,6 +50,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: app_config.get(DATA_DELETED_IDS, []), DATA_DEVICES: {}, + DATA_PUSH_CHANNEL: {}, DATA_STORE: store, } @@ -61,6 +66,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): discovery.async_load_platform(hass, "notify", DOMAIN, {}, config) ) + websocket_api.async_register_command(hass, handle_push_notification_channel) + return True @@ -120,3 +127,52 @@ async def async_remove_entry(hass, entry): if CONF_CLOUDHOOK_URL in entry.data: with suppress(cloud.CloudNotAvailable): await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID]) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "mobile_app/push_notification_channel", + vol.Required("webhook_id"): str, + } +) +def handle_push_notification_channel(hass, connection, msg): + """Set up a direct push notification channel.""" + webhook_id = msg["webhook_id"] + + # Validate that the webhook ID is registered to the user of the websocket connection + config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES].get(webhook_id) + + if config_entry is None: + connection.send_error( + msg["id"], websocket_api.ERR_NOT_FOUND, "Webhook ID not found" + ) + return + + if config_entry.data[CONF_USER_ID] != connection.user.id: + connection.send_error( + msg["id"], + websocket_api.ERR_UNAUTHORIZED, + "User not linked to this webhook ID", + ) + return + + registered_channels = hass.data[DOMAIN][DATA_PUSH_CHANNEL] + + if webhook_id in registered_channels: + registered_channels.pop(webhook_id)() + + @callback + def forward_push_notification(data): + """Forward events to websocket.""" + connection.send_message(websocket_api.messages.event_message(msg["id"], data)) + + @callback + def unsub(): + # pylint: disable=comparison-with-callable + if registered_channels.get(webhook_id) == forward_push_notification: + registered_channels.pop(webhook_id) + + registered_channels[webhook_id] = forward_push_notification + connection.subscriptions[msg["id"]] = unsub + connection.send_result(msg["id"]) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index af828ce423e..e375ec55ff2 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -14,6 +14,7 @@ DATA_DELETED_IDS = "deleted_ids" DATA_DEVICES = "devices" DATA_STORE = "store" DATA_NOTIFY = "notify" +DATA_PUSH_CHANNEL = "push_channel" ATTR_APP_DATA = "app_data" ATTR_APP_ID = "app_id" diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 2372ee0c515..d850d9ab469 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": ["PyNaCl==1.3.0", "emoji==1.2.0"], - "dependencies": ["http", "webhook", "person", "tag"], + "dependencies": ["http", "webhook", "person", "tag", "websocket_api"], "after_dependencies": ["cloud", "camera", "notify"], "codeowners": ["@robbiet480"], "quality_scale": "internal", diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 803f00764e7..1acb9f25c0c 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -37,6 +37,7 @@ from .const import ( ATTR_PUSH_URL, DATA_CONFIG_ENTRIES, DATA_NOTIFY, + DATA_PUSH_CHANNEL, DOMAIN, ) from .util import supports_push @@ -119,7 +120,13 @@ class MobileAppNotificationService(BaseNotificationService): if kwargs.get(ATTR_DATA) is not None: data[ATTR_DATA] = kwargs.get(ATTR_DATA) + local_push_channels = self.hass.data[DOMAIN][DATA_PUSH_CHANNEL] + for target in targets: + if target in local_push_channels: + local_push_channels[target](data) + continue + entry = self.hass.data[DOMAIN][DATA_CONFIG_ENTRIES][target] entry_data = entry.data @@ -127,7 +134,8 @@ class MobileAppNotificationService(BaseNotificationService): push_token = app_data[ATTR_PUSH_TOKEN] push_url = app_data[ATTR_PUSH_URL] - data[ATTR_PUSH_TOKEN] = push_token + target_data = dict(data) + target_data[ATTR_PUSH_TOKEN] = push_token reg_info = { ATTR_APP_ID: entry_data[ATTR_APP_ID], @@ -136,12 +144,12 @@ class MobileAppNotificationService(BaseNotificationService): if ATTR_OS_VERSION in entry_data: reg_info[ATTR_OS_VERSION] = entry_data[ATTR_OS_VERSION] - data["registration_info"] = reg_info + target_data["registration_info"] = reg_info try: with async_timeout.timeout(10): response = await async_get_clientsession(self._hass).post( - push_url, json=data + push_url, json=target_data ) result = await response.json() diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index af2c914bfbd..53ff6d1da26 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -395,7 +395,6 @@ def handle_entity_source(hass, connection, msg): connection.send_result(msg["id"], sources) -@callback @decorators.websocket_command( { vol.Required("type"): "subscribe_trigger", diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index 8823fefd92c..9c4ca146898 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -1,5 +1,6 @@ """Notify platform tests for mobile_app.""" -# pylint: disable=redefined-outer-name +from datetime import datetime, timedelta + import pytest from homeassistant.components.mobile_app.const import DOMAIN @@ -9,12 +10,10 @@ from tests.common import MockConfigEntry @pytest.fixture -async def setup_push_receiver(hass, aioclient_mock): +async def setup_push_receiver(hass, aioclient_mock, hass_admin_user): """Fixture that sets up a mocked push receiver.""" push_url = "https://mobile-push.home-assistant.dev/push" - from datetime import datetime, timedelta - now = datetime.now() + timedelta(hours=24) iso_time = now.strftime("%Y-%m-%dT%H:%M:%SZ") @@ -47,8 +46,8 @@ async def setup_push_receiver(hass, aioclient_mock): "os_version": "5.0.6", "secret": "123abc", "supports_encryption": False, - "user_id": "1a2b3c", - "webhook_id": "webhook_id", + "user_id": hass_admin_user.id, + "webhook_id": "mock-webhook_id", }, domain=DOMAIN, source="registration", @@ -118,3 +117,77 @@ async def test_notify_works(hass, aioclient_mock, setup_push_receiver): assert call_json["message"] == "Hello world" assert call_json["registration_info"]["app_id"] == "io.homeassistant.mobile_app" assert call_json["registration_info"]["app_version"] == "1.0" + + +async def test_notify_ws_works( + hass, aioclient_mock, setup_push_receiver, hass_ws_client +): + """Test notify works.""" + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 5, + "type": "mobile_app/push_notification_channel", + "webhook_id": "mock-webhook_id", + } + ) + + sub_result = await client.receive_json() + assert sub_result["success"] + + assert await hass.services.async_call( + "notify", "mobile_app_test", {"message": "Hello world"}, blocking=True + ) + + assert len(aioclient_mock.mock_calls) == 0 + + msg_result = await client.receive_json() + assert msg_result["event"] == {"message": "Hello world"} + + # Unsubscribe, now it should go over http + await client.send_json( + { + "id": 6, + "type": "unsubscribe_events", + "subscription": 5, + } + ) + sub_result = await client.receive_json() + assert sub_result["success"] + + assert await hass.services.async_call( + "notify", "mobile_app_test", {"message": "Hello world 2"}, blocking=True + ) + + assert len(aioclient_mock.mock_calls) == 1 + + # Test non-existing webhook ID + await client.send_json( + { + "id": 7, + "type": "mobile_app/push_notification_channel", + "webhook_id": "non-existing", + } + ) + sub_result = await client.receive_json() + assert not sub_result["success"] + assert sub_result["error"] == { + "code": "not_found", + "message": "Webhook ID not found", + } + + # Test webhook ID linked to other user + await client.send_json( + { + "id": 8, + "type": "mobile_app/push_notification_channel", + "webhook_id": "webhook_id_2", + } + ) + sub_result = await client.receive_json() + assert not sub_result["success"] + assert sub_result["error"] == { + "code": "unauthorized", + "message": "User not linked to this webhook ID", + } From 5ad71b5e452210ecf2f19f181453ff013eb045f8 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Mon, 17 May 2021 20:54:06 +0100 Subject: [PATCH 534/852] Define sync hass.create_task function (#50788) --- homeassistant/core.py | 7 +++++++ tests/test_core.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/homeassistant/core.py b/homeassistant/core.py index 067afb23c8a..b1610faad6e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -373,6 +373,13 @@ class HomeAssistant: return task + def create_task(self, target: Coroutine) -> None: + """Add task to the executor pool. + + target: target to call. + """ + self.loop.call_soon_threadsafe(self.async_create_task, target) + @callback def async_create_task(self, target: Coroutine) -> asyncio.tasks.Task: """Create a task from within the eventloop. diff --git a/tests/test_core.py b/tests/test_core.py index 0a205cedad1..39c5b310537 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -233,6 +233,29 @@ async def test_async_add_job_pending_tasks_coro(hass): assert len(call_count) == 2 +async def test_async_create_task_pending_tasks_coro(hass): + """Add a coro to pending tasks.""" + call_count = [] + + async def test_coro(): + """Test Coro.""" + call_count.append("call") + + for _ in range(2): + hass.create_task(test_coro()) + + async def wait_finish_callback(): + """Wait until all stuff is scheduled.""" + await asyncio.sleep(0) + await asyncio.sleep(0) + + await wait_finish_callback() + + assert len(hass._pending_tasks) == 2 + await hass.async_block_till_done() + assert len(call_count) == 2 + + async def test_async_add_job_pending_tasks_executor(hass): """Run an executor in pending tasks.""" call_count = [] From f762d3c748cf88a07865ddb66c854364f1cda835 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 May 2021 13:03:47 -0700 Subject: [PATCH 535/852] Fire time changed event in Hue tests (#50783) --- tests/components/hue/test_sensor_base.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index 46237c510f7..bc11c013555 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -4,11 +4,13 @@ from unittest.mock import Mock import aiohue +from homeassistant.components.hue import sensor_base from homeassistant.components.hue.hue_event import CONF_HUE_EVENT +from homeassistant.util import dt as dt_util from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge -from tests.common import async_capture_events +from tests.common import async_capture_events, async_fire_time_changed PRESENCE_SENSOR_1_PRESENT = { "state": {"presence": True, "lastupdated": "2019-01-01T01:00:00"}, @@ -452,7 +454,9 @@ async def test_hue_events(hass, mock_bridge): mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + async_fire_time_changed( + hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL + ) await hass.async_block_till_done() assert len(mock_bridge.mock_requests) == 2 @@ -473,7 +477,9 @@ async def test_hue_events(hass, mock_bridge): mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + async_fire_time_changed( + hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL + ) await hass.async_block_till_done() assert len(mock_bridge.mock_requests) == 3 @@ -495,7 +501,9 @@ async def test_hue_events(hass, mock_bridge): mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + async_fire_time_changed( + hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL + ) await hass.async_block_till_done() assert len(mock_bridge.mock_requests) == 4 @@ -538,7 +546,9 @@ async def test_hue_events(hass, mock_bridge): mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + async_fire_time_changed( + hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL + ) await hass.async_block_till_done() assert len(mock_bridge.mock_requests) == 5 @@ -550,7 +560,9 @@ async def test_hue_events(hass, mock_bridge): mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + async_fire_time_changed( + hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL + ) await hass.async_block_till_done() assert len(mock_bridge.mock_requests) == 6 From ba827db8ec4f0d913f353eb1d5761f7333e9c9c1 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 17 May 2021 22:12:18 +0200 Subject: [PATCH 536/852] Update remaining modbus platforms to use pymodbus_call (#50768) --- homeassistant/components/modbus/climate.py | 16 ++--- homeassistant/components/modbus/cover.py | 70 +++++++++------------- homeassistant/components/modbus/modbus.py | 20 ------- homeassistant/components/modbus/switch.py | 35 ++++------- 4 files changed, 44 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 41843fd4929..501061b9e0b 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -28,7 +28,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( ATTR_TEMPERATURE, CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, CONF_CURRENT_TEMP, CONF_CURRENT_TEMP_REGISTER_TYPE, @@ -208,10 +208,11 @@ class ModbusThermostat(ClimateEntity): ) byte_string = struct.pack(self._structure, target_temperature) register_value = struct.unpack(">h", byte_string[0:2])[0] - self._available = await self._hub.async_write_registers( + self._available = await self._hub.async_pymodbus_call( self._slave, self._target_temperature_register, register_value, + CALL_TYPE_WRITE_REGISTERS, ) await self.async_update() @@ -235,14 +236,9 @@ class ModbusThermostat(ClimateEntity): async def _async_read_register(self, register_type, register) -> float | None: """Read register using the Modbus hub slave.""" - if register_type == CALL_TYPE_REGISTER_INPUT: - result = await self._hub.async_read_input_registers( - self._slave, register, self._count - ) - else: - result = await self._hub.async_read_holding_registers( - self._slave, register, self._count - ) + result = await self._hub.async_pymodbus_call( + self._slave, register, self._count, register_type + ) if result is None: self._available = False return -1 diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index bfe4ce1fb51..b64a8d81777 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -27,7 +27,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_WRITE_COIL, + CALL_TYPE_WRITE_REGISTER, CONF_REGISTER, CONF_STATE_CLOSED, CONF_STATE_CLOSING, @@ -94,18 +95,22 @@ class ModbusCover(CoverEntity, RestoreEntity): # If we read cover status from coil, and not from optional status register, # we interpret boolean value False as closed cover, and value True as open cover. # Intermediate states are not supported in such a setup. - if self._coil is not None and self._status_register is None: - self._state_closed = False - self._state_open = True - self._state_closing = None - self._state_opening = None + if self._coil is not None: + self._write_type = CALL_TYPE_WRITE_COIL + if self._status_register is None: + self._state_closed = False + self._state_open = True + self._state_closing = None + self._state_opening = None # If we read cover status from the main register (i.e., an optional # status register is not specified), we need to make sure the register_type # is set to "holding". - if self._register is not None and self._status_register is None: - self._status_register = self._register - self._status_register_type = CALL_TYPE_REGISTER_HOLDING + if self._register is not None: + self._write_type = CALL_TYPE_WRITE_REGISTER + if self._status_register is None: + self._status_register = self._register + self._status_register_type = CALL_TYPE_REGISTER_HOLDING async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -169,21 +174,17 @@ class ModbusCover(CoverEntity, RestoreEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" - if self._coil is not None: - await self._async_write_coil(True) - else: - await self._async_write_register(self._state_open) - - await self.async_update() + self._available = await self._hub.async_pymodbus_call( + self._slave, self._register, self._state_open, self._write_type + ) + self.async_update() async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - if self._coil is not None: - await self._async_write_coil(False) - else: - await self._async_write_register(self._state_closed) - - await self.async_update() + self._available = await self._hub.async_pymodbus_call( + self._slave, self._register, self._state_closed, self._write_type + ) + self.async_update() async def async_update(self, now=None): """Update the state of the cover.""" @@ -198,14 +199,9 @@ class ModbusCover(CoverEntity, RestoreEntity): async def _async_read_status_register(self) -> int | None: """Read status register using the Modbus hub slave.""" - if self._status_register_type == CALL_TYPE_REGISTER_INPUT: - result = await self._hub.async_read_input_registers( - self._slave, self._status_register, 1 - ) - else: - result = await self._hub.async_read_holding_registers( - self._slave, self._status_register, 1 - ) + result = await self._hub.async_pymodbus_call( + self._slave, self._status_register, 1, self._status_register_type + ) if result is None: self._available = False return None @@ -215,24 +211,14 @@ class ModbusCover(CoverEntity, RestoreEntity): return value - async def _async_write_register(self, value): - """Write holding register using the Modbus hub slave.""" - self._available = await self._hub.async_write_register( - self._slave, self._register, value - ) - async def _async_read_coil(self) -> bool | None: """Read coil using the Modbus hub slave.""" - result = await self._hub.async_read_coils(self._slave, self._coil, 1) + result = await self._hub.async_pymodbus_call( + self._slave, self._coil, 1, CALL_TYPE_COIL + ) if result is None: self._available = False return None value = bool(result.bits[0] & 1) return value - - async def _async_write_coil(self, value): - """Write coil using the Modbus hub slave.""" - self._available = await self._hub.async_write_coil( - self._slave, self._coil, value - ) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 5c44580e0df..1a8da35f6fe 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -327,26 +327,6 @@ class ModbusHub: self._pymodbus_call, unit, address, value, use_call ) - async def async_read_coils(self, unit, address, count): - """Read coils.""" - return await self.async_pymodbus_call(unit, address, count, CALL_TYPE_COIL) - - async def async_read_discrete_inputs(self, unit, address, count): - """Read discrete inputs.""" - return await self.async_pymodbus_call(unit, address, count, CALL_TYPE_DISCRETE) - - async def async_read_input_registers(self, unit, address, count): - """Read input registers.""" - return await self.async_pymodbus_call( - unit, address, count, CALL_TYPE_REGISTER_INPUT - ) - - async def async_read_holding_registers(self, unit, address, count): - """Read holding registers.""" - return await self.async_pymodbus_call( - unit, address, count, CALL_TYPE_REGISTER_HOLDING - ) - async def async_write_coil(self, unit, address, value) -> bool: """Write coil.""" return await self.async_pymodbus_call( diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 9c8f2d1d12d..1589a22da4a 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -22,9 +22,6 @@ from homeassistant.helpers.typing import ConfigType from .const import ( CALL_TYPE_COIL, - CALL_TYPE_DISCRETE, - CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_REGISTER_INPUT, CONF_INPUT_TYPE, CONF_STATE_OFF, CONF_STATE_ON, @@ -62,14 +59,9 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): self._available = True self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) self._address = config[CONF_ADDRESS] - if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: - self._async_write_func = self._hub.async_write_coil - self._command_on = 0x01 - self._command_off = 0x00 - else: - self._async_write_func = self._hub.async_write_register - self._command_on = config[CONF_COMMAND_ON] - self._command_off = config[CONF_COMMAND_OFF] + self._write_type = config[CONF_WRITE_TYPE] + self._command_on = config[CONF_COMMAND_ON] + self._command_off = config[CONF_COMMAND_OFF] if CONF_VERIFY in config: if config[CONF_VERIFY] is None: config[CONF_VERIFY] = {} @@ -82,15 +74,6 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): ) self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self._command_on) self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) - - if self._verify_type == CALL_TYPE_REGISTER_HOLDING: - self._async_read_func = self._hub.async_read_holding_registers - elif self._verify_type == CALL_TYPE_DISCRETE: - self._async_read_func = self._hub.async_read_discrete_inputs - elif self._verify_type == CALL_TYPE_REGISTER_INPUT: - self._async_read_func = self._hub.async_read_input_registers - else: # self._verify_type == CALL_TYPE_COIL: - self._async_read_func = self._hub.async_read_coils else: self._verify_active = False @@ -125,8 +108,8 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs): """Set switch on.""" - result = await self._async_write_func( - self._slave, self._address, self._command_on + result = await self._hub.async_pymodbus_call( + self._slave, self._address, self._command_on, self._write_type ) if result is False: self._available = False @@ -141,8 +124,8 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): async def async_turn_off(self, **kwargs): """Set switch off.""" - result = await self._async_write_func( - self._slave, self._address, self._command_off + result = await self._hub.async_pymodbus_call( + self._slave, self._address, self._command_off, self._write_type ) if result is False: self._available = False @@ -164,7 +147,9 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): self.async_write_ha_state() return - result = await self._async_read_func(self._slave, self._verify_address, 1) + result = await self._hub.async_pymodbus_call( + self._slave, self._verify_address, 1, self._verify_type + ) if result is None: self._available = False self.async_write_ha_state() From 6f7ae3727b4d03a520991c4d0f34006663ed3218 Mon Sep 17 00:00:00 2001 From: G Johansson <62932417+gjohansson-ST@users.noreply.github.com> Date: Mon, 17 May 2021 22:39:56 +0200 Subject: [PATCH 537/852] Bump yalesmartalarmclient to 0.3.3 (#50613) --- CODEOWNERS | 1 + homeassistant/components/yale_smart_alarm/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a6e9461544c..12689918e55 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -561,6 +561,7 @@ homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf +homeassistant/components/yale_smart_alarm/* @gjohansson-ST homeassistant/components/yamaha_musiccast/* @jalmeroth homeassistant/components/yandex_transport/* @rishatik92 @devbis homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index fd1fa3bee23..e900f4e0373 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -2,7 +2,7 @@ "domain": "yale_smart_alarm", "name": "Yale Smart Living", "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", - "requirements": ["yalesmartalarmclient==0.1.6"], - "codeowners": [], + "requirements": ["yalesmartalarmclient==0.3.3"], + "codeowners": ["@gjohansson-ST"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 5b84ddc46df..feadab872ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2378,7 +2378,7 @@ xmltodict==0.12.0 xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.1.6 +yalesmartalarmclient==0.3.3 # homeassistant.components.august yalexs==1.1.11 From b1ff9dc45efa5ac08304b79705839c7103633cb9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 17 May 2021 16:06:13 -0500 Subject: [PATCH 538/852] Bump pysonos to 0.0.47 (#50792) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index e410fff2a07..5d2ad08bf03 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.46"], + "requirements": ["pysonos==0.0.47"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index feadab872ad..52a57ffd4bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1750,7 +1750,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.46 +pysonos==0.0.47 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15a8c0af298..46e30480ac8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.46 +pysonos==0.0.47 # homeassistant.components.spc pyspcwebgw==0.4.0 From e7f7e61e88a208d2dc01ea7d053aa578d35c19f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 May 2021 17:27:51 -0400 Subject: [PATCH 539/852] Ensure a wal checkpoint is scheduled nightly (#50746) --- homeassistant/components/recorder/__init__.py | 31 ++++++++++--- homeassistant/components/recorder/util.py | 12 +++++ tests/components/recorder/test_init.py | 46 ++++++++++++++++++- tests/components/recorder/test_util.py | 8 ++++ 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 091bff8445f..e2a5f6b9a6d 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -54,6 +54,7 @@ from .util import ( dburl_to_path, end_incomplete_runs, move_away_broken_database, + perodic_db_cleanups, session_scope, setup_connection_for_dialect, validate_or_move_away_sqlite_database, @@ -278,6 +279,10 @@ class PurgeTask(NamedTuple): apply_filter: bool +class PerodicCleanupTask: + """An object to insert into the recorder to trigger cleanup tasks when auto purge is disabled.""" + + class StatisticsTask(NamedTuple): """An object to insert into the recorder queue to run a statistics task.""" @@ -484,9 +489,15 @@ class Recorder(threading.Thread): self.async_recorder_ready.set() @callback - def async_purge(self, now): + def async_nightly_tasks(self, now): """Trigger the purge.""" - self.queue.put(PurgeTask(self.keep_days, repack=False, apply_filter=False)) + if self.auto_purge: + # Purge will schedule the perodic cleanups + # after it completes to ensure it does not happen + # until after the database is vacuumed + self.queue.put(PurgeTask(self.keep_days, repack=False, apply_filter=False)) + else: + self.queue.put(PerodicCleanupTask()) @callback def async_hourly_statistics(self, now): @@ -496,11 +507,10 @@ class Recorder(threading.Thread): def _async_setup_periodic_tasks(self): """Prepare periodic tasks.""" - if self.auto_purge: - # Purge every night at 4:12am - async_track_time_change( - self.hass, self.async_purge, hour=4, minute=12, second=0 - ) + # Run nightly tasks at 4:12am + async_track_time_change( + self.hass, self.async_nightly_tasks, hour=4, minute=12, second=0 + ) # Compile hourly statistics every hour at *:12 async_track_time_change( self.hass, self.async_hourly_statistics, minute=12, second=0 @@ -646,6 +656,10 @@ class Recorder(threading.Thread): def _run_purge(self, keep_days, repack, apply_filter): """Purge the database.""" if purge.purge_old_data(self, keep_days, repack, apply_filter): + # We always need to do the db cleanups after a purge + # is finished to ensure the WAL checkpoint and other + # tasks happen after a vacuum. + perodic_db_cleanups(self) return # Schedule a new purge task if this one didn't finish self.queue.put(PurgeTask(keep_days, repack, apply_filter)) @@ -662,6 +676,9 @@ class Recorder(threading.Thread): if isinstance(event, PurgeTask): self._run_purge(event.keep_days, event.repack, event.apply_filter) return + if isinstance(event, PerodicCleanupTask): + perodic_db_cleanups(self) + return if isinstance(event, StatisticsTask): self._run_statistics(event.start) return diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 186aad4fe9e..db9fb46425b 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -318,3 +318,15 @@ def retryable_database_job(description: str): return wrapper return decorator + + +def perodic_db_cleanups(instance: Recorder): + """Run any database cleanups that need to happen perodiclly. + + These cleanups will happen nightly or after any purge. + """ + + if instance.engine.dialect.name == "sqlite": + # Execute sqlite to create a wal checkpoint and free up disk space + _LOGGER.debug("WAL checkpoint") + instance.engine.execute("PRAGMA wal_checkpoint(TRUNCATE);") diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 540764b2ed0..bb334599c26 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -8,6 +8,7 @@ from sqlalchemy.exc import DatabaseError, OperationalError, SQLAlchemyError from homeassistant.components import recorder from homeassistant.components.recorder import ( + CONF_AUTO_PURGE, CONF_DB_URL, CONFIG_SCHEMA, DOMAIN, @@ -610,30 +611,73 @@ def test_auto_purge(hass_recorder): with patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True - ) as purge_old_data: + ) as purge_old_data, patch( + "homeassistant.components.recorder.perodic_db_cleanups" + ) as perodic_db_cleanups: # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 1 + assert len(perodic_db_cleanups.mock_calls) == 1 purge_old_data.reset_mock() + perodic_db_cleanups.reset_mock() # Advance one day, and the purge task should run again test_time = test_time + timedelta(days=1) run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 1 + assert len(perodic_db_cleanups.mock_calls) == 1 purge_old_data.reset_mock() + perodic_db_cleanups.reset_mock() # Advance less than one full day. The alarm should not yet fire. test_time = test_time + timedelta(hours=23) run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 0 + assert len(perodic_db_cleanups.mock_calls) == 0 # Advance to the next day and fire the alarm again test_time = test_time + timedelta(hours=1) run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 1 + assert len(perodic_db_cleanups.mock_calls) == 1 + + dt_util.set_default_time_zone(original_tz) + + +def test_auto_purge_disabled(hass_recorder): + """Test periodic db cleanup still run when auto purge is disabled.""" + hass = hass_recorder({CONF_AUTO_PURGE: False}) + + original_tz = dt_util.DEFAULT_TIME_ZONE + + tz = dt_util.get_time_zone("Europe/Copenhagen") + dt_util.set_default_time_zone(tz) + + # Purging is scheduled to happen at 4:12am every day. We want + # to verify that when auto purge is disabled perodic db cleanups + # are still scheduled + # + # The clock is started at 4:15am then advanced forward below + now = dt_util.utcnow() + test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) + run_tasks_at_time(hass, test_time) + + with patch( + "homeassistant.components.recorder.purge.purge_old_data", return_value=True + ) as purge_old_data, patch( + "homeassistant.components.recorder.perodic_db_cleanups" + ) as perodic_db_cleanups: + # Advance one day, and the purge task should run + test_time = test_time + timedelta(days=1) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 0 + assert len(perodic_db_cleanups.mock_calls) == 1 + + purge_old_data.reset_mock() + perodic_db_cleanups.reset_mock() dt_util.set_default_time_zone(original_tz) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 0a9f90be83e..5ba206a751f 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -269,3 +269,11 @@ def test_end_incomplete_runs(hass_recorder, caplog): assert run_info.end == now_without_tz assert "Ended unfinished session" in caplog.text + + +def test_perodic_db_cleanups(hass_recorder): + """Test perodic db cleanups.""" + hass = hass_recorder() + with patch.object(hass.data[DATA_INSTANCE].engine, "execute") as execute_mock: + util.perodic_db_cleanups(hass.data[DATA_INSTANCE]) + assert execute_mock.call_args[0][0] == "PRAGMA wal_checkpoint(TRUNCATE);" From 9e681cd214b5a8971c1439968c7bd59e39f78427 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 May 2021 23:50:12 +0200 Subject: [PATCH 540/852] Refactor MQTT basic light pt5: Add RGB color helpers (#50780) * Refactor MQTT basic light pt5: Add RGB color helpers * Revert change of rounding instead of truncating RGB --- .../components/mqtt/light/schema_basic.py | 107 ++++++++++-------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index b96a5e4a815..f7c1648e96b 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -11,6 +11,7 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + COLOR_MODE_RGB, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -324,20 +325,31 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) restore_state(ATTR_BRIGHTNESS) + def _rgbx_received(msg, template, color_mode, convert_color): + """Handle new MQTT messages for RGBW and RGBWW.""" + payload = self._value_templates[template](msg.payload, None) + if not payload: + _LOGGER.debug( + "Ignoring empty %s message from '%s'", color_mode, msg.topic + ) + return None + color = tuple(int(val) for val in payload.split(",")) + if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: + rgb = convert_color(*color) + percent_bright = float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0 + self._brightness = percent_bright * 255 + return color + @callback @log_messages(self.hass, self.entity_id) def rgb_received(msg): """Handle new MQTT messages for RGB.""" - payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload, None) - if not payload: - _LOGGER.debug("Ignoring empty rgb message from '%s'", msg.topic) + rgb = _rgbx_received( + msg, CONF_RGB_VALUE_TEMPLATE, COLOR_MODE_RGB, lambda *x: x + ) + if not rgb: return - - rgb = [int(val) for val in payload.split(",")] self._hs_color = color_util.color_RGB_to_hs(*rgb) - if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: - percent_bright = float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0 - self._brightness = percent_bright * 255 self.async_write_ha_state() add_topic(CONF_RGB_STATE_TOPIC, rgb_received) @@ -385,9 +397,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not payload: _LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic) return - try: - hs_color = [float(val) for val in payload.split(",", 2)] + hs_color = tuple(float(val) for val in payload.split(",", 2)) self._hs_color = hs_color self.async_write_ha_state() except ValueError: @@ -424,7 +435,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): _LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic) return - xy_color = [float(val) for val in payload.split(",")] + xy_color = tuple(float(val) for val in payload.split(",")) self._hs_color = color_util.color_xy_to_hs(*xy_color) self.async_write_ha_state() @@ -550,6 +561,29 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._config[CONF_RETAIN], ) + def scale_rgbx(color, brightness=None): + """Scale RGBx for brightness.""" + if brightness is None: + # If there's a brightness topic set, we don't want to scale the RGBx + # values given using the brightness. + if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255 + ) + return tuple(int(channel * brightness / 255) for channel in color) + + def render_rgbx(color, template): + """Render RGBx payload.""" + tpl = self._command_templates[template] + if tpl: + keys = ["red", "green", "blue"] + rgb_color_str = tpl(zip(keys, color)) + else: + rgb_color_str = ",".join(str(channel) for channel in color) + return rgb_color_str + def set_optimistic(attribute, value, condition_attribute=None): """Optimistically update a state attribute.""" if condition_attribute is None: @@ -569,38 +603,20 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): elif on_command_type == "brightness" and ATTR_BRIGHTNESS not in kwargs: kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 - if ATTR_HS_COLOR in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None: - - hs_color = kwargs[ATTR_HS_COLOR] - - # If there's a brightness topic set, we don't want to scale the RGB - # values given using the brightness. - if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: - brightness = 255 - else: - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255 - ) - rgb = color_util.color_hsv_to_RGB( - hs_color[0], hs_color[1], brightness / 255 * 100 - ) - tpl = self._command_templates[CONF_RGB_COMMAND_TEMPLATE] - if tpl: - rgb_color_str = tpl({"red": rgb[0], "green": rgb[1], "blue": rgb[2]}) - else: - rgb_color_str = f"{rgb[0]},{rgb[1]},{rgb[2]}" - - publish(CONF_RGB_COMMAND_TOPIC, rgb_color_str) + hs_color = kwargs.get(ATTR_HS_COLOR) + if hs_color and self._topic[CONF_RGB_COMMAND_TOPIC] is not None: + # Convert HS to RGB + rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100)) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE) + publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ATTR_RGB_COLOR) - if ATTR_HS_COLOR in kwargs and self._topic[CONF_HS_COMMAND_TOPIC] is not None: - hs_color = kwargs[ATTR_HS_COLOR] + if hs_color and self._topic[CONF_HS_COMMAND_TOPIC] is not None: publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") should_update |= set_optimistic(ATTR_HS_COLOR, hs_color) - if ATTR_HS_COLOR in kwargs and self._topic[CONF_XY_COMMAND_TOPIC] is not None: - - xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + if hs_color and self._topic[CONF_XY_COMMAND_TOPIC] is not None: + xy_color = color_util.color_hs_to_xy(*hs_color) publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ATTR_XY_COLOR) @@ -623,16 +639,10 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): and self._topic[CONF_RGB_COMMAND_TOPIC] is not None ): hs_color = self._hs_color if self._hs_color is not None else (0, 0) - rgb = color_util.color_hsv_to_RGB( - hs_color[0], hs_color[1], kwargs[ATTR_BRIGHTNESS] / 255 * 100 - ) - tpl = self._command_templates[CONF_RGB_COMMAND_TEMPLATE] - if tpl: - rgb_color_str = tpl({"red": rgb[0], "green": rgb[1], "blue": rgb[2]}) - else: - rgb_color_str = f"{rgb[0]},{rgb[1]},{rgb[2]}" - - publish(CONF_RGB_COMMAND_TOPIC, rgb_color_str) + brightness = kwargs[ATTR_BRIGHTNESS] + rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100), brightness) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE) + publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) if ( @@ -641,7 +651,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ): color_temp = int(kwargs[ATTR_COLOR_TEMP]) tpl = self._command_templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE] - if tpl: color_temp = tpl({"value": color_temp}) From 8129db1cfec67d962a90a61d655411cf1a11fdf5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 17 May 2021 17:26:48 -0500 Subject: [PATCH 541/852] Handle Sonos subscription renewal failures (#50793) --- homeassistant/components/sonos/const.py | 1 + homeassistant/components/sonos/speaker.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 9aaecee08af..e14024c32d2 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -147,3 +147,4 @@ BATTERY_SCAN_INTERVAL = datetime.timedelta(minutes=15) SCAN_INTERVAL = datetime.timedelta(seconds=10) DISCOVERY_INTERVAL = datetime.timedelta(seconds=60) SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL +SUBSCRIPTION_TIMEOUT = 1200 diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 0c2b28dbdf2..e647fc2fd68 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -48,6 +48,7 @@ from .const import ( SONOS_STATE_UPDATED, SOURCE_LINEIN, SOURCE_TV, + SUBSCRIPTION_TIMEOUT, ) from .favorites import SonosFavorites from .helpers import soco_error @@ -251,8 +252,11 @@ class SonosSpeaker: self, target: SubscriptionBase, sub_callback: Callable ) -> None: """Create a Sonos subscription.""" - subscription = await target.subscribe(auto_renew=True) + subscription = await target.subscribe( + auto_renew=True, requested_timeout=SUBSCRIPTION_TIMEOUT + ) subscription.callback = sub_callback + subscription.auto_renew_fail = self.async_renew_failed self._subscriptions.append(subscription) @callback @@ -309,11 +313,19 @@ class SonosSpeaker: self.async_write_entity_states() + @callback + def async_renew_failed(self, exception: Exception) -> None: + """Handle a failed subscription renewal.""" + if self.available: + self.hass.async_add_job(self.async_unseen) + async def async_unseen(self, now: datetime.datetime | None = None) -> None: """Make this player unavailable when it was not seen recently.""" self.async_write_entity_states() - self._seen_timer = None + if self._seen_timer: + self._seen_timer() + self._seen_timer = None if self._poll_timer: self._poll_timer() From a43561e3e6fa76a3e086be8c076454738197ec73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 May 2021 18:32:05 -0400 Subject: [PATCH 542/852] Ensure startup can proceed if async_get_integration raises (#50799) * Ensure startup can proceed if async_get_integration raises There were cases where the event would never get set and startup would deadlock because the second attempt to load the integration would block forever * pylint * reorder --- homeassistant/loader.py | 34 +++++++++++++++++++--------------- tests/test_loader.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index abc5e533df5..adebe535f6a 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -506,31 +506,35 @@ async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration event = cache[domain] = asyncio.Event() + try: + integration = await _async_get_integration(hass, domain) + except Exception: # pylint: disable=broad-except + # Remove event from cache. + cache.pop(domain) + event.set() + raise + + cache[domain] = integration + event.set() + return integration + + +async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integration: # Instead of using resolve_from_root we use the cache of custom # components to find the integration. - integration = (await async_get_custom_components(hass)).get(domain) - if integration is not None: + if integration := (await async_get_custom_components(hass)).get(domain): validate_custom_integration_version(integration) _LOGGER.warning(CUSTOM_WARNING, integration.domain) - cache[domain] = integration - event.set() return integration from homeassistant import components # pylint: disable=import-outside-toplevel - integration = await hass.async_add_executor_job( + if integration := await hass.async_add_executor_job( Integration.resolve_from_root, hass, components, domain - ) - event.set() + ): + return integration - if not integration: - # Remove event from cache. - cache.pop(domain) - raise IntegrationNotFound(domain) - - cache[domain] = integration - - return integration + raise IntegrationNotFound(domain) class LoaderError(Exception): diff --git a/tests/test_loader.py b/tests/test_loader.py index c2c176b62ab..e696f27351d 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -467,3 +467,32 @@ async def test_get_custom_components_safe_mode(hass): """Test that we get empty custom components in safe mode.""" hass.config.safe_mode = True assert await loader.async_get_custom_components(hass) == {} + + +async def test_custom_integration_missing_version(hass, caplog): + """Test trying to load a custom integration without a version twice does not deadlock.""" + test_integration_1 = loader.Integration( + hass, "custom_components.test1", None, {"domain": "test1"} + ) + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = { + "test1": test_integration_1, + } + + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test1") + + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test1") + + +async def test_custom_integration_missing(hass, caplog): + """Test trying to load a custom integration that is missing twice not deadlock.""" + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = {} + + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test1") + + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test1") From 781524ee363d1c449f390b3c2a19c3a480920edf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 May 2021 16:54:06 -0700 Subject: [PATCH 543/852] Updated frontend to 20210517.0 (#50804) --- 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 65927e6da0c..d7cecbe163a 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==20210504.0" + "home-assistant-frontend==20210517.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d957f9c7cd9..0d51642db39 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210504.0 +home-assistant-frontend==20210517.0 httpx==0.18.0 jinja2>=2.11.3 netdisco==2.8.3 diff --git a/requirements_all.txt b/requirements_all.txt index 52a57ffd4bc..9c38fa48803 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210504.0 +home-assistant-frontend==20210517.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 46e30480ac8..67a4b3f20cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -432,7 +432,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210504.0 +home-assistant-frontend==20210517.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 1ea0d8ae025e649fa064bd5e4c835a6eb3436c64 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 18 May 2021 01:54:17 +0200 Subject: [PATCH 544/852] Correct trace of condition actions (#50800) --- homeassistant/helpers/condition.py | 13 ++++++++++--- homeassistant/helpers/script.py | 10 +++++++--- homeassistant/helpers/trace.py | 7 +++++++ tests/helpers/test_script.py | 14 ++++++++------ 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index d1a7c95d16c..a59ad459874 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -112,15 +112,22 @@ def condition_trace_update_result(**kwargs: Any) -> None: @contextmanager def trace_condition(variables: TemplateVarsType) -> Generator: """Trace condition evaluation.""" - trace_element = condition_trace_append(variables, trace_path_get()) - trace_stack_push(trace_stack_cv, trace_element) + should_pop = True + trace_element = trace_stack_top(trace_stack_cv) + if trace_element and trace_element.reuse_by_child: + should_pop = False + trace_element.reuse_by_child = False + else: + trace_element = condition_trace_append(variables, trace_path_get()) + trace_stack_push(trace_stack_cv, trace_element) try: yield trace_element except Exception as ex: trace_element.set_error(ex) raise ex finally: - trace_stack_pop(trace_stack_cv) + if should_pop: + trace_stack_pop(trace_stack_cv) def trace_condition_function(condition: ConditionCheckerType) -> ConditionCheckerType: diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1deb5a5073f..ea3635888bb 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -87,6 +87,8 @@ from .trace import ( trace_stack_cv, trace_stack_pop, trace_stack_push, + trace_stack_top, + trace_update_result, ) # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -619,14 +621,16 @@ class _ScriptRun: ) cond = await self._async_get_condition(self._action) try: - with trace_path("condition"): - check = cond(self._hass, self._variables) + trace_element = trace_stack_top(trace_stack_cv) + if trace_element: + trace_element.reuse_by_child = True + check = cond(self._hass, self._variables) except exceptions.ConditionError as ex: _LOGGER.warning("Error in 'condition' evaluation:\n%s", ex) check = False self._log("Test condition %s: %s", self._script.last_action, check) - trace_set_result(result=check) + trace_update_result(result=check) if not check: raise _StopScript diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index e2d5144f374..bfa713a3b2a 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -22,6 +22,7 @@ class TraceElement: self._error: Exception | None = None self.path: str = path self._result: dict | None = None + self.reuse_by_child = False self._timestamp = dt_util.utcnow() if variables is None: @@ -198,6 +199,12 @@ def trace_set_result(**kwargs: Any) -> None: node.set_result(**kwargs) +def trace_update_result(**kwargs: Any) -> None: + """Update the result of TraceElement at the top of the stack.""" + node = cast(TraceElement, trace_stack_top(trace_stack_cv)) + node.update_result(**kwargs) + + class StopReason: """Mutable container class for script_execution.""" diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 2045f8cdbbc..546f494735e 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1412,8 +1412,7 @@ async def test_condition_warning(hass, caplog): { "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [{"error_type": script._StopScript, "result": {"result": False}}], - "1/condition": [{"error_type": ConditionError}], - "1/condition/entity_id/0": [{"error_type": ConditionError}], + "1/entity_id/0": [{"error_type": ConditionError}], }, expected_script_execution="aborted", ) @@ -1448,8 +1447,7 @@ async def test_condition_basic(hass, caplog): assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{"result": {"result": True}}], - "1/condition": [{"result": {"entities": ["test.entity"], "result": True}}], + "1": [{"result": {"entities": ["test.entity"], "result": True}}], "2": [{"result": {"event": "test_event", "event_data": {}}}], } ) @@ -1465,8 +1463,12 @@ async def test_condition_basic(hass, caplog): assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{"error_type": script._StopScript, "result": {"result": False}}], - "1/condition": [{"result": {"entities": ["test.entity"], "result": False}}], + "1": [ + { + "error_type": script._StopScript, + "result": {"entities": ["test.entity"], "result": False}, + } + ], }, expected_script_execution="aborted", ) From 1f80defe3abd07b28e78721c0bf47659d97afb4b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 18 May 2021 00:12:13 +0000 Subject: [PATCH 545/852] [ci skip] Translation update --- .../components/aemet/translations/ca.json | 9 +++++ .../components/aemet/translations/et.json | 9 +++++ .../components/aemet/translations/it.json | 9 +++++ .../components/aemet/translations/nl.json | 9 +++++ .../components/aemet/translations/ru.json | 9 +++++ .../aemet/translations/zh-Hant.json | 9 +++++ .../components/apple_tv/translations/it.json | 2 +- .../components/arcam_fmj/translations/it.json | 2 +- .../automation/translations/ru.json | 4 +- .../azure_devops/translations/it.json | 2 +- .../binary_sensor/translations/ru.json | 8 ++-- .../components/blebox/translations/it.json | 2 +- .../components/bond/translations/it.json | 2 +- .../components/bosch_shc/translations/ca.json | 38 +++++++++++++++++++ .../components/bosch_shc/translations/et.json | 38 +++++++++++++++++++ .../components/bosch_shc/translations/it.json | 38 +++++++++++++++++++ .../components/bosch_shc/translations/nl.json | 38 +++++++++++++++++++ .../components/bosch_shc/translations/ru.json | 38 +++++++++++++++++++ .../bosch_shc/translations/zh-Hant.json | 38 +++++++++++++++++++ .../components/brother/translations/it.json | 2 +- .../components/bsblan/translations/it.json | 2 +- .../components/calendar/translations/ru.json | 4 +- .../components/canary/translations/it.json | 2 +- .../components/cast/translations/et.json | 2 +- .../components/cast/translations/zh-Hant.json | 2 +- .../cloudflare/translations/it.json | 2 +- .../components/deconz/translations/it.json | 2 +- .../components/denonavr/translations/it.json | 2 +- .../components/directv/translations/it.json | 2 +- .../components/doorbird/translations/it.json | 2 +- .../components/elgato/translations/it.json | 2 +- .../components/emonitor/translations/it.json | 2 +- .../enphase_envoy/translations/it.json | 2 +- .../components/esphome/translations/it.json | 2 +- .../components/fan/translations/ru.json | 4 +- .../forked_daapd/translations/it.json | 2 +- .../components/fritz/translations/ca.json | 11 ++++++ .../components/fritz/translations/et.json | 11 ++++++ .../components/fritz/translations/it.json | 13 ++++++- .../components/fritz/translations/nl.json | 11 ++++++ .../components/fritz/translations/ru.json | 11 ++++++ .../fritz/translations/zh-Hant.json | 11 ++++++ .../components/fritzbox/translations/it.json | 2 +- .../fritzbox_callmonitor/translations/it.json | 2 +- .../garages_amsterdam/translations/ca.json | 18 +++++++++ .../garages_amsterdam/translations/et.json | 18 +++++++++ .../garages_amsterdam/translations/it.json | 18 +++++++++ .../garages_amsterdam/translations/nl.json | 18 +++++++++ .../garages_amsterdam/translations/ru.json | 18 +++++++++ .../translations/zh-Hant.json | 18 +++++++++ .../components/goalzero/translations/ca.json | 7 +++- .../components/goalzero/translations/et.json | 8 +++- .../components/goalzero/translations/it.json | 10 ++++- .../components/goalzero/translations/nl.json | 8 +++- .../components/goalzero/translations/ru.json | 10 ++++- .../goalzero/translations/zh-Hant.json | 10 ++++- .../components/gogogate2/translations/it.json | 3 +- .../components/group/translations/ru.json | 4 +- .../growatt_server/translations/ca.json | 2 +- .../growatt_server/translations/et.json | 2 +- .../growatt_server/translations/it.json | 2 +- .../growatt_server/translations/nl.json | 2 +- .../growatt_server/translations/ru.json | 2 +- .../growatt_server/translations/zh-Hant.json | 2 +- .../components/guardian/translations/it.json | 3 ++ .../components/harmony/translations/it.json | 2 +- .../homekit_controller/translations/it.json | 2 +- .../huawei_lte/translations/it.json | 2 +- .../humidifier/translations/ru.json | 4 +- .../input_boolean/translations/ru.json | 4 +- .../components/ipp/translations/it.json | 2 +- .../components/isy994/translations/it.json | 2 +- .../components/kodi/translations/it.json | 2 +- .../components/kraken/translations/ca.json | 22 +++++++++++ .../components/kraken/translations/et.json | 22 +++++++++++ .../components/kraken/translations/it.json | 22 +++++++++++ .../components/kraken/translations/nl.json | 21 ++++++++++ .../components/kraken/translations/ru.json | 22 +++++++++++ .../kraken/translations/zh-Hant.json | 22 +++++++++++ .../components/light/translations/ru.json | 4 +- .../lutron_caseta/translations/it.json | 2 +- .../media_player/translations/ru.json | 4 +- .../components/nexia/translations/ca.json | 1 + .../components/nexia/translations/et.json | 1 + .../components/nexia/translations/it.json | 1 + .../components/nexia/translations/nl.json | 1 + .../components/nexia/translations/ru.json | 1 + .../nexia/translations/zh-Hant.json | 1 + .../components/nzbget/translations/it.json | 2 +- .../ovo_energy/translations/it.json | 2 +- .../components/plugwise/translations/it.json | 2 +- .../components/powerwall/translations/it.json | 2 +- .../rainmachine/translations/it.json | 2 +- .../components/remote/translations/ru.json | 4 +- .../components/roku/translations/it.json | 2 +- .../components/roomba/translations/it.json | 2 +- .../components/samsungtv/translations/it.json | 2 +- .../screenlogic/translations/it.json | 2 +- .../components/script/translations/ru.json | 4 +- .../components/sensor/translations/ru.json | 4 +- .../components/smappee/translations/it.json | 2 +- .../somfy_mylink/translations/it.json | 2 +- .../components/sonarr/translations/it.json | 2 +- .../components/songpal/translations/it.json | 2 +- .../squeezebox/translations/it.json | 2 +- .../components/switch/translations/ru.json | 4 +- .../components/syncthru/translations/it.json | 2 +- .../synology_dsm/translations/it.json | 2 +- .../system_bridge/translations/it.json | 2 +- .../components/unifi/translations/it.json | 2 +- .../components/upnp/translations/ca.json | 10 +++++ .../components/upnp/translations/en.json | 4 +- .../components/upnp/translations/et.json | 10 +++++ .../components/upnp/translations/it.json | 12 +++++- .../components/upnp/translations/nl.json | 10 +++++ .../components/upnp/translations/ru.json | 10 +++++ .../water_heater/translations/ru.json | 2 +- .../components/wilight/translations/it.json | 2 +- .../components/withings/translations/it.json | 2 +- .../components/wled/translations/it.json | 2 +- .../xiaomi_aqara/translations/it.json | 2 +- .../xiaomi_miio/translations/it.json | 2 +- .../components/yeelight/translations/it.json | 4 ++ .../components/zha/translations/it.json | 2 +- 124 files changed, 789 insertions(+), 101 deletions(-) create mode 100644 homeassistant/components/bosch_shc/translations/ca.json create mode 100644 homeassistant/components/bosch_shc/translations/et.json create mode 100644 homeassistant/components/bosch_shc/translations/it.json create mode 100644 homeassistant/components/bosch_shc/translations/nl.json create mode 100644 homeassistant/components/bosch_shc/translations/ru.json create mode 100644 homeassistant/components/bosch_shc/translations/zh-Hant.json create mode 100644 homeassistant/components/garages_amsterdam/translations/ca.json create mode 100644 homeassistant/components/garages_amsterdam/translations/et.json create mode 100644 homeassistant/components/garages_amsterdam/translations/it.json create mode 100644 homeassistant/components/garages_amsterdam/translations/nl.json create mode 100644 homeassistant/components/garages_amsterdam/translations/ru.json create mode 100644 homeassistant/components/garages_amsterdam/translations/zh-Hant.json create mode 100644 homeassistant/components/kraken/translations/ca.json create mode 100644 homeassistant/components/kraken/translations/et.json create mode 100644 homeassistant/components/kraken/translations/it.json create mode 100644 homeassistant/components/kraken/translations/nl.json create mode 100644 homeassistant/components/kraken/translations/ru.json create mode 100644 homeassistant/components/kraken/translations/zh-Hant.json diff --git a/homeassistant/components/aemet/translations/ca.json b/homeassistant/components/aemet/translations/ca.json index 85b22e72d76..75784ddfc87 100644 --- a/homeassistant/components/aemet/translations/ca.json +++ b/homeassistant/components/aemet/translations/ca.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Obt\u00e9 les dades de les estacions meteorol\u00f2giques d'AEMET" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/et.json b/homeassistant/components/aemet/translations/et.json index bc0a26179d5..0d542fcc744 100644 --- a/homeassistant/components/aemet/translations/et.json +++ b/homeassistant/components/aemet/translations/et.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Koguandmeid AEMETi ilmajaamadest" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/it.json b/homeassistant/components/aemet/translations/it.json index 112630028b9..a55e003ca4e 100644 --- a/homeassistant/components/aemet/translations/it.json +++ b/homeassistant/components/aemet/translations/it.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Raccogli i dati dalle stazioni meteorologiche AEMET" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/nl.json b/homeassistant/components/aemet/translations/nl.json index 77589e20490..40fab5d9e0f 100644 --- a/homeassistant/components/aemet/translations/nl.json +++ b/homeassistant/components/aemet/translations/nl.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Verzamel gegevens van AEMET-weerstations" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/ru.json b/homeassistant/components/aemet/translations/ru.json index 4da9a032d2b..1dc0e21b0df 100644 --- a/homeassistant/components/aemet/translations/ru.json +++ b/homeassistant/components/aemet/translations/ru.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "\u0421\u0431\u043e\u0440 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0439 AEMET" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/zh-Hant.json b/homeassistant/components/aemet/translations/zh-Hant.json index 75b251ae2ff..e2b1eef10b9 100644 --- a/homeassistant/components/aemet/translations/zh-Hant.json +++ b/homeassistant/components/aemet/translations/zh-Hant.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "\u81ea AEMET \u6c23\u8c61\u7ad9\u7372\u5f97\u8cc7\u6599" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/it.json b/homeassistant/components/apple_tv/translations/it.json index 7ed3306721c..8faf1be4326 100644 --- a/homeassistant/components/apple_tv/translations/it.json +++ b/homeassistant/components/apple_tv/translations/it.json @@ -16,7 +16,7 @@ "no_usable_service": "\u00c8 stato trovato un dispositivo ma non \u00e8 stato possibile identificare alcun modo per stabilire una connessione ad esso. Se continui a vedere questo messaggio, prova a specificarne l'indirizzo IP o a riavviare l'Apple TV.", "unknown": "Errore imprevisto" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Stai per aggiungere l'Apple TV denominata \"{name}\" a Home Assistant. \n\n **Per completare la procedura, potrebbe essere necessario inserire pi\u00f9 codici PIN.** \n\nTieni presente che *non* sarai in grado di spegnere la tua Apple TV con questa integrazione. Solo il lettore multimediale in Home Assistant si spegner\u00e0!", diff --git a/homeassistant/components/arcam_fmj/translations/it.json b/homeassistant/components/arcam_fmj/translations/it.json index f5cef4cd8b0..24c9b99e7a8 100644 --- a/homeassistant/components/arcam_fmj/translations/it.json +++ b/homeassistant/components/arcam_fmj/translations/it.json @@ -5,7 +5,7 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "cannot_connect": "Impossibile connettersi" }, - "flow_title": "Arcam FMJ su {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "Vuoi aggiungere Arcam FMJ su `{host}` a Home Assistant?" diff --git a/homeassistant/components/automation/translations/ru.json b/homeassistant/components/automation/translations/ru.json index 79732bea385..d98f55a898e 100644 --- a/homeassistant/components/automation/translations/ru.json +++ b/homeassistant/components/automation/translations/ru.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f" diff --git a/homeassistant/components/azure_devops/translations/it.json b/homeassistant/components/azure_devops/translations/it.json index 4b2f5e0efae..02900b93935 100644 --- a/homeassistant/components/azure_devops/translations/it.json +++ b/homeassistant/components/azure_devops/translations/it.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticazione non valida", "project_error": "Non \u00e8 stato possibile ottenere informazioni sul progetto." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index fe9e6773547..2db1506b392 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -91,8 +91,8 @@ }, "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "battery": { "off": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439", @@ -127,8 +127,8 @@ "on": "\u041d\u0430\u0433\u0440\u0435\u0432" }, "light": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u041d\u0435\u0442 \u0441\u0432\u0435\u0442\u0430", + "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0441\u0432\u0435\u0442" }, "lock": { "off": "\u0417\u0430\u043a\u0440\u044b\u0442", diff --git a/homeassistant/components/blebox/translations/it.json b/homeassistant/components/blebox/translations/it.json index 265a158e22d..6d377840e90 100644 --- a/homeassistant/components/blebox/translations/it.json +++ b/homeassistant/components/blebox/translations/it.json @@ -9,7 +9,7 @@ "unknown": "Errore imprevisto", "unsupported_version": "Il dispositivo BleBox ha un firmware obsoleto. Si prega di aggiornarlo prima." }, - "flow_title": "Dispositivo BleBox: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/bond/translations/it.json b/homeassistant/components/bond/translations/it.json index e22ad82e1fd..b6c628abf63 100644 --- a/homeassistant/components/bond/translations/it.json +++ b/homeassistant/components/bond/translations/it.json @@ -9,7 +9,7 @@ "old_firmware": "Firmware precedente non supportato sul dispositivo Bond - si prega di aggiornare prima di continuare", "unknown": "Errore imprevisto" }, - "flow_title": "Bond: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/ca.json b/homeassistant/components/bosch_shc/translations/ca.json new file mode 100644 index 00000000000..6db49cf7103 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/ca.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "pairing_failed": "La vinculaci\u00f3 ha fallat; comprova que el controlador Bosch Smart Home est\u00e0 en mode vinculaci\u00f3 (LED parpellejant) i que la contrasenya \u00e9s correcta.", + "session_error": "Error de sessi\u00f3: l'API ha retornat No OK.", + "unknown": "Error inesperat" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Prem el bot\u00f3 de la part frontal/lateral del controlador Bosh Smart Home fins que el LED parpellegi.\nPreparat per continuar la configuraci\u00f3 de {model} @ {host} amb Home Assistant?" + }, + "credentials": { + "data": { + "password": "Contrasenya de Smart Home Controller" + } + }, + "reauth_confirm": { + "description": "La integraci\u00f3 bosch_shc ha de tornar a autenticar el teu compte", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Configura Bosch Smart Controller per poder controlar i monitoritzar-lo des de Home Assistant.", + "title": "Par\u00e0metres d'autenticaci\u00f3 SHC" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/et.json b/homeassistant/components/bosch_shc/translations/et.json new file mode 100644 index 00000000000..cb095bb9de7 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/et.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamise viga", + "pairing_failed": "Sidumine nurjus; palun kontrolli kas Bosch Smart Home Controller on sidumisre\u017eiimis (LED vilgub) ning kas salas\u00f5na on \u00f5ige.", + "session_error": "Seansi viga: API tagastas veateate.", + "unknown": "Tundmatu viga" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Vajuta palun Bosch Smart Home Controlleri esik\u00fclje nuppu kuni LED hakkab vilkuma.\n Kas oled valmis j\u00e4tkama {model} @ {host} seadistamist Home Assistanti abil?" + }, + "credentials": { + "data": { + "password": "Smart Home kontrolleri salas\u00f5na" + } + }, + "reauth_confirm": { + "description": "Bosch_shc sidumine peab konto uuesti autentima.", + "title": "Taastuvastamine" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Seadista oma Bosch Smart Home Controller, et v\u00f5imaldada j\u00e4lgimist ja juhtimist Home Assistantiga.", + "title": "SHC autentimisparameetrid" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/it.json b/homeassistant/components/bosch_shc/translations/it.json new file mode 100644 index 00000000000..d6e971cb079 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/it.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "pairing_failed": "Associazione fallita; verificare che il controller Bosch Smart Home sia in modalit\u00e0 di associazione (LED lampeggiante) e che la password sia corretta.", + "session_error": "Errore di sessione: l'API restituisce il risultato Non-OK.", + "unknown": "Errore imprevisto" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Premere il pulsante sul lato anteriore del controller Bosch Smart Home finch\u00e9 il LED non inizia a lampeggiare.\nPronto per continuare a configurare {model} @ {host} con Home Assistant?" + }, + "credentials": { + "data": { + "password": "Password del controller Smart Home" + } + }, + "reauth_confirm": { + "description": "L'integrazione bosch_shc deve autenticare nuovamente il tuo account", + "title": "Autenticare nuovamente l'integrazione" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Configura il tuo Bosch Smart Home Controller per consentire il monitoraggio e il controllo con Home Assistant.", + "title": "Parametri di autenticazione SHC" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/nl.json b/homeassistant/components/bosch_shc/translations/nl.json new file mode 100644 index 00000000000..8a6cbd6cfdd --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/nl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "pairing_failed": "Koppelen mislukt; Controleer of de Bosch Smart Home Controller zich in de koppelingsmodus bevindt (LED knippert) en of uw wachtwoord correct is.", + "session_error": "Sessiefout: API retourneert niet-OK resultaat.", + "unknown": "Onverwachte fout" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Druk op de knop aan de voorzijde van de Bosch Smart Home Controller totdat de LED begint te knipperen.\nKlaar om verder te gaan met het instellen van {model} @ {host} met Home Assistant?" + }, + "credentials": { + "data": { + "password": "Wachtwoord van de Smart Home Controller" + } + }, + "reauth_confirm": { + "description": "De bosch_shc integratie moet uw account herauthenticeren", + "title": "Verifieer de integratie opnieuw" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Stel uw Bosch Smart Home Controller in om monitoring en bediening met Home Assistant mogelijk te maken.", + "title": "SHC-authenticatieparameters" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/ru.json b/homeassistant/components/bosch_shc/translations/ru.json new file mode 100644 index 00000000000..ebbfd46812c --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/ru.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "pairing_failed": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 Bosch Smart Home Controller \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f (\u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u043d\u044b\u0439 \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0438\u0433\u0430\u0435\u0442) \u0438 \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439.", + "session_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u0435\u0430\u043d\u0441\u0430: API \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 Non-OK.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "\u0423\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u043d\u0435\u0439 \u043f\u0430\u043d\u0435\u043b\u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430, \u043f\u043e\u043a\u0430 \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u043d\u044b\u0435 \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u043d\u0435 \u043d\u0430\u0447\u043d\u0443\u0442 \u043c\u0438\u0433\u0430\u0442\u044c.\n\u0413\u043e\u0442\u043e\u0432\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 {model} @ {host}?" + }, + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c Smart Home Controller" + } + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Bosch SHC", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Bosch Smart Home Controller.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 SHC" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/zh-Hant.json b/homeassistant/components/bosch_shc/translations/zh-Hant.json new file mode 100644 index 00000000000..fd667210d3e --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/zh-Hant.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "pairing_failed": "\u914d\u5c0d\u5931\u6557\uff1a\u8acb\u78ba\u8a8d Bosch Smart Home Controller \u5df2\u7d93\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\uff08LED \u9583\u720d\uff09\u3001\u4e26\u4e14\u5bc6\u78bc\u8f38\u5165\u6b63\u78ba\u3002", + "session_error": "Session \u932f\u8aa4\uff1aAPI \u56de\u8986\u975e\u6b63\u5e38\u7d50\u679c\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "\u8acb\u6309\u4e0b Bosch Smart Home Controller \u524d\u65b9\u7684\u6309\u9215\u76f4\u5230 LED \u9583\u720d\u3002\n\u662f\u5426\u8981\u7e7c\u7e8c\u8a2d\u5b9a\u4f4d\u65bc {host} \u7684 {model} \u9023\u63a5\u81f3 Home Assistant\uff1f" + }, + "credentials": { + "data": { + "password": "Smart Home Controller \u5bc6\u78bc" + } + }, + "reauth_confirm": { + "description": "bosch_shc \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8a2d\u5b9a Bosch Smart Home Controller \u4ee5\u5141\u8a31 Home Assistant \u9032\u884c\u76e3\u63a7\u3002", + "title": "SHC \u8a8d\u8b49\u53c3\u6578" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/it.json b/homeassistant/components/brother/translations/it.json index 06decb26173..0d4af015524 100644 --- a/homeassistant/components/brother/translations/it.json +++ b/homeassistant/components/brother/translations/it.json @@ -9,7 +9,7 @@ "snmp_error": "Server SNMP spento o stampante non supportata.", "wrong_host": "Nome host o indirizzo IP non valido." }, - "flow_title": "Stampante Brother: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/it.json b/homeassistant/components/bsblan/translations/it.json index 3eb7feec614..fa7874630b0 100644 --- a/homeassistant/components/bsblan/translations/it.json +++ b/homeassistant/components/bsblan/translations/it.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/calendar/translations/ru.json b/homeassistant/components/calendar/translations/ru.json index 0a95a70ae06..81cf8250c21 100644 --- a/homeassistant/components/calendar/translations/ru.json +++ b/homeassistant/components/calendar/translations/ru.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044c" diff --git a/homeassistant/components/canary/translations/it.json b/homeassistant/components/canary/translations/it.json index b29a758acaa..8a29451bb44 100644 --- a/homeassistant/components/canary/translations/it.json +++ b/homeassistant/components/canary/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/cast/translations/et.json b/homeassistant/components/cast/translations/et.json index 427be338fea..6fb37f802e0 100644 --- a/homeassistant/components/cast/translations/et.json +++ b/homeassistant/components/cast/translations/et.json @@ -29,7 +29,7 @@ "ignore_cec": "Eira CEC-i", "uuid": "Lubatud UUID-d" }, - "description": "Lubatud UUID-d - komadega eraldatud loetelu UUID-dest, mida soovitakse lisada Home Assistant'ile. Kasuta ainult siis, kui ei soovi lisada k\u00f5iki olemasolevaid Cast seadmeid.\nIgnore CEC - komadega eraldatud loetelu Chromecastidest, mis peaksid aktiivse sisendi m\u00e4\u00e4ramisel CEC-andmeid ignoreerima. See edastatakse pychromecast.IGNORE_CEC.", + "description": "Lubatud UUID-d - komadega eraldatud loetelu UUID-dest, mida soovitakse lisada Home Assistant'ile. Kasuta ainult siis,kui ei soovi lisada k\u00f5iki olemasolevaid Cast seadmeid.\nIgnore CEC - komadega eraldatud loetelu Chromecastidest, mis peaksid aktiivse sisendi m\u00e4\u00e4ramisel CEC-andmeid ignoreerima. See edastatakse pychromecast.IGNORE_CEC.", "title": "Google Casti seadistamise t\u00e4psemad valikud" }, "basic_options": { diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json index 7d3def31eb4..1994465c410 100644 --- a/homeassistant/components/cast/translations/zh-Hant.json +++ b/homeassistant/components/cast/translations/zh-Hant.json @@ -29,7 +29,7 @@ "ignore_cec": "\u5ffd\u7565 CEC", "uuid": "\u5df2\u5141\u8a31 UUID" }, - "description": "\u5df2\u5141\u8a31 UUID - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e UUID \u5217\u8868\u4ee5\u65b0\u589e\u81f3 Home Assistant\u3002\u50c5\u65bc\u4e0d\u60f3\u5168\u90e8\u65b0\u589e\u6642\u4f7f\u7528\u3002\n\u5ffd\u7565 CEC - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u5217\u8868\u3001\u5ffd\u7565\u5176 CEC \u63a7\u5236\u4ee5\u907f\u514d\u555f\u52d5\u8f38\u5165\u4f86\u6e90\u3002\u8cc7\u6599\u5c07\u6703\u50b3\u905e\u81f3 pychromecast.IGNORE_CEC\u3002", + "description": "\u5df2\u5141\u8a31 UUID - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e UUID \u5217\u8868\u4ee5\u65b0\u589e\u81f3 Home Assistant\u3002\u50c5\u65bc\u4e0d\u60f3\u5c07\u5168\u90e8 Cast \u88dd\u7f6e\u65b0\u589e\u6642\u4f7f\u7528\u3002\n\u5ffd\u7565 CEC - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u5217\u8868\u3001\u5ffd\u7565\u5176 CEC \u63a7\u5236\u4ee5\u907f\u514d\u555f\u52d5\u8f38\u5165\u4f86\u6e90\u3002\u8cc7\u6599\u5c07\u6703\u50b3\u905e\u81f3 pychromecast.IGNORE_CEC\u3002", "title": "Google Cast \u9032\u968e\u8a2d\u5b9a" }, "basic_options": { diff --git a/homeassistant/components/cloudflare/translations/it.json b/homeassistant/components/cloudflare/translations/it.json index 48d9acc0861..df5c045dd99 100644 --- a/homeassistant/components/cloudflare/translations/it.json +++ b/homeassistant/components/cloudflare/translations/it.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticazione non valida", "invalid_zone": "Zona non valida" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index cb445ac4f76..09f93e35911 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -11,7 +11,7 @@ "error": { "no_key": "Impossibile ottenere una API key" }, - "flow_title": "Gateway Zigbee deCONZ ({host})", + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo: {addon}?", diff --git a/homeassistant/components/denonavr/translations/it.json b/homeassistant/components/denonavr/translations/it.json index 6f671438777..86e5bb309b9 100644 --- a/homeassistant/components/denonavr/translations/it.json +++ b/homeassistant/components/denonavr/translations/it.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Impossibile rilevare un ricevitore di rete Denon AVR" }, - "flow_title": "Ricevitore di rete Denon AVR: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Si prega di confermare l'aggiunta del ricevitore", diff --git a/homeassistant/components/directv/translations/it.json b/homeassistant/components/directv/translations/it.json index 2b6e25d63ef..2f8e5f29943 100644 --- a/homeassistant/components/directv/translations/it.json +++ b/homeassistant/components/directv/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Vuoi impostare {name} ?" diff --git a/homeassistant/components/doorbird/translations/it.json b/homeassistant/components/doorbird/translations/it.json index 51b45cb79bb..99d721fe491 100644 --- a/homeassistant/components/doorbird/translations/it.json +++ b/homeassistant/components/doorbird/translations/it.json @@ -10,7 +10,7 @@ "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/it.json b/homeassistant/components/elgato/translations/it.json index b23a0aa9392..52c718715b3 100644 --- a/homeassistant/components/elgato/translations/it.json +++ b/homeassistant/components/elgato/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "Elgato Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/emonitor/translations/it.json b/homeassistant/components/emonitor/translations/it.json index 7a194a301a5..36115111583 100644 --- a/homeassistant/components/emonitor/translations/it.json +++ b/homeassistant/components/emonitor/translations/it.json @@ -7,7 +7,7 @@ "cannot_connect": "Impossibile connettersi", "unknown": "Errore imprevisto" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Vuoi impostare {name} ({host})?", diff --git a/homeassistant/components/enphase_envoy/translations/it.json b/homeassistant/components/enphase_envoy/translations/it.json index 2f0e1edc845..8fd9ab9ce25 100644 --- a/homeassistant/components/enphase_envoy/translations/it.json +++ b/homeassistant/components/enphase_envoy/translations/it.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index c21d8da7166..34d1ec78f6e 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticazione non valida", "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se questo errore persiste, impostare un indirizzo IP statico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/fan/translations/ru.json b/homeassistant/components/fan/translations/ru.json index 320b4e280c5..bc2fd221736 100644 --- a/homeassistant/components/fan/translations/ru.json +++ b/homeassistant/components/fan/translations/ru.json @@ -15,8 +15,8 @@ }, "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u0412\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440" diff --git a/homeassistant/components/forked_daapd/translations/it.json b/homeassistant/components/forked_daapd/translations/it.json index d4303f6dba6..f33f5b7cc2b 100644 --- a/homeassistant/components/forked_daapd/translations/it.json +++ b/homeassistant/components/forked_daapd/translations/it.json @@ -12,7 +12,7 @@ "wrong_password": "Password errata", "wrong_server_type": "L'integrazione forked-daapd richiede un server forked-daapd con versione >= 27.0." }, - "flow_title": "server forked-daapd: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json index bfd095d4dc0..10926ed8348 100644 --- a/homeassistant/components/fritz/translations/ca.json +++ b/homeassistant/components/fritz/translations/ca.json @@ -8,6 +8,7 @@ "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3", "connection_error": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, @@ -38,6 +39,16 @@ }, "description": "Configura FRITZ!Box Tools per poder controlar FRITZ!Box.\nEl m\u00ednim necessari \u00e9s: nom d'usuari i contrasenya.", "title": "Configuraci\u00f3 de FRITZ!Box Tools - obligatori" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "description": "Configura FRITZ!Box Tools per poder controlar FRITZ!Box.\nEl m\u00ednim necessari \u00e9s: nom d'usuari i contrasenya.", + "title": "Configuraci\u00f3 de FRITZ!Box Tools" } } } diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json index bb72dde74b8..5914932125c 100644 --- a/homeassistant/components/fritz/translations/et.json +++ b/homeassistant/components/fritz/translations/et.json @@ -8,6 +8,7 @@ "error": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4ivitatud", + "cannot_connect": "\u00dchendamine nurjus", "connection_error": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus" }, @@ -38,6 +39,16 @@ }, "description": "Seadista FRITZ!Boxi t\u00f6\u00f6riistad oma FRITZ!Boxi juhtimiseks.\n Minimaalselt vaja: kasutajanimi ja salas\u00f5na.", "title": "FRITZ! Boxi t\u00f6\u00f6riistade seadistamine - kohustuslik" + }, + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5ma", + "port": "Port", + "username": "Kasutajanimi" + }, + "description": "Seadista FRITZ!Box Tools oma FRITZ!Boxi juhtimiseks.\n Minimaalselt vajalik: kasutajanimi ja salas\u00f5na.", + "title": "Seadista FRITZ! Box Tools" } } } diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json index 257198cf684..f39b8bc7a7f 100644 --- a/homeassistant/components/fritz/translations/it.json +++ b/homeassistant/components/fritz/translations/it.json @@ -8,10 +8,11 @@ "error": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi", "connection_error": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, - "flow_title": "Strumenti FRITZ! Box: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { @@ -38,6 +39,16 @@ }, "description": "Configura gli strumenti FRITZ!Box per controllare il tuo FRITZ!Box.\n Minimo necessario: nome utente, password.", "title": "Configurazione degli strumenti FRITZ!Box - obbligatorio" + }, + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "description": "Configura gli strumenti FRITZ!Box per controllare il tuo FRITZ! Box.\nMinimo necessario: nome utente, password.", + "title": "Configura gli strumenti del FRITZ!Box" } } } diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json index 54c6e758f33..904dc6629b9 100644 --- a/homeassistant/components/fritz/translations/nl.json +++ b/homeassistant/components/fritz/translations/nl.json @@ -8,6 +8,7 @@ "error": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", + "cannot_connect": "Kan geen verbinding maken", "connection_error": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, @@ -38,6 +39,16 @@ }, "description": "Stel FRITZ!Box Tools in om uw FRITZ!Box te bedienen.\nMinimaal nodig: gebruikersnaam, wachtwoord.", "title": "Configureer FRITZ! Box Tools - verplicht" + }, + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "description": "Stel FRITZ!Box Tools in om uw FRITZ!Box te bedienen.\nMinimaal nodig: gebruikersnaam, wachtwoord.", + "title": "Setup FRITZ!Box Tools" } } } diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json index d6921c900e6..52fe427f822 100644 --- a/homeassistant/components/fritz/translations/ru.json +++ b/homeassistant/components/fritz/translations/ru.json @@ -8,6 +8,7 @@ "error": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, @@ -38,6 +39,16 @@ }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 FRITZ!Box Tools \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0412\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c FRITZ!Box.\n\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", "title": "FRITZ!Box Tools" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 FRITZ!Box Tools \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0412\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c FRITZ!Box.\n\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "title": "FRITZ!Box Tools" } } } diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json index 370894a5e00..900cbd7df59 100644 --- a/homeassistant/components/fritz/translations/zh-Hant.json +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -8,6 +8,7 @@ "error": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "connection_error": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, @@ -38,6 +39,16 @@ }, "description": "\u8a2d\u5b9a FRITZ!Box Tools \u4ee5\u63a7\u5236 FRITZ!Box\u3002\n\u9700\u8981\u8f38\u5165\uff1a\u4f7f\u7528\u8005\u540d\u7a31\u3001\u5bc6\u78bc\u3002", "title": "\u8a2d\u5b9a FRITZ!Box Tools - \u5f37\u5236" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a FRITZ!Box Tools \u4ee5\u63a7\u5236 FRITZ!Box\u3002\n\u9700\u8981\u8f38\u5165\uff1a\u4f7f\u7528\u8005\u540d\u7a31\u3001\u5bc6\u78bc\u3002", + "title": "\u8a2d\u5b9a FRITZ!Box Tools" } } } diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json index da01b34ad02..bf94dd476a1 100644 --- a/homeassistant/components/fritzbox/translations/it.json +++ b/homeassistant/components/fritzbox/translations/it.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Autenticazione non valida" }, - "flow_title": "AVM FRITZ! SmartHome: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/it.json b/homeassistant/components/fritzbox_callmonitor/translations/it.json index 5696bf86fd1..154f57aee52 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/it.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/it.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Autenticazione non valida" }, - "flow_title": "Monitoraggio chiamate FRITZ! Box AVM: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/garages_amsterdam/translations/ca.json b/homeassistant/components/garages_amsterdam/translations/ca.json new file mode 100644 index 00000000000..328054bafdf --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "garage_name": "Nom del garatge" + }, + "title": "Tria un garatge a controlar" + } + } + }, + "title": "Garatges Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/et.json b/homeassistant/components/garages_amsterdam/translations/et.json new file mode 100644 index 00000000000..027743da89d --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Tundmatu viga" + }, + "step": { + "user": { + "data": { + "garage_name": "Garaa\u017ei nimi" + }, + "title": "Vali j\u00e4lgitav garaa\u017e" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/it.json b/homeassistant/components/garages_amsterdam/translations/it.json new file mode 100644 index 00000000000..4124c1c07a5 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "garage_name": "Nome del garage" + }, + "title": "Scegli un garage da monitorare" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/nl.json b/homeassistant/components/garages_amsterdam/translations/nl.json new file mode 100644 index 00000000000..c4c4c9ed86c --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "garage_name": "Garage naam" + }, + "title": "Kies een garage om te monitoren" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/ru.json b/homeassistant/components/garages_amsterdam/translations/ru.json new file mode 100644 index 00000000000..c4629647d05 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "garage_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u0430\u0440\u0430\u0436\u0430" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0433\u0430\u0440\u0430\u0436 \u0434\u043b\u044f \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u044f" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/zh-Hant.json b/homeassistant/components/garages_amsterdam/translations/zh-Hant.json new file mode 100644 index 00000000000..1c039792da1 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "garage_name": "\u8eca\u5eab\u540d\u7a31" + }, + "title": "\u9078\u64c7\u6240\u8981\u76e3\u8996\u7684\u8eca\u5eab" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json index 5fb9643d097..cc7dbb0ab85 100644 --- a/homeassistant/components/goalzero/translations/ca.json +++ b/homeassistant/components/goalzero/translations/ca.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", + "unknown": "Error inesperat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +11,9 @@ "unknown": "Error inesperat" }, "step": { + "confirm_discovery": { + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/goalzero/translations/et.json b/homeassistant/components/goalzero/translations/et.json index 5bc1af97297..5d0111aa946 100644 --- a/homeassistant/components/goalzero/translations/et.json +++ b/homeassistant/components/goalzero/translations/et.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Konto on juba seadistatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "invalid_host": "Tundmatu host", + "unknown": "Tundmatu viga" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -9,6 +11,10 @@ "unknown": "Tundmatu viga" }, "step": { + "confirm_discovery": { + "description": "Soovitatav on DHCP aadressi reserveerimine ruuteris. Kui seda pole seadistatud, v\u00f5ib seade osutuda k\u00e4ttesaamatuks kuni Home Assistant tuvastab uue IP-aadressi. Vaata ruuteri kasutusjuhendit.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/goalzero/translations/it.json b/homeassistant/components/goalzero/translations/it.json index 98b682dd76b..541cf7e01e1 100644 --- a/homeassistant/components/goalzero/translations/it.json +++ b/homeassistant/components/goalzero/translations/it.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "invalid_host": "Nome host o indirizzo IP non valido", + "unknown": "Errore imprevisto" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -9,12 +11,16 @@ "unknown": "Errore imprevisto" }, "step": { + "confirm_discovery": { + "description": "Si consiglia la prenotazione DHCP sul router. Se non configurato, il dispositivo potrebbe non essere disponibile fino a quando Home Assistant non rileva il nuovo indirizzo IP. Fare riferimento al manuale utente del router.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Host", "name": "Nome" }, - "description": "Innanzitutto, devi scaricare l'app Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegui le istruzioni per connettere il tuo Yeti alla tua rete Wifi. La prenotazione DHCP per il dispositivo deve essere configurata nelle impostazioni del router per assicurarsi che l'IP host non cambi. Fare riferimento al manuale utente del router.", + "description": "Innanzitutto, devi scaricare l'app Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegui le istruzioni per connettere il tuo Yeti alla tua rete Wi-Fi. Si consiglia la prenotazione DHCP sul router. Se non configurato, il dispositivo potrebbe non essere disponibile fino a quando Home Assistant non rileva il nuovo indirizzo IP. Fare riferimento al manuale utente del router.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/nl.json b/homeassistant/components/goalzero/translations/nl.json index c84ef7adb1f..4f902f8bea2 100644 --- a/homeassistant/components/goalzero/translations/nl.json +++ b/homeassistant/components/goalzero/translations/nl.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "invalid_host": "Ongeldige hostnaam of IP-adres", + "unknown": "Onverwachte fout" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +11,10 @@ "unknown": "Onverwachte fout" }, "step": { + "confirm_discovery": { + "description": "DHCP-reservering op uw router wordt aanbevolen. Als dit niet het geval is, is het apparaat mogelijk niet meer beschikbaar totdat Home Assistant het nieuwe IP-adres detecteert. Raadpleeg de gebruikershandleiding van uw router.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/goalzero/translations/ru.json b/homeassistant/components/goalzero/translations/ru.json index 066c93545d6..52d8bbcbdc7 100644 --- a/homeassistant/components/goalzero/translations/ru.json +++ b/homeassistant/components/goalzero/translations/ru.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\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.", @@ -9,12 +11,16 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "confirm_discovery": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u0438\u043c\u0438, \u0447\u0442\u043e\u0431\u044b IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u043b\u0441\u044f \u0441\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043c\u043e\u0436\u0435\u0442 \u0441\u0442\u0430\u0442\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c \u0434\u043e \u0442\u0435\u0445 \u043f\u043e\u0440, \u043f\u043e\u043a\u0430 Home Assistant \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442 \u043d\u043e\u0432\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u043e\u0443\u0442\u0435\u0440\u0430.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0441\u043a\u0430\u0447\u0430\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Goal Zero: https://www.goalzero.com/product-features/yeti-app/.\n\n\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c \u043f\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044e Yeti \u043a \u0441\u0435\u0442\u0438 WiFi. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u0438\u043c\u0438, \u0447\u0442\u043e\u0431\u044b IP \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u043b\u0441\u044f \u0441\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u044d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430.", + "description": "\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0441\u043a\u0430\u0447\u0430\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Goal Zero: https://www.goalzero.com/product-features/yeti-app/.\n\n\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c \u043f\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044e Yeti \u043a \u0441\u0435\u0442\u0438 WiFi. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u0438\u043c\u0438, \u0447\u0442\u043e\u0431\u044b IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u043b\u0441\u044f \u0441\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043c\u043e\u0436\u0435\u0442 \u0441\u0442\u0430\u0442\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c \u0434\u043e \u0442\u0435\u0445 \u043f\u043e\u0440, \u043f\u043e\u043a\u0430 Home Assistant \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442 \u043d\u043e\u0432\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u044d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/zh-Hant.json b/homeassistant/components/goalzero/translations/zh-Hant.json index 5560def5eb1..13c49f8d2ac 100644 --- a/homeassistant/components/goalzero/translations/zh-Hant.json +++ b/homeassistant/components/goalzero/translations/zh-Hant.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,12 +11,16 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "confirm_discovery": { + "description": "\u5efa\u8b70\u65bc\u8def\u7531\u5668\u7684 DHCP \u8a2d\u5b9a\u4e2d\u4fdd\u7559\u56fa\u5b9a IP\uff0c\u5047\u5982\u672a\u8a2d\u5b9a\u3001\u88dd\u7f6e\u53ef\u80fd\u6703\u5728 Home Assistant \u5075\u6e2c\u5230\u65b0 IP \u4e4b\u524d\u8b8a\u6210\u7121\u6cd5\u4f7f\u7528\u3002\u8acb\u53c3\u95b1\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a\u3002", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, - "description": "\u9996\u5148\u5fc5\u9808\u5148\u4e0b\u8f09 Goal Zero app\uff1ahttps://www.goalzero.com/product-features/yeti-app/\n\n\u8ddf\u96a8\u6307\u793a\u5c07 Yeti \u9023\u7dda\u81f3\u7121\u7dda\u7db2\u8def\u3002\u5fc5\u9808\u65bc\u8def\u7531\u5668\u5167\u8a2d\u5b9a\u88dd\u7f6e\u7684 DHCP \u56fa\u5b9a IP\u3001\u4ee5\u78ba\u4fdd\u4e3b\u6a5f\u7aef IP \u4e0d\u81f3\u65bc\u6539\u8b8a\u3002\u8acb\u53c3\u8003\u60a8\u7684\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u64cd\u4f5c\u3002", + "description": "\u9996\u5148\u5fc5\u9808\u5148\u4e0b\u8f09 Goal Zero app\uff1ahttps://www.goalzero.com/product-features/yeti-app/\n\n\u8ddf\u96a8\u6307\u793a\u5c07 Yeti \u9023\u7dda\u81f3\u7121\u7dda\u7db2\u8def\u3002\u5efa\u8b70\u65bc\u8def\u7531\u5668\u7684 DHCP \u8a2d\u5b9a\u4e2d\u4fdd\u7559\u56fa\u5b9a IP\uff0c\u5047\u5982\u672a\u8a2d\u5b9a\u3001\u88dd\u7f6e\u53ef\u80fd\u6703\u5728 Home Assistant \u5075\u6e2c\u5230\u65b0 IP \u4e4b\u524d\u8b8a\u6210\u7121\u6cd5\u4f7f\u7528\u3002\u8acb\u53c3\u95b1\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a\u3002", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/gogogate2/translations/it.json b/homeassistant/components/gogogate2/translations/it.json index 7b1dbe4e3e4..71510b90040 100644 --- a/homeassistant/components/gogogate2/translations/it.json +++ b/homeassistant/components/gogogate2/translations/it.json @@ -7,6 +7,7 @@ "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { @@ -15,7 +16,7 @@ "username": "Nome utente" }, "description": "Fornire le informazioni richieste di seguito.", - "title": "Configurazione di GogoGate2 o iSmartGate" + "title": "Imposta Gogogate2 o ismartgate" } } } diff --git a/homeassistant/components/group/translations/ru.json b/homeassistant/components/group/translations/ru.json index 7103b9f75d0..7e8ab4d8be1 100644 --- a/homeassistant/components/group/translations/ru.json +++ b/homeassistant/components/group/translations/ru.json @@ -5,9 +5,9 @@ "home": "\u0414\u043e\u043c\u0430", "locked": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e", "not_home": "\u041d\u0435 \u0434\u043e\u043c\u0430", - "off": "\u0412\u044b\u043a\u043b", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "ok": "\u041e\u041a", - "on": "\u0412\u043a\u043b", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", "problem": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430", "unlocked": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e" diff --git a/homeassistant/components/growatt_server/translations/ca.json b/homeassistant/components/growatt_server/translations/ca.json index 19c0eecdafb..0c1e1b6cb83 100644 --- a/homeassistant/components/growatt_server/translations/ca.json +++ b/homeassistant/components/growatt_server/translations/ca.json @@ -16,7 +16,7 @@ "user": { "data": { "name": "Nom", - "password": "Nom", + "password": "Contrasenya", "username": "Nom d'usuari" }, "title": "Introdueix la teva informaci\u00f3 de Growatt" diff --git a/homeassistant/components/growatt_server/translations/et.json b/homeassistant/components/growatt_server/translations/et.json index c371234e6da..3115713bc68 100644 --- a/homeassistant/components/growatt_server/translations/et.json +++ b/homeassistant/components/growatt_server/translations/et.json @@ -16,7 +16,7 @@ "user": { "data": { "name": "Nimi", - "password": "Nimi", + "password": "Salas\u00f5na", "username": "Kasutajanimi" }, "title": "Sisesta oma Growatti teave" diff --git a/homeassistant/components/growatt_server/translations/it.json b/homeassistant/components/growatt_server/translations/it.json index 7676ecfff3c..19862f82d83 100644 --- a/homeassistant/components/growatt_server/translations/it.json +++ b/homeassistant/components/growatt_server/translations/it.json @@ -16,7 +16,7 @@ "user": { "data": { "name": "Nome", - "password": "Nome", + "password": "Password", "username": "Utente" }, "title": "Inserisci le tue informazioni Growatt" diff --git a/homeassistant/components/growatt_server/translations/nl.json b/homeassistant/components/growatt_server/translations/nl.json index b4e27643cdb..86b5a98b131 100644 --- a/homeassistant/components/growatt_server/translations/nl.json +++ b/homeassistant/components/growatt_server/translations/nl.json @@ -16,7 +16,7 @@ "user": { "data": { "name": "Naam", - "password": "Naam", + "password": "Wachtwoord", "username": "Gebruikersnaam" }, "title": "Vul uw Growatt gegevens in" diff --git a/homeassistant/components/growatt_server/translations/ru.json b/homeassistant/components/growatt_server/translations/ru.json index 8db89da175b..02557515f97 100644 --- a/homeassistant/components/growatt_server/translations/ru.json +++ b/homeassistant/components/growatt_server/translations/ru.json @@ -16,7 +16,7 @@ "user": { "data": { "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "password": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Growatt." diff --git a/homeassistant/components/growatt_server/translations/zh-Hant.json b/homeassistant/components/growatt_server/translations/zh-Hant.json index 47efaddf3fa..4d00b4e8066 100644 --- a/homeassistant/components/growatt_server/translations/zh-Hant.json +++ b/homeassistant/components/growatt_server/translations/zh-Hant.json @@ -16,7 +16,7 @@ "user": { "data": { "name": "\u540d\u7a31", - "password": "\u540d\u7a31", + "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "title": "\u8f38\u5165 Growatt \u8cc7\u8a0a" diff --git a/homeassistant/components/guardian/translations/it.json b/homeassistant/components/guardian/translations/it.json index 5cd20f78a3f..0f3aacf43bf 100644 --- a/homeassistant/components/guardian/translations/it.json +++ b/homeassistant/components/guardian/translations/it.json @@ -6,6 +6,9 @@ "cannot_connect": "Impossibile connettersi" }, "step": { + "discovery_confirm": { + "description": "Vuoi configurare questo dispositivo Guardian?" + }, "user": { "data": { "ip_address": "Indirizzo IP", diff --git a/homeassistant/components/harmony/translations/it.json b/homeassistant/components/harmony/translations/it.json index 3bc46a83b3a..1edaa9edc15 100644 --- a/homeassistant/components/harmony/translations/it.json +++ b/homeassistant/components/harmony/translations/it.json @@ -7,7 +7,7 @@ "cannot_connect": "Impossibile connettersi", "unknown": "Errore imprevisto" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "Vuoi impostare {name} ({host})?", diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index 38a3bfccd10..f42a92a8c1a 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -17,7 +17,7 @@ "unable_to_pair": "Impossibile abbinare, per favore riprova.", "unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito." }, - "flow_title": "{name} tramite il Protocollo degli Accessori HomeKit", + "flow_title": "{name}", "step": { "busy_error": { "description": "Interrompere l'associazione su tutti i controller o provare a riavviare il dispositivo, quindi continuare a riprendere l'associazione.", diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json index 545d3b35daf..df2c6fd441b 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -15,7 +15,7 @@ "response_error": "Errore sconosciuto dal dispositivo", "unknown": "Errore imprevisto" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/humidifier/translations/ru.json b/homeassistant/components/humidifier/translations/ru.json index 7845a3d36a3..85a311857fd 100644 --- a/homeassistant/components/humidifier/translations/ru.json +++ b/homeassistant/components/humidifier/translations/ru.json @@ -20,8 +20,8 @@ }, "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u0423\u0432\u043b\u0430\u0436\u043d\u0438\u0442\u0435\u043b\u044c" diff --git a/homeassistant/components/input_boolean/translations/ru.json b/homeassistant/components/input_boolean/translations/ru.json index 2aa6a382570..d6762e9d237 100644 --- a/homeassistant/components/input_boolean/translations/ru.json +++ b/homeassistant/components/input_boolean/translations/ru.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c" diff --git a/homeassistant/components/ipp/translations/it.json b/homeassistant/components/ipp/translations/it.json index 7e3d5a853ad..32dfb7fab19 100644 --- a/homeassistant/components/ipp/translations/it.json +++ b/homeassistant/components/ipp/translations/it.json @@ -13,7 +13,7 @@ "cannot_connect": "Impossibile connettersi", "connection_upgrade": "Impossibile connettersi alla stampante. Riprovare selezionando l'opzione SSL/TLS." }, - "flow_title": "Stampante: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/it.json b/homeassistant/components/isy994/translations/it.json index 77d9f8206bd..448ecf6d570 100644 --- a/homeassistant/components/isy994/translations/it.json +++ b/homeassistant/components/isy994/translations/it.json @@ -9,7 +9,7 @@ "invalid_host": "La voce host non era nel formato URL completo, ad esempio http://192.168.10.100:80", "unknown": "Errore imprevisto" }, - "flow_title": "Universal Devices ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/kodi/translations/it.json b/homeassistant/components/kodi/translations/it.json index cadec0d387e..d7b67e3c2b8 100644 --- a/homeassistant/components/kodi/translations/it.json +++ b/homeassistant/components/kodi/translations/it.json @@ -12,7 +12,7 @@ "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/kraken/translations/ca.json b/homeassistant/components/kraken/translations/ca.json new file mode 100644 index 00000000000..d5c59d7b11c --- /dev/null +++ b/homeassistant/components/kraken/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval d'actualitzaci\u00f3", + "tracked_asset_pairs": "Parells de recursos en seguiment" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/et.json b/homeassistant/components/kraken/translations/et.json new file mode 100644 index 00000000000..74693aa9525 --- /dev/null +++ b/homeassistant/components/kraken/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Lubatud on ainult \u00fcks sidumine" + }, + "step": { + "user": { + "description": "Kinnita s\u00e4tted" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "V\u00e4rskendamise intervall", + "tracked_asset_pairs": "J\u00e4lgitavad paarid" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/it.json b/homeassistant/components/kraken/translations/it.json new file mode 100644 index 00000000000..a436844fb75 --- /dev/null +++ b/homeassistant/components/kraken/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "user": { + "description": "Vuoi iniziare la configurazione?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervallo di aggiornamento", + "tracked_asset_pairs": "Coppie di attivit\u00e0 monitorate" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/nl.json b/homeassistant/components/kraken/translations/nl.json new file mode 100644 index 00000000000..8e63d5b5373 --- /dev/null +++ b/homeassistant/components/kraken/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wil je beginnen met instellen?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update-interval" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/ru.json b/homeassistant/components/kraken/translations/ru.json new file mode 100644 index 00000000000..ba46597b5aa --- /dev/null +++ b/homeassistant/components/kraken/translations/ru.json @@ -0,0 +1,22 @@ +{ + "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. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f", + "tracked_asset_pairs": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u0430\u0440\u044b \u0430\u043a\u0442\u0438\u0432\u043e\u0432" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/zh-Hant.json b/homeassistant/components/kraken/translations/zh-Hant.json new file mode 100644 index 00000000000..8d64c026579 --- /dev/null +++ b/homeassistant/components/kraken/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387", + "tracked_asset_pairs": "\u5df2\u8ffd\u8e64\u8a2d\u5099" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/ru.json b/homeassistant/components/light/translations/ru.json index 3237a2812c1..523c2a9c91e 100644 --- a/homeassistant/components/light/translations/ru.json +++ b/homeassistant/components/light/translations/ru.json @@ -19,8 +19,8 @@ }, "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u041e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435" diff --git a/homeassistant/components/lutron_caseta/translations/it.json b/homeassistant/components/lutron_caseta/translations/it.json index d1b3b754812..4d895ad8438 100644 --- a/homeassistant/components/lutron_caseta/translations/it.json +++ b/homeassistant/components/lutron_caseta/translations/it.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "Impossibile impostare il bridge (host: {host}) importato da configuration.yaml.", diff --git a/homeassistant/components/media_player/translations/ru.json b/homeassistant/components/media_player/translations/ru.json index df0b00d2482..6d7ff9efe9a 100644 --- a/homeassistant/components/media_player/translations/ru.json +++ b/homeassistant/components/media_player/translations/ru.json @@ -18,8 +18,8 @@ "state": { "_": { "idle": "\u0411\u0435\u0437\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435", - "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d", "paused": "\u041f\u0430\u0443\u0437\u0430", "playing": "\u0412\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435", "standby": "\u041e\u0436\u0438\u0434\u0430\u043d\u0438\u0435" diff --git a/homeassistant/components/nexia/translations/ca.json b/homeassistant/components/nexia/translations/ca.json index beb5b1a1d9d..e0f0c87f60f 100644 --- a/homeassistant/components/nexia/translations/ca.json +++ b/homeassistant/components/nexia/translations/ca.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Marca", "password": "Contrasenya", "username": "Nom d'usuari" }, diff --git a/homeassistant/components/nexia/translations/et.json b/homeassistant/components/nexia/translations/et.json index ac8c354c3b3..2f9348c1eed 100644 --- a/homeassistant/components/nexia/translations/et.json +++ b/homeassistant/components/nexia/translations/et.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Tootja", "password": "Salas\u00f5na", "username": "Kasutajanimi" }, diff --git a/homeassistant/components/nexia/translations/it.json b/homeassistant/components/nexia/translations/it.json index 254617d718b..65d06c3c8f0 100644 --- a/homeassistant/components/nexia/translations/it.json +++ b/homeassistant/components/nexia/translations/it.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Marca", "password": "Password", "username": "Nome utente" }, diff --git a/homeassistant/components/nexia/translations/nl.json b/homeassistant/components/nexia/translations/nl.json index faa19d3b63c..14efdfcd221 100644 --- a/homeassistant/components/nexia/translations/nl.json +++ b/homeassistant/components/nexia/translations/nl.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Brand", "password": "Wachtwoord", "username": "Gebruikersnaam" }, diff --git a/homeassistant/components/nexia/translations/ru.json b/homeassistant/components/nexia/translations/ru.json index 74ec08ec2cf..b5572c9f2da 100644 --- a/homeassistant/components/nexia/translations/ru.json +++ b/homeassistant/components/nexia/translations/ru.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "\u041c\u0430\u0440\u043a\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, diff --git a/homeassistant/components/nexia/translations/zh-Hant.json b/homeassistant/components/nexia/translations/zh-Hant.json index 0e5f79ddc90..c066a433d1b 100644 --- a/homeassistant/components/nexia/translations/zh-Hant.json +++ b/homeassistant/components/nexia/translations/zh-Hant.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "\u54c1\u724c", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, diff --git a/homeassistant/components/nzbget/translations/it.json b/homeassistant/components/nzbget/translations/it.json index 7eaf5c78e08..17d7d93a6d6 100644 --- a/homeassistant/components/nzbget/translations/it.json +++ b/homeassistant/components/nzbget/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/it.json b/homeassistant/components/ovo_energy/translations/it.json index 9f4c2c68935..0f8dd492d02 100644 --- a/homeassistant/components/ovo_energy/translations/it.json +++ b/homeassistant/components/ovo_energy/translations/it.json @@ -5,7 +5,7 @@ "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index 18851d055a5..316d733121b 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -8,7 +8,7 @@ "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json index 48cd7c04743..85edd1656f8 100644 --- a/homeassistant/components/powerwall/translations/it.json +++ b/homeassistant/components/powerwall/translations/it.json @@ -10,7 +10,7 @@ "unknown": "Errore imprevisto", "wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Si prega di considerare l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/it.json b/homeassistant/components/rainmachine/translations/it.json index 7834c61bb44..c63bbd7db11 100644 --- a/homeassistant/components/rainmachine/translations/it.json +++ b/homeassistant/components/rainmachine/translations/it.json @@ -6,7 +6,7 @@ "error": { "invalid_auth": "Autenticazione non valida" }, - "flow_title": "RainMachine {ip}", + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/remote/translations/ru.json b/homeassistant/components/remote/translations/ru.json index 7afb30bda1f..f4ddbc09024 100644 --- a/homeassistant/components/remote/translations/ru.json +++ b/homeassistant/components/remote/translations/ru.json @@ -16,8 +16,8 @@ }, "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u041f\u0443\u043b\u044c\u0442 \u0414\u0423" diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json index 3c11aa4d8ae..5d833960240 100644 --- a/homeassistant/components/roku/translations/it.json +++ b/homeassistant/components/roku/translations/it.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "Vuoi configurare {name}?", diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json index 5e2e2b47141..0b2c9079ac9 100644 --- a/homeassistant/components/roomba/translations/it.json +++ b/homeassistant/components/roomba/translations/it.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/samsungtv/translations/it.json b/homeassistant/components/samsungtv/translations/it.json index 605685913ad..46cc5df4edd 100644 --- a/homeassistant/components/samsungtv/translations/it.json +++ b/homeassistant/components/samsungtv/translations/it.json @@ -7,7 +7,7 @@ "cannot_connect": "Impossibile connettersi", "not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato." }, - "flow_title": "Samsung TV: {model}", + "flow_title": "{model}", "step": { "confirm": { "description": "Vuoi configurare Samsung TV {model}? Se non hai mai connesso Home Assistant in precedenza, dovresti vedere un messaggio sul tuo TV in cui \u00e8 richiesta l'autorizzazione. Le configurazioni manuali per questo TV verranno sovrascritte.", diff --git a/homeassistant/components/screenlogic/translations/it.json b/homeassistant/components/screenlogic/translations/it.json index 8fc3c346c0f..778f69fc530 100644 --- a/homeassistant/components/screenlogic/translations/it.json +++ b/homeassistant/components/screenlogic/translations/it.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/script/translations/ru.json b/homeassistant/components/script/translations/ru.json index 327dc27843c..5feb9f660de 100644 --- a/homeassistant/components/script/translations/ru.json +++ b/homeassistant/components/script/translations/ru.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u0421\u043a\u0440\u0438\u043f\u0442" diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index 0e823c8d94c..c44c9002fef 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -35,8 +35,8 @@ }, "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u0421\u0435\u043d\u0441\u043e\u0440" diff --git a/homeassistant/components/smappee/translations/it.json b/homeassistant/components/smappee/translations/it.json index 5fd6bbf49c1..cc67aab0237 100644 --- a/homeassistant/components/smappee/translations/it.json +++ b/homeassistant/components/smappee/translations/it.json @@ -9,7 +9,7 @@ "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/it.json b/homeassistant/components/somfy_mylink/translations/it.json index ce049782c43..e519a4edd72 100644 --- a/homeassistant/components/somfy_mylink/translations/it.json +++ b/homeassistant/components/somfy_mylink/translations/it.json @@ -8,7 +8,7 @@ "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/it.json b/homeassistant/components/sonarr/translations/it.json index 71a4cca729e..9189c82692d 100644 --- a/homeassistant/components/sonarr/translations/it.json +++ b/homeassistant/components/sonarr/translations/it.json @@ -9,7 +9,7 @@ "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "L'integrazione di Sonarr deve essere nuovamente autenticata manualmente con l'API Sonarr ospitata su: {host}", diff --git a/homeassistant/components/songpal/translations/it.json b/homeassistant/components/songpal/translations/it.json index fff030a6d08..2eb4651d548 100644 --- a/homeassistant/components/songpal/translations/it.json +++ b/homeassistant/components/songpal/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "Vuoi impostare {name} ({host})?" diff --git a/homeassistant/components/squeezebox/translations/it.json b/homeassistant/components/squeezebox/translations/it.json index 13686f1eebc..166c69e21d6 100644 --- a/homeassistant/components/squeezebox/translations/it.json +++ b/homeassistant/components/squeezebox/translations/it.json @@ -10,7 +10,7 @@ "no_server_found": "Impossibile rilevare automaticamente il server.", "unknown": "Errore imprevisto" }, - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/switch/translations/ru.json b/homeassistant/components/switch/translations/ru.json index bd11c80ac6b..7e514883b48 100644 --- a/homeassistant/components/switch/translations/ru.json +++ b/homeassistant/components/switch/translations/ru.json @@ -16,8 +16,8 @@ }, "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c" diff --git a/homeassistant/components/syncthru/translations/it.json b/homeassistant/components/syncthru/translations/it.json index d8d0b70a8ed..d04af9a2a5b 100644 --- a/homeassistant/components/syncthru/translations/it.json +++ b/homeassistant/components/syncthru/translations/it.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "Il dispositivo non supporta SyncThru", "unknown_state": "Stato della stampante sconosciuto, verificare l'URL e la connettivit\u00e0 di rete" }, - "flow_title": "Stampante Samsung SyncThru: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json index f57cfedd12d..6f5fd4ac245 100644 --- a/homeassistant/components/synology_dsm/translations/it.json +++ b/homeassistant/components/synology_dsm/translations/it.json @@ -10,7 +10,7 @@ "otp_failed": "Autenticazione in due fasi fallita, riprovare con un nuovo codice di accesso", "unknown": "Errore imprevisto" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/system_bridge/translations/it.json b/homeassistant/components/system_bridge/translations/it.json index 5cc7ac8c7be..0fe7f0103b8 100644 --- a/homeassistant/components/system_bridge/translations/it.json +++ b/homeassistant/components/system_bridge/translations/it.json @@ -10,7 +10,7 @@ "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, - "flow_title": "Bridge di sistema: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index f5311f538c1..007da9f80ed 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -10,7 +10,7 @@ "service_unavailable": "Impossibile connettersi", "unknown_client_mac": "Nessun client disponibile su quell'indirizzo MAC" }, - "flow_title": "Rete UniFi {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/translations/ca.json b/homeassistant/components/upnp/translations/ca.json index 3b5763163b8..0fc68f8a9b7 100644 --- a/homeassistant/components/upnp/translations/ca.json +++ b/homeassistant/components/upnp/translations/ca.json @@ -17,9 +17,19 @@ "user": { "data": { "scan_interval": "Interval d'actualitzaci\u00f3 (en segons, m\u00ednim 30)", + "unique_id": "Dispositiu", "usn": "Dispositiu" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval d'actualitzaci\u00f3 (en segons, m\u00ednim 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/en.json b/homeassistant/components/upnp/translations/en.json index aa22348e308..c62ebfd2a87 100644 --- a/homeassistant/components/upnp/translations/en.json +++ b/homeassistant/components/upnp/translations/en.json @@ -12,7 +12,9 @@ }, "user": { "data": { - "unique_id": "Device" + "scan_interval": "Update interval (seconds, minimal 30)", + "unique_id": "Device", + "usn": "Device" } } } diff --git a/homeassistant/components/upnp/translations/et.json b/homeassistant/components/upnp/translations/et.json index dc505d30ffc..2ac4884c9ea 100644 --- a/homeassistant/components/upnp/translations/et.json +++ b/homeassistant/components/upnp/translations/et.json @@ -21,9 +21,19 @@ "user": { "data": { "scan_interval": "P\u00e4ringute intervall (sekundites, v\u00e4hemalt 30)", + "unique_id": "Seade", "usn": "Seade" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "P\u00e4ringute intervall (sekundites, v\u00e4hemalt 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json index ca4e376432c..67a3a385dbc 100644 --- a/homeassistant/components/upnp/translations/it.json +++ b/homeassistant/components/upnp/translations/it.json @@ -9,7 +9,7 @@ "one": "Vuoto", "other": "Vuoto" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Vuoi configurare questo dispositivo UPnP/IGD?" @@ -17,9 +17,19 @@ "user": { "data": { "scan_interval": "Intervallo di aggiornamento (secondi, minimo 30)", + "unique_id": "Dispositivo", "usn": "Dispositivo" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervallo di aggiornamento (secondi, minimo 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/nl.json b/homeassistant/components/upnp/translations/nl.json index c23d461da50..b4d690ca58c 100644 --- a/homeassistant/components/upnp/translations/nl.json +++ b/homeassistant/components/upnp/translations/nl.json @@ -21,9 +21,19 @@ "user": { "data": { "scan_interval": "Update-interval (seconden, minimaal 30)", + "unique_id": "Apparaat", "usn": "Apparaat" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update-interval (seconden, minimaal 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/ru.json b/homeassistant/components/upnp/translations/ru.json index 869428eb921..20c652fa1e0 100644 --- a/homeassistant/components/upnp/translations/ru.json +++ b/homeassistant/components/upnp/translations/ru.json @@ -19,9 +19,19 @@ "user": { "data": { "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0438\u043d\u0438\u043c\u0443\u043c 30)", + "unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "usn": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0438\u043d\u0438\u043c\u0443\u043c 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/ru.json b/homeassistant/components/water_heater/translations/ru.json index b3491f82e5e..1220a4ec70f 100644 --- a/homeassistant/components/water_heater/translations/ru.json +++ b/homeassistant/components/water_heater/translations/ru.json @@ -12,7 +12,7 @@ "gas": "\u0413\u0430\u0437", "heat_pump": "\u0422\u0435\u043f\u043b\u043e\u0432\u043e\u0439 \u043d\u0430\u0441\u043e\u0441", "high_demand": "\u0411\u043e\u043b\u044c\u0448\u0430\u044f \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0430", - "off": "\u0412\u044b\u043a\u043b", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "performance": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c" } } diff --git a/homeassistant/components/wilight/translations/it.json b/homeassistant/components/wilight/translations/it.json index eb9cdcc8a55..84b323a0000 100644 --- a/homeassistant/components/wilight/translations/it.json +++ b/homeassistant/components/wilight/translations/it.json @@ -5,7 +5,7 @@ "not_supported_device": "Questo WiLight non \u00e8 attualmente supportato", "not_wilight_device": "Questo dispositivo non \u00e8 WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Vuoi configurare WiLight {name}? \n\nSupporta: {components}", diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 8fb4dee9918..77e29c71e0c 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -12,7 +12,7 @@ "error": { "already_configured": "L'account \u00e8 gi\u00e0 configurato" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Scegli il metodo di autenticazione" diff --git a/homeassistant/components/wled/translations/it.json b/homeassistant/components/wled/translations/it.json index d34e1a98301..874639638b7 100644 --- a/homeassistant/components/wled/translations/it.json +++ b/homeassistant/components/wled/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index 275729e4e81..a730a2e24d9 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -12,7 +12,7 @@ "invalid_key": "Chiave gateway non valida", "invalid_mac": "Indirizzo Mac non valido" }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index 7eec7d7e424..8eb851cda5d 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -9,7 +9,7 @@ "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo.", "unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/yeelight/translations/it.json b/homeassistant/components/yeelight/translations/it.json index a8129f4359d..1a139dcd8b4 100644 --- a/homeassistant/components/yeelight/translations/it.json +++ b/homeassistant/components/yeelight/translations/it.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Impossibile connettersi" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Vuoi configurare {model} ({host})?" + }, "pick_device": { "data": { "device": "Dispositivo" diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 4a63866e7a3..247f6b4027e 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { From 2e1037005c4f107e67ced0876788cbe048707b0a Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 17 May 2021 20:34:25 -0700 Subject: [PATCH 546/852] Allow camera stream to fail safely (#50728) --- homeassistant/components/camera/__init__.py | 9 +++++--- tests/components/camera/test_init.py | 23 ++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index b52a36515d8..d9ccb0490c6 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -160,7 +160,7 @@ async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | @bind_hass async def async_get_mjpeg_stream( hass: HomeAssistant, request: web.Request, entity_id: str -) -> web.StreamResponse: +) -> web.StreamResponse | None: """Fetch an mjpeg stream from a camera entity.""" camera = _get_camera_from_entity_id(hass, entity_id) @@ -399,7 +399,7 @@ class Camera(Entity): async def handle_async_mjpeg_stream( self, request: web.Request - ) -> web.StreamResponse: + ) -> web.StreamResponse | None: """Serve an HTTP MJPEG stream from the camera. This method can be overridden by camera platforms to proxy @@ -543,7 +543,10 @@ class CameraMjpegStream(CameraView): """Serve camera stream, possibly with interval.""" interval_str = request.query.get("interval") if interval_str is None: - return await camera.handle_async_mjpeg_stream(request) + stream = await camera.handle_async_mjpeg_stream(request) + if stream is None: + raise web.HTTPBadGateway() + return stream try: # Compose camera stream from stills diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 340a4b5d756..7c7890a3e5f 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -11,7 +11,12 @@ 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.config import async_process_ha_core_config -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_START, + HTTP_BAD_GATEWAY, + HTTP_OK, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -354,3 +359,19 @@ async def test_record_service(hass, mock_camera, mock_stream): # So long as we call stream.record, the rest should be covered # by those tests. assert mock_record.called + + +async def test_camera_proxy_stream(hass, mock_camera, hass_client): + """Test record service.""" + + client = await hass_client() + + response = await client.get("/api/camera_proxy_stream/camera.demo_camera") + assert response.status == HTTP_OK + + with patch( + "homeassistant.components.demo.camera.DemoCamera.handle_async_mjpeg_stream", + return_value=None, + ): + response = await client.get("/api/camera_proxy_stream/camera.demo_camera") + assert response.status == HTTP_BAD_GATEWAY From 7a60d0eae41dff307e3378748e057da6e0fddc8a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 18 May 2021 06:41:56 +0300 Subject: [PATCH 547/852] Enable back free-mobile (#50802) --- homeassistant/components/free_mobile/manifest.json | 5 ++--- homeassistant/components/free_mobile/notify.py | 2 +- requirements_all.txt | 3 +++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index 89a21298a38..7fb7f998643 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -2,8 +2,7 @@ "domain": "free_mobile", "name": "Free Mobile", "documentation": "https://www.home-assistant.io/integrations/free_mobile", - "requirements": ["freesms==0.1.2"], + "requirements": ["freesms==0.2.0"], "codeowners": [], - "iot_class": "cloud_push", - "disabled": "https://github.com/home-assistant/core/pull/50749" + "iot_class": "cloud_push" } diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index 2cde2dd135e..a4351bfe678 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -1,7 +1,7 @@ """Support for Free Mobile SMS platform.""" import logging -from freesms import FreeClient # pylint: disable=import-error +from freesms import FreeClient import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService diff --git a/requirements_all.txt b/requirements_all.txt index 9c38fa48803..2c427851d01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -623,6 +623,9 @@ fortiosapi==0.10.8 # homeassistant.components.freebox freebox-api==0.0.10 +# homeassistant.components.free_mobile +freesms==0.2.0 + # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor From 3cc3cacd08cb3c9dad355bc7c47f113f30b6a485 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 May 2021 22:51:05 -0500 Subject: [PATCH 548/852] Start ServiceBrowser as soon as possible in zeroconf (#50784) Co-authored-by: Ruslan Sayfutdinov --- homeassistant/components/zeroconf/__init__.py | 181 +++++++++++------ .../components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zeroconf/test_init.py | 188 ++++++++++-------- 6 files changed, 227 insertions(+), 150 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 484d4404a66..ceaba3f02a1 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,7 +1,8 @@ """Support for exposing Home Assistant via Zeroconf.""" from __future__ import annotations -from collections.abc import Iterable +import asyncio +from collections.abc import Coroutine, Iterable from contextlib import suppress import fnmatch import ipaddress @@ -13,7 +14,6 @@ from typing import Any, TypedDict, cast from pyroute2 import IPRoute import voluptuous as vol from zeroconf import ( - Error as ZeroconfError, InterfaceChoice, IPVersion, NonUniqueNameException, @@ -29,7 +29,8 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, __version__, ) -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass @@ -90,6 +91,14 @@ class HaServiceInfo(TypedDict): properties: dict[str, Any] +class ZeroconfFlow(TypedDict): + """A queued zeroconf discovery flow.""" + + domain: str + context: dict[str, Any] + data: HaServiceInfo + + @bind_hass async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: """Zeroconf instance to be shared with other integrations that use it.""" @@ -183,6 +192,12 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: aio_zc = await _async_get_instance(hass, **zc_args) zeroconf = aio_zc.zeroconf + zeroconf_types, homekit_models = await asyncio.gather( + async_get_zeroconf(hass), async_get_homekit(hass) + ) + discovery = ZeroconfDiscovery(hass, zeroconf, zeroconf_types, homekit_models) + await discovery.async_setup() + async def _async_zeroconf_hass_start(_event: Event) -> None: """Expose Home Assistant on zeroconf when it starts. @@ -191,15 +206,17 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: uuid = await hass.helpers.instance_id.async_get() await _async_register_hass_zc_service(hass, aio_zc, uuid) - async def _async_zeroconf_hass_started(_event: Event) -> None: - """Start the service browser.""" + @callback + def _async_start_discovery(_event: Event) -> None: + """Start processing flows.""" + discovery.async_start() - await _async_start_zeroconf_browser(hass, zeroconf) + async def _async_zeroconf_hass_stop(_event: Event) -> None: + await discovery.async_stop() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_zeroconf_hass_start) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, _async_zeroconf_hass_started - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_start_discovery) return True @@ -259,44 +276,98 @@ async def _async_register_hass_zc_service( ) -async def _async_start_zeroconf_browser( - hass: HomeAssistant, zeroconf: Zeroconf -) -> None: - """Start the zeroconf browser.""" +class FlowDispatcher: + """Dispatch discovery flows.""" - zeroconf_types = await async_get_zeroconf(hass) - homekit_models = await async_get_homekit(hass) + def __init__(self, hass: HomeAssistant): + """Init the discovery dispatcher.""" + self.hass = hass + self.pending_flows: list[ZeroconfFlow] = [] + self.started = False - types = list(zeroconf_types) + @callback + def async_start(self) -> None: + """Start processing pending flows.""" + self.started = True + self.hass.loop.call_soon(self._async_process_pending_flows) - for hk_type in HOMEKIT_TYPES: - if hk_type not in zeroconf_types: - types.append(hk_type) + def _async_process_pending_flows(self) -> None: + for flow in self.pending_flows: + self.hass.async_create_task(self._init_flow(flow)) + self.pending_flows = [] + + def create(self, flow: ZeroconfFlow) -> None: + """Create and add or queue a flow.""" + if self.started: + self.hass.create_task(self._init_flow(flow)) + else: + self.pending_flows.append(flow) + + def _init_flow(self, flow: ZeroconfFlow) -> Coroutine[None, None, FlowResult]: + """Create a flow.""" + return self.hass.config_entries.flow.async_init( + flow["domain"], context=flow["context"], data=flow["data"] + ) + + +class ZeroconfDiscovery: + """Discovery via zeroconf.""" + + def __init__( + self, + hass: HomeAssistant, + zeroconf: Zeroconf, + zeroconf_types: dict[str, list[dict[str, str]]], + homekit_models: dict[str, str], + ) -> None: + """Init discovery.""" + self.hass = hass + self.zeroconf = zeroconf + self.zeroconf_types = zeroconf_types + self.homekit_models = homekit_models + + self.flow_dispatcher: FlowDispatcher | None = None + self.service_browser: HaServiceBrowser | None = None + + async def async_setup(self) -> None: + """Start discovery.""" + self.flow_dispatcher = FlowDispatcher(self.hass) + types = list(self.zeroconf_types) + # We want to make sure we know about other HomeAssistant + # instances as soon as possible to avoid name conflicts + # so we always browse for ZEROCONF_TYPE + for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES): + if hk_type not in self.zeroconf_types: + types.append(hk_type) + _LOGGER.debug("Starting Zeroconf browser") + self.service_browser = HaServiceBrowser( + self.zeroconf, types, handlers=[self.service_update] + ) + + async def async_stop(self) -> None: + """Cancel the service browser and stop processing the queue.""" + if self.service_browser: + await self.hass.async_add_executor_job(self.service_browser.cancel) + + @callback + def async_start(self) -> None: + """Start processing discovery flows.""" + assert self.flow_dispatcher is not None + self.flow_dispatcher.async_start() def service_update( + self, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange, ) -> None: """Service state changed.""" - nonlocal zeroconf_types - nonlocal homekit_models - if state_change == ServiceStateChange.Removed: return - try: - service_info = zeroconf.get_service_info(service_type, name) - except ZeroconfError: - _LOGGER.exception("Failed to get info for device %s", name) - return - - if not service_info: - # Prevent the browser thread from collapsing as - # service_info can be None - _LOGGER.debug("Failed to get info for device %s", name) - return + service_info = ServiceInfo(service_type, name) + service_info.load_from_cache(zeroconf) info = info_from_service(service_info) if not info: @@ -305,10 +376,12 @@ async def _async_start_zeroconf_browser( return _LOGGER.debug("Discovered new device %s %s", name, info) + assert self.flow_dispatcher is not None # If we can handle it as a HomeKit discovery, we do that here. if service_type in HOMEKIT_TYPES: - discovery_was_forwarded = handle_homekit(hass, homekit_models, info) + if pending_flow := handle_homekit(self.hass, self.homekit_models, info): + self.flow_dispatcher.create(pending_flow) # Continue on here as homekit_controller # still needs to get updates on devices # so it can see when the 'c#' field is updated. @@ -316,10 +389,7 @@ async def _async_start_zeroconf_browser( # We only send updates to homekit_controller # if the device is already paired in order to avoid # offering a second discovery for the same device - if ( - discovery_was_forwarded - and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"] - ): + if pending_flow and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"]: try: # 0 means paired and not discoverable by iOS clients) if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]): @@ -348,7 +418,7 @@ async def _async_start_zeroconf_browser( # Not all homekit types are currently used for discovery # so not all service type exist in zeroconf_types - for matcher in zeroconf_types.get(service_type, []): + for matcher in self.zeroconf_types.get(service_type, []): if len(matcher) > 1: if "macaddress" in matcher and ( uppercase_mac is None @@ -368,19 +438,17 @@ async def _async_start_zeroconf_browser( ): continue - hass.add_job( - hass.config_entries.flow.async_init( - matcher["domain"], context={"source": DOMAIN}, data=info - ) # type: ignore - ) - - _LOGGER.debug("Starting Zeroconf browser") - HaServiceBrowser(zeroconf, types, handlers=[service_update]) + flow: ZeroconfFlow = { + "domain": matcher["domain"], + "context": {"source": config_entries.SOURCE_ZEROCONF}, + "data": info, + } + self.flow_dispatcher.create(flow) def handle_homekit( hass: HomeAssistant, homekit_models: dict[str, str], info: HaServiceInfo -) -> bool: +) -> ZeroconfFlow | None: """Handle a HomeKit discovery. Return if discovery was forwarded. @@ -394,7 +462,7 @@ def handle_homekit( break if model is None: - return False + return None for test_model in homekit_models: if ( @@ -404,16 +472,13 @@ def handle_homekit( ): continue - hass.add_job( - hass.config_entries.flow.async_init( - homekit_models[test_model], - context={"source": config_entries.SOURCE_HOMEKIT}, - data=info, - ) # type: ignore - ) - return True + return { + "domain": homekit_models[test_model], + "context": {"source": config_entries.SOURCE_HOMEKIT}, + "data": info, + } - return False + return None def info_from_service(service: ServiceInfo) -> HaServiceInfo | None: diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index d76639f3b5b..3abd8824eba 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.30.0","pyroute2==0.5.18"], + "requirements": ["zeroconf==0.31.0","pyroute2==0.5.18"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0d51642db39..9755a5b2e5d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ sqlalchemy==1.4.13 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.30.0 +zeroconf==0.31.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 2c427851d01..4a14eb63e6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2402,7 +2402,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.30.0 +zeroconf==0.31.0 # homeassistant.components.zha zha-quirks==0.0.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67a4b3f20cd..fd825852e90 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1290,7 +1290,7 @@ yeelight==0.6.2 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.30.0 +zeroconf==0.31.0 # homeassistant.components.zha zha-quirks==0.0.57 diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 809177b6089..d8a9a94da96 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,14 +1,7 @@ """Test Zeroconf component setup process.""" from unittest.mock import patch -from zeroconf import ( - BadTypeInNameException, - Error as ZeroconfError, - InterfaceChoice, - IPVersion, - ServiceInfo, - ServiceStateChange, -) +from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange from homeassistant.components import zeroconf from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 @@ -149,8 +142,10 @@ async def test_setup(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_service_info_mock + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -181,8 +176,9 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog): hass.config, "location_name", "\u00dcBER \u00dcber German Umlaut long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string", + ), patch( + "homeassistant.components.zeroconf.ServiceInfo.request", ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -195,8 +191,10 @@ async def test_setup_with_default_interface(hass, mock_zeroconf): """Test default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: True}} ) @@ -210,8 +208,10 @@ async def test_setup_without_default_interface(hass, mock_zeroconf): """Test without default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: False}} ) @@ -223,8 +223,10 @@ async def test_setup_without_ipv6(hass, mock_zeroconf): """Test without ipv6.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: False}} ) @@ -238,8 +240,10 @@ async def test_setup_with_ipv6(hass, mock_zeroconf): """Test without ipv6.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: True}} ) @@ -253,8 +257,10 @@ async def test_setup_with_ipv6_default(hass, mock_zeroconf): """Test without ipv6 as default.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -262,20 +268,6 @@ async def test_setup_with_ipv6_default(hass, mock_zeroconf): assert mock_zeroconf.called_with() -async def test_service_with_invalid_name(hass, mock_zeroconf, caplog): - """Test we do not crash on service with an invalid name.""" - with patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = BadTypeInNameException - assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert len(mock_service_browser.mock_calls) == 1 - assert "Failed to get info for device" in caplog.text - - async def test_zeroconf_match_macaddress(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" @@ -300,10 +292,10 @@ async def test_zeroconf_match_macaddress(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( - "FFAADDCC11DD" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -333,10 +325,10 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = ( - get_zeroconf_info_mock_manufacturer("Samsung Electronics") - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -366,10 +358,10 @@ async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( - "aa:bb:cc:dd:ee:ff" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock("aabbccddeeff"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -398,10 +390,10 @@ async def test_zeroconf_no_match(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( - "FFAADDCC11DD" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -430,10 +422,10 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = ( - get_zeroconf_info_mock_manufacturer("Not Samsung Electronics") - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -456,10 +448,10 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "LIFX bulb", HOMEKIT_STATUS_UNPAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -483,10 +475,10 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._udp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -510,10 +502,10 @@ async def test_homekit_match_full(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._udp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "BSB002", HOMEKIT_STATUS_UNPAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -537,10 +529,10 @@ async def test_homekit_already_paired(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "tado", HOMEKIT_STATUS_PAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -565,10 +557,10 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "tado", b"invalid" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("tado", b"invalid"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -588,10 +580,12 @@ async def test_homekit_not_paired(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock( "this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED - ) + ), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -636,34 +630,44 @@ async def test_get_instance(hass, mock_zeroconf): async def test_removed_ignored(hass, mock_zeroconf): """Test we remove it when a zeroconf entry is removed.""" - mock_zeroconf.get_service_info.side_effect = ZeroconfError def service_update_mock(zeroconf, services, handlers): """Call service update handler.""" handlers[0]( - zeroconf, "_service.added", "name._service.added", ServiceStateChange.Added + zeroconf, + "_service.added.local.", + "name._service.added.local.", + ServiceStateChange.Added, ) handlers[0]( zeroconf, - "_service.updated", - "name._service.updated", + "_service.updated.local.", + "name._service.updated.local.", ServiceStateChange.Updated, ) handlers[0]( zeroconf, - "_service.removed", - "name._service.removed", + "_service.removed.local.", + "name._service.removed.local.", ServiceStateChange.Removed, ) - with patch.object(zeroconf, "HaServiceBrowser", side_effect=service_update_mock): + with patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, + ) as mock_service_info: assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert len(mock_zeroconf.get_service_info.mock_calls) == 2 - assert mock_zeroconf.get_service_info.mock_calls[0][1][0] == "_service.added" - assert mock_zeroconf.get_service_info.mock_calls[1][1][0] == "_service.updated" + assert len(mock_service_info.mock_calls) == 2 + import pprint + + pprint.pprint(mock_service_info.mock_calls[0][1]) + assert mock_service_info.mock_calls[0][1][0] == "_service.added.local." + assert mock_service_info.mock_calls[1][1][0] == "_service.updated.local." async def test_async_detect_interfaces_setting_non_loopback_route(hass, mock_zeroconf): @@ -673,8 +677,10 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, mock_zer ), patch( "homeassistant.components.zeroconf.IPRoute.route", return_value=_ROUTE_NO_LOOPBACK, + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -688,8 +694,10 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, mock_zerocon zeroconf, "HaServiceBrowser", side_effect=service_update_mock ), patch( "homeassistant.components.zeroconf.IPRoute.route", return_value=_ROUTE_LOOPBACK + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -701,8 +709,10 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_zeroconf): """Test without default interface config and the route returns nothing.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ), patch("homeassistant.components.zeroconf.IPRoute.route", return_value=[]): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock + ), patch("homeassistant.components.zeroconf.IPRoute.route", return_value=[]), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -716,8 +726,10 @@ async def test_async_detect_interfaces_setting_exception(hass, mock_zeroconf): zeroconf, "HaServiceBrowser", side_effect=service_update_mock ), patch( "homeassistant.components.zeroconf.IPRoute.route", side_effect=AttributeError + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() From 5da0191fe32edfbe9693ea43f2a402c567e88b32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 May 2021 22:52:08 -0500 Subject: [PATCH 549/852] Bump zeroconf to 0.31.0 (#50807) From 9abf43f95ff7d05f0a959c9b6e9a2fa85b05f928 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 18 May 2021 08:24:42 +0200 Subject: [PATCH 550/852] Mqtt fan feature for resetting current speed `percentage` or `preset_mode` (#50565) * Mqtt fan resetting speed percentage or preset_mode * tests reset payload is working with val templates * Remove duplicate line for CONF_PAYLOAD_HIGH_SPEED --- .../components/mqtt/abbreviations.py | 2 + homeassistant/components/mqtt/fan.py | 44 ++++++++++++++++--- tests/components/mqtt/test_fan.py | 39 ++++++++++++++++ 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 78d0434e412..5d34c92c1b1 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -119,6 +119,8 @@ ABBREVIATIONS = { "pl_osc_off": "payload_oscillation_off", "pl_osc_on": "payload_oscillation_on", "pl_paus": "payload_pause", + "pl_rst_pct": "payload_reset_percentage", + "pl_rst_pr_mode": "payload_reset_preset_mode", "pl_stop": "payload_stop", "pl_strt": "payload_start", "pl_stpa": "payload_start_pause", diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index b4718499d64..5cd924551f7 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -59,6 +59,7 @@ CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template" CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template" +CONF_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage" CONF_SPEED_RANGE_MIN = "speed_range_min" CONF_SPEED_RANGE_MAX = "speed_range_max" CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic" @@ -66,6 +67,7 @@ CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic" CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template" CONF_PRESET_MODES_LIST = "preset_modes" +CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode" CONF_SPEED_STATE_TOPIC = "speed_state_topic" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" CONF_SPEED_VALUE_TEMPLATE = "speed_value_template" @@ -84,6 +86,7 @@ CONF_SPEED_LIST = "speeds" DEFAULT_NAME = "MQTT Fan" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" +DEFAULT_PAYLOAD_RESET = "None" DEFAULT_OPTIMISTIC = False DEFAULT_SPEED_RANGE_MIN = 1 DEFAULT_SPEED_RANGE_MAX = 100 @@ -113,6 +116,13 @@ def valid_speed_range_configuration(config): return config +def valid_preset_mode_configuration(config): + """Validate that the preset mode reset payload is not one of the preset modes.""" + if config.get(CONF_PAYLOAD_RESET_PRESET_MODE) in config.get(CONF_PRESET_MODES_LIST): + raise ValueError("preset_modes must not contain payload_reset_preset_mode") + return config + + PLATFORM_SCHEMA = vol.All( # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, CONF_SPEED_LIST and # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, @@ -153,6 +163,12 @@ PLATFORM_SCHEMA = vol.All( vol.Optional( CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX ): cv.positive_int, + vol.Optional( + CONF_PAYLOAD_RESET_PERCENTAGE, default=DEFAULT_PAYLOAD_RESET + ): cv.string, + vol.Optional( + CONF_PAYLOAD_RESET_PRESET_MODE, default=DEFAULT_PAYLOAD_RESET + ): cv.string, vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string, @@ -177,6 +193,7 @@ PLATFORM_SCHEMA = vol.All( ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), valid_fan_speed_configuration, valid_speed_range_configuration, + valid_preset_mode_configuration, ) @@ -281,6 +298,8 @@ class MqttFan(MqttEntity, FanEntity): "SPEED_MEDIUM": config[CONF_PAYLOAD_MEDIUM_SPEED], "SPEED_HIGH": config[CONF_PAYLOAD_HIGH_SPEED], "SPEED_OFF": config[CONF_PAYLOAD_OFF_SPEED], + "PERCENTAGE_RESET": config[CONF_PAYLOAD_RESET_PERCENTAGE], + "PRESET_MODE_RESET": config[CONF_PAYLOAD_RESET_PRESET_MODE], } # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) self._feature_legacy_speeds = not self._topic[CONF_SPEED_COMMAND_TOPIC] is None @@ -364,20 +383,27 @@ class MqttFan(MqttEntity, FanEntity): @log_messages(self.hass, self.entity_id) def percentage_received(msg): """Handle new received MQTT message for the percentage.""" - numeric_val_str = self._value_templates[ATTR_PERCENTAGE](msg.payload) - if not numeric_val_str: + rendered_percentage_payload = self._value_templates[ATTR_PERCENTAGE]( + msg.payload + ) + if not rendered_percentage_payload: _LOGGER.debug("Ignoring empty speed from '%s'", msg.topic) return + if rendered_percentage_payload == self._payload["PERCENTAGE_RESET"]: + self._percentage = None + self._speed = None + self.async_write_ha_state() + return try: percentage = ranged_value_to_percentage( - self._speed_range, int(numeric_val_str) + self._speed_range, int(rendered_percentage_payload) ) except ValueError: _LOGGER.warning( "'%s' received on topic %s. '%s' is not a valid speed within the speed range", msg.payload, msg.topic, - numeric_val_str, + rendered_percentage_payload, ) return if percentage < 0 or percentage > 100: @@ -385,7 +411,7 @@ class MqttFan(MqttEntity, FanEntity): "'%s' received on topic %s. '%s' is not a valid speed within the speed range", msg.payload, msg.topic, - numeric_val_str, + rendered_percentage_payload, ) return self._percentage = percentage @@ -404,6 +430,10 @@ class MqttFan(MqttEntity, FanEntity): def preset_mode_received(msg): """Handle new received MQTT message for preset mode.""" preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload) + if preset_mode == self._payload["PRESET_MODE_RESET"]: + self._preset_mode = None + self.async_write_ha_state() + return if not preset_mode: _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) return @@ -658,7 +688,7 @@ class MqttFan(MqttEntity, FanEntity): if self._optimistic_preset_mode: self._preset_mode = preset_mode - self.async_write_ha_state() + self.async_write_ha_state() # async_set_speed is deprecated, support will be removed after a quarter (2021.7) async def async_set_speed(self, speed: str) -> None: @@ -691,7 +721,7 @@ class MqttFan(MqttEntity, FanEntity): if self._optimistic_speed and speed_payload: self._speed = speed - self.async_write_ha_state() + self.async_write_ha_state() async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation. diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index ee12a7ce03c..dad365e3c66 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -100,6 +100,8 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): "payload_low_speed": "speed_lOw", "payload_medium_speed": "speed_mEdium", "payload_high_speed": "speed_High", + "payload_reset_percentage": "rEset_percentage", + "payload_reset_preset_mode": "rEset_preset_mode", } }, ) @@ -168,6 +170,10 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): state = hass.states.get("fan.test") assert state.attributes.get("preset_mode") == "silent" + async_fire_mqtt_message(hass, "preset-mode-state-topic", "rEset_preset_mode") + state = hass.states.get("fan.test") + assert state.attributes.get("preset_mode") is None + async_fire_mqtt_message(hass, "preset-mode-state-topic", "ModeUnknown") assert "not a valid preset mode" in caplog.text caplog.clear() @@ -191,6 +197,11 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): state = hass.states.get("fan.test") assert state.attributes.get("speed") == fan.SPEED_OFF + async_fire_mqtt_message(hass, "percentage-state-topic", "rEset_percentage") + state = hass.states.get("fan.test") + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None + assert state.attributes.get(fan.ATTR_SPEED) is None + async_fire_mqtt_message(hass, "speed-state-topic", "speed_very_high") assert "not a valid speed" in caplog.text caplog.clear() @@ -408,6 +419,10 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 + async_fire_mqtt_message(hass, "percentage-state-topic", '{"val": "None"}') + state = hass.states.get("fan.test") + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None + async_fire_mqtt_message(hass, "percentage-state-topic", '{"otherval": 100}') assert "Ignoring empty speed from" in caplog.text caplog.clear() @@ -428,6 +443,10 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap state = hass.states.get("fan.test") assert state.attributes.get("preset_mode") == "silent" + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "None"}') + state = hass.states.get("fan.test") + assert state.attributes.get("preset_mode") is None + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"otherval": 100}') assert "Ignoring empty preset_mode from" in caplog.text caplog.clear() @@ -1895,6 +1914,21 @@ async def test_supported_features(hass, mqtt_mock): "speed_range_min": 0, "speed_range_max": 40, }, + { + "platform": "mqtt", + "name": "test7reset_payload_in_preset_modes_a", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["auto", "smart", "normal", "None"], + }, + { + "platform": "mqtt", + "name": "test7reset_payload_in_preset_modes_b", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["whoosh", "silent", "auto", "None"], + "payload_reset_preset_mode": "normal", + }, ] }, ) @@ -1962,6 +1996,11 @@ async def test_supported_features(hass, mqtt_mock): state = hass.states.get("fan.test6spd_range_c") assert state is None + state = hass.states.get("fan.test7reset_payload_in_preset_modes_a") + assert state is None + state = hass.states.get("fan.test7reset_payload_in_preset_modes_b") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE + async def test_availability_when_connection_lost(hass, mqtt_mock): """Test availability after MQTT disconnection.""" From 1d174a1f6f73a99df2d6c88134541bcfb3f8f45c Mon Sep 17 00:00:00 2001 From: Aaron David Schneider Date: Tue, 18 May 2021 08:40:51 +0200 Subject: [PATCH 551/852] Bump pysonos to 0.0.48 (#50798) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 5d2ad08bf03..af3d79f96d6 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.47"], + "requirements": ["pysonos==0.0.48"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 4a14eb63e6f..120926d5123 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1753,7 +1753,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.47 +pysonos==0.0.48 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd825852e90..2894eea7c2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.47 +pysonos==0.0.48 # homeassistant.components.spc pyspcwebgw==0.4.0 From 775af9d2c5f9b9cfab56879dcc35525ae5d27121 Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 18 May 2021 14:15:47 -0500 Subject: [PATCH 552/852] Update PyISY to v3.0.0 and ISY994 to use Async IO (#50806) --- homeassistant/components/isy994/__init__.py | 82 ++++++++--- .../components/isy994/binary_sensor.py | 45 +++--- homeassistant/components/isy994/climate.py | 20 +-- .../components/isy994/config_flow.py | 50 +++---- homeassistant/components/isy994/const.py | 5 +- homeassistant/components/isy994/cover.py | 21 +-- homeassistant/components/isy994/entity.py | 40 ++--- homeassistant/components/isy994/fan.py | 24 ++- homeassistant/components/isy994/helpers.py | 5 +- homeassistant/components/isy994/light.py | 24 +-- homeassistant/components/isy994/lock.py | 17 ++- homeassistant/components/isy994/manifest.json | 6 +- homeassistant/components/isy994/sensor.py | 7 +- homeassistant/components/isy994/services.py | 28 ++-- homeassistant/components/isy994/services.yaml | 40 ++--- homeassistant/components/isy994/switch.py | 17 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/isy994/test_config_flow.py | 138 +++++++++++------- 19 files changed, 325 insertions(+), 248 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 90e114e7023..27d81a671c8 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,16 +1,23 @@ """Support the ISY-994 controllers.""" from __future__ import annotations -from functools import partial from urllib.parse import urlparse -from pyisy import ISY +from aiohttp import CookieJar +import async_timeout +from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -32,7 +39,7 @@ from .const import ( ISY994_VARIABLES, MANUFACTURER, PLATFORMS, - SUPPORTED_PROGRAM_PLATFORMS, + PROGRAM_PLATFORMS, UNDO_UPDATE_LISTENER, ) from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables @@ -115,7 +122,7 @@ async def async_setup_entry( hass_isy_data[ISY994_NODES][platform] = [] hass_isy_data[ISY994_PROGRAMS] = {} - for platform in SUPPORTED_PROGRAM_PLATFORMS: + for platform in PROGRAM_PLATFORMS: hass_isy_data[ISY994_PROGRAMS][platform] = [] hass_isy_data[ISY994_VARIABLES] = [] @@ -139,31 +146,50 @@ async def async_setup_entry( if host.scheme == "http": https = False port = host.port or 80 + session = aiohttp_client.async_create_clientsession( + hass, verify_ssl=None, cookie_jar=CookieJar(unsafe=True) + ) elif host.scheme == "https": https = True port = host.port or 443 + session = aiohttp_client.async_get_clientsession(hass) else: _LOGGER.error("The isy994 host value in configuration is invalid") return False # Connect to ISY controller. - isy = await hass.async_add_executor_job( - partial( - ISY, - host.hostname, - port, - username=user, - password=password, - use_https=https, - tls_ver=tls_version, - webroot=host.path, - ) + isy = ISY( + host.hostname, + port, + username=user, + password=password, + use_https=https, + tls_ver=tls_version, + webroot=host.path, + websession=session, + use_websocket=True, ) - if not isy.connected: - return False - # Trigger a status update for all nodes, not done automatically in PyISY v2.x - await hass.async_add_executor_job(isy.nodes.update) + try: + with async_timeout.timeout(30): + await isy.initialize() + except ISYInvalidAuthError as err: + _LOGGER.error( + "Invalid credentials for the ISY, please adjust settings and try again: %s", + err, + ) + return False + except ISYConnectionError as err: + _LOGGER.error( + "Failed to connect to the ISY, please adjust settings and try again: %s", + err, + ) + raise ConfigEntryNotReady from err + except ISYResponseParseError as err: + _LOGGER.warning( + "Error processing responses from the ISY; device may be busy, trying again later" + ) + raise ConfigEntryNotReady from err _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) _categorize_programs(hass_isy_data, isy.programs) @@ -181,13 +207,21 @@ async def async_setup_entry( def _start_auto_update() -> None: """Start isy auto update.""" _LOGGER.debug("ISY Starting Event Stream and automatic updates") - isy.auto_update = True + isy.websocket.start() + + def _stop_auto_update(event) -> None: + """Stop the isy auto update on Home Assistant Shutdown.""" + _LOGGER.debug("ISY Stopping Event Stream and automatic updates") + isy.websocket.stop() await hass.async_add_executor_job(_start_auto_update) undo_listener = entry.add_update_listener(_async_update_listener) hass_isy_data[UNDO_UPDATE_LISTENER] = undo_listener + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_auto_update) + ) # Register Integration-wide Services: async_setup_services(hass) @@ -248,9 +282,9 @@ async def async_unload_entry( isy = hass_isy_data[ISY994_ISY] def _stop_auto_update() -> None: - """Start isy auto update.""" + """Stop the isy auto update.""" _LOGGER.debug("ISY Stopping Event Stream and automatic updates") - isy.auto_update = False + isy.websocket.stop() await hass.async_add_executor_job(_stop_auto_update) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index c58c37edb42..4a259dac6d8 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -251,11 +251,11 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): """Subscribe to the node and subnode event emitters.""" await super().async_added_to_hass() - self._node.control_events.subscribe(self._positive_node_control_handler) + self._node.control_events.subscribe(self._async_positive_node_control_handler) if self._negative_node is not None: self._negative_node.control_events.subscribe( - self._negative_node_control_handler + self._async_negative_node_control_handler ) def add_heartbeat_device(self, device) -> None: @@ -267,10 +267,10 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): """ self._heartbeat_device = device - def _heartbeat(self) -> None: + def _async_heartbeat(self) -> None: """Send a heartbeat to our heartbeat device, if we have one.""" if self._heartbeat_device is not None: - self._heartbeat_device.heartbeat() + self._heartbeat_device.async_heartbeat() def add_negative_node(self, child) -> None: """Add a negative node to this binary sensor device. @@ -292,7 +292,8 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): # of the sensor until we receive our first ON event. self._computed_state = None - def _negative_node_control_handler(self, event: object) -> None: + @callback + def _async_negative_node_control_handler(self, event: object) -> None: """Handle an "On" control event from the "negative" node.""" if event.control == CMD_ON: _LOGGER.debug( @@ -300,10 +301,11 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): self.name, ) self._computed_state = False - self.schedule_update_ha_state() - self._heartbeat() + self.async_write_ha_state() + self._async_heartbeat() - def _positive_node_control_handler(self, event: object) -> None: + @callback + def _async_positive_node_control_handler(self, event: object) -> None: """Handle On and Off control event coming from the primary node. Depending on device configuration, sometimes only On events @@ -316,18 +318,19 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): self.name, ) self._computed_state = True - self.schedule_update_ha_state() - self._heartbeat() + self.async_write_ha_state() + self._async_heartbeat() if event.control == CMD_OFF: _LOGGER.debug( "Sensor %s turning Off via the Primary node sending a DOF command", self.name, ) self._computed_state = False - self.schedule_update_ha_state() - self._heartbeat() + self.async_write_ha_state() + self._async_heartbeat() - def on_update(self, event: object) -> None: + @callback + def async_on_update(self, event: object) -> None: """Primary node status updates. We MOSTLY ignore these updates, as we listen directly to the Control @@ -340,8 +343,8 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): if self._status_was_unknown and self._computed_state is None: self._computed_state = bool(self._node.status) self._status_was_unknown = False - self.schedule_update_ha_state() - self._heartbeat() + self.async_write_ha_state() + self._async_heartbeat() @property def is_on(self) -> bool: @@ -395,9 +398,10 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): The ISY uses both DON and DOF commands (alternating) for a heartbeat. """ if event.control in [CMD_ON, CMD_OFF]: - self.heartbeat() + self.async_heartbeat() - def heartbeat(self): + @callback + def async_heartbeat(self): """Mark the device as online, and restart the 25 hour timer. This gets called when the heartbeat node beats, but also when the @@ -407,7 +411,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): """ self._computed_state = False self._restart_timer() - self.schedule_update_ha_state() + self.async_write_ha_state() def _restart_timer(self): """Restart the 25 hour timer.""" @@ -423,7 +427,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): """Heartbeat missed; set state to ON to indicate dead battery.""" self._computed_state = True self._heartbeat_timer = None - self.schedule_update_ha_state() + self.async_write_ha_state() point_in_time = dt_util.utcnow() + timedelta(hours=25) _LOGGER.debug( @@ -436,7 +440,8 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): self.hass, timer_elapsed, point_in_time ) - def on_update(self, event: object) -> None: + @callback + def async_on_update(self, event: object) -> None: """Ignore node status updates. We listen directly to the Control events for this device. diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 5895a060db2..578fbe2bf21 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -203,7 +203,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return None return UOM_TO_STATES[UOM_FAN_MODES].get(fan_mode.value) - def set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" target_temp = kwargs.get(ATTR_TEMPERATURE) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) @@ -214,27 +214,27 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): if self.hvac_mode == HVAC_MODE_HEAT: target_temp_low = target_temp if target_temp_low is not None: - self._node.set_climate_setpoint_heat(int(target_temp_low)) + await self._node.set_climate_setpoint_heat(int(target_temp_low)) # Presumptive setting--event stream will correct if cmd fails: self._target_temp_low = target_temp_low if target_temp_high is not None: - self._node.set_climate_setpoint_cool(int(target_temp_high)) + await self._node.set_climate_setpoint_cool(int(target_temp_high)) # Presumptive setting--event stream will correct if cmd fails: self._target_temp_high = target_temp_high - self.schedule_update_ha_state() + self.async_write_ha_state() - def set_fan_mode(self, fan_mode: str) -> None: + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" _LOGGER.debug("Requested fan mode %s", fan_mode) - self._node.set_fan_mode(HA_FAN_TO_ISY.get(fan_mode)) + await self._node.set_fan_mode(HA_FAN_TO_ISY.get(fan_mode)) # Presumptive setting--event stream will correct if cmd fails: self._fan_mode = fan_mode - self.schedule_update_ha_state() + self.async_write_ha_state() - def set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" _LOGGER.debug("Requested operation mode %s", hvac_mode) - self._node.set_climate_mode(HA_HVAC_TO_ISY.get(hvac_mode)) + await self._node.set_climate_mode(HA_HVAC_TO_ISY.get(hvac_mode)) # Presumptive setting--event stream will correct if cmd fails: self._hvac_mode = hvac_mode - self.schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 248d2d9c520..bd1baa66045 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -2,6 +2,9 @@ import logging from urllib.parse import urlparse +from aiohttp import CookieJar +import async_timeout +from pyisy import ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError from pyisy.configuration import Configuration from pyisy.connection import Connection import voluptuous as vol @@ -11,6 +14,7 @@ from homeassistant.components import ssdp from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client from .const import ( CONF_IGNORE_STRING, @@ -57,25 +61,41 @@ async def validate_input(hass: core.HomeAssistant, data): if host.scheme == "http": https = False port = host.port or 80 + session = aiohttp_client.async_create_clientsession( + hass, verify_ssl=None, cookie_jar=CookieJar(unsafe=True) + ) elif host.scheme == "https": https = True port = host.port or 443 + session = aiohttp_client.async_get_clientsession(hass) else: _LOGGER.error("The isy994 host value in configuration is invalid") raise InvalidHost # Connect to ISY controller. - isy_conf = await hass.async_add_executor_job( - _fetch_isy_configuration, + isy_conn = Connection( host.hostname, port, user, password, - https, - tls_version, - host.path, + use_https=https, + tls_ver=tls_version, + webroot=host.path, + websession=session, ) + try: + with async_timeout.timeout(30): + isy_conf_xml = await isy_conn.test_connection() + except ISYInvalidAuthError as error: + raise InvalidAuth from error + except ISYConnectionError as error: + raise CannotConnect from error + + try: + isy_conf = Configuration(xml=isy_conf_xml) + except ISYResponseParseError as error: + raise CannotConnect from error if not isy_conf or "name" not in isy_conf or not isy_conf["name"]: raise CannotConnect @@ -83,26 +103,6 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": f"{isy_conf['name']} ({host.hostname})", "uuid": isy_conf["uuid"]} -def _fetch_isy_configuration( - address, port, username, password, use_https, tls_ver, webroot -): - """Validate and fetch the configuration from the ISY.""" - try: - isy_conn = Connection( - address, - port, - username, - password, - use_https, - tls_ver, - webroot=webroot, - ) - except ValueError as err: - raise InvalidAuth(err.args[0]) from err - - return Configuration(xml=isy_conn.get_config()) - - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Universal Devices ISY994.""" diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 9fdef92c84f..ed40c7eb289 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -130,7 +130,7 @@ KEY_ACTIONS = "actions" KEY_STATUS = "status" PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH, CLIMATE] -SUPPORTED_PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH] +PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH] SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] @@ -184,6 +184,7 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" # Used for discovery UDN_UUID_PREFIX = "uuid:" ISY_URL_POSTFIX = "/desc" +EVENTS_SUFFIX = "_ISYSUB" # Special Units of Measure UOM_ISYV4_DEGREES = "degrees" @@ -352,7 +353,7 @@ UOM_FRIENDLY_NAME = { "22": "%RH", "23": PRESSURE_INHG, "24": SPEED_INCHES_PER_HOUR, - UOM_INDEX: "index", # Index type. Use "node.formatted" for value + UOM_INDEX: UOM_INDEX, # Index type. Use "node.formatted" for value "26": TEMP_KELVIN, "27": "keyword", "28": MASS_KILOGRAMS, diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 8bac6f50eb7..ca5432f4456 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,4 +1,5 @@ """Support for ISY994 covers.""" + from pyisy.constants import ISY_VALUE_UNKNOWN from homeassistant.components.cover import ( @@ -67,23 +68,23 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity): """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - def open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs) -> None: """Send the open cover command to the ISY994 cover device.""" val = 100 if self._node.uom == UOM_BARRIER else None - if not self._node.turn_on(val=val): + if not await self._node.turn_on(val=val): _LOGGER.error("Unable to open the cover") - def close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs) -> None: """Send the close cover command to the ISY994 cover device.""" - if not self._node.turn_off(): + if not await self._node.turn_off(): _LOGGER.error("Unable to close the cover") - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] if self._node.uom == UOM_8_BIT_RANGE: position = round(position * 255.0 / 100.0) - if not self._node.turn_on(val=position): + if not await self._node.turn_on(val=position): _LOGGER.error("Unable to set cover position") @@ -95,12 +96,12 @@ class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity): """Get whether the ISY994 cover program is closed.""" return bool(self._node.status) - def open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs) -> None: """Send the open cover command to the ISY994 cover program.""" - if not self._actions.run_then(): + if not await self._actions.run_then(): _LOGGER.error("Unable to open the cover") - def close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs) -> None: """Send the close cover command to the ISY994 cover program.""" - if not self._actions.run_else(): + if not await self._actions.run_else(): _LOGGER.error("Unable to close the cover") diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 25a2dc428a6..6dab5b2ed65 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -11,9 +11,11 @@ from pyisy.constants import ( from pyisy.helpers import NodeProperty from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import Entity -from .const import _LOGGER, DOMAIN +from .const import DOMAIN class ISYEntity(Entity): @@ -30,16 +32,20 @@ class ISYEntity(Entity): async def async_added_to_hass(self) -> None: """Subscribe to the node change events.""" - self._change_handler = self._node.status_events.subscribe(self.on_update) + self._change_handler = self._node.status_events.subscribe(self.async_on_update) if hasattr(self._node, "control_events"): - self._control_handler = self._node.control_events.subscribe(self.on_control) + self._control_handler = self._node.control_events.subscribe( + self.async_on_control + ) - def on_update(self, event: object) -> None: + @callback + def async_on_update(self, event: object) -> None: """Handle the update event from the ISY994 Node.""" - self.schedule_update_ha_state() + self.async_write_ha_state() - def on_control(self, event: NodeProperty) -> None: + @callback + def async_on_control(self, event: NodeProperty) -> None: """Handle a control event from the ISY994 Node.""" event_data = { "entity_id": self.entity_id, @@ -52,7 +58,7 @@ class ISYEntity(Entity): if event.control not in EVENT_PROPS_IGNORED: # New state attributes may be available, update the state. - self.schedule_update_ha_state() + self.async_write_ha_state() self.hass.bus.fire("isy994_control", event_data) @@ -99,9 +105,9 @@ class ISYEntity(Entity): f"ProductTypeID:{node.zwave_props.prod_type_id} " f"ProductID:{node.zwave_props.product_id}" ) - # Note: sw_version is not exposed by the ISY for the individual devices. if hasattr(node, "folder") and node.folder is not None: device_info["suggested_area"] = node.folder + # Note: sw_version is not exposed by the ISY for the individual devices. return device_info @@ -155,25 +161,23 @@ class ISYNodeEntity(ISYEntity): self._attrs.update(attr) return self._attrs - def send_node_command(self, command): + async def async_send_node_command(self, command): """Respond to an entity service command call.""" if not hasattr(self._node, command): - _LOGGER.error( - "Invalid Service Call %s for device %s", command, self.entity_id + raise HomeAssistantError( + f"Invalid service call: {command} for device {self.entity_id}" ) - return - getattr(self._node, command)() + await getattr(self._node, command)() - def send_raw_node_command( + async def async_send_raw_node_command( self, command, value=None, unit_of_measurement=None, parameters=None ): """Respond to an entity service raw command call.""" if not hasattr(self._node, "send_cmd"): - _LOGGER.error( - "Invalid Service Call %s for device %s", command, self.entity_id + raise HomeAssistantError( + f"Invalid service call: {command} for device {self.entity_id}" ) - return - self._node.send_cmd(command, value, unit_of_measurement, parameters) + await self._node.send_cmd(command, value, unit_of_measurement, parameters) class ISYProgramEntity(ISYEntity): diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 5d40eaef2a9..73b5bd683ba 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -65,17 +65,17 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): return None return self._node.status != 0 - def set_percentage(self, percentage: int) -> None: + async def async_set_percentage(self, percentage: int) -> None: """Set node to speed percentage for the ISY994 fan device.""" if percentage == 0: - self._node.turn_off() + await self._node.turn_off() return isy_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) - self._node.turn_on(val=isy_speed) + await self._node.turn_on(val=isy_speed) - def turn_on( + async def async_turn_on( self, speed: str = None, percentage: int = None, @@ -83,11 +83,11 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): **kwargs, ) -> None: """Send the turn on command to the ISY994 fan device.""" - self.set_percentage(percentage) + await self.async_set_percentage(percentage) - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 fan device.""" - self._node.turn_off() + await self._node.turn_off() @property def supported_features(self) -> int: @@ -108,8 +108,6 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - if self._node.protocol == PROTO_INSTEON: - return 3 return int_states_in_range(SPEED_RANGE) @property @@ -117,12 +115,12 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): """Get if the fan is on.""" return self._node.status != 0 - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Send the turn on command to ISY994 fan program.""" - if not self._actions.run_then(): + if not await self._actions.run_then(): _LOGGER.error("Unable to turn off the fan") - def turn_on( + async def async_turn_on( self, speed: str = None, percentage: int = None, @@ -130,5 +128,5 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): **kwargs, ) -> None: """Send the turn off command to ISY994 fan program.""" - if not self._actions.run_else(): + if not await self._actions.run_else(): _LOGGER.error("Unable to turn on the fan") diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 5322c8e0abf..b9b1a71901c 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -41,12 +41,12 @@ from .const import ( KEY_STATUS, NODE_FILTERS, PLATFORMS, + PROGRAM_PLATFORMS, SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, SUBNODE_EZIO2X4_SENSORS, SUBNODE_FANLINC_LIGHT, SUBNODE_IOLINC_RELAY, - SUPPORTED_PROGRAM_PLATFORMS, TYPE_CATEGORY_SENSOR_ACTUATORS, TYPE_EZIO2X4, UOM_DOUBLE_TEMP, @@ -167,7 +167,6 @@ def _check_for_zwave_cat( device_type.startswith(t) for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT]) ): - hass_isy_data[ISY994_NODES][platform].append(node) return True @@ -314,7 +313,7 @@ def _categorize_nodes( def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: """Categorize the ISY994 programs.""" - for platform in SUPPORTED_PROGRAM_PLATFORMS: + for platform in PROGRAM_PLATFORMS: folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}") if not folder: continue diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 73bd2f5934f..509fd259830 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -9,7 +9,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -72,30 +72,32 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): return round(self._node.status * 255.0 / 100.0) return int(self._node.status) - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 light device.""" self._last_brightness = self.brightness - if not self._node.turn_off(): + if not await self._node.turn_off(): _LOGGER.debug("Unable to turn off light") - def on_update(self, event: object) -> None: + @callback + def async_on_update(self, event: object) -> None: """Save brightness in the update event from the ISY994 Node.""" if self._node.status not in (0, ISY_VALUE_UNKNOWN): + self._last_brightness = self._node.status if self._node.uom == UOM_PERCENTAGE: self._last_brightness = round(self._node.status * 255.0 / 100.0) else: self._last_brightness = self._node.status - super().on_update(event) + super().async_on_update(event) # pylint: disable=arguments-differ - def turn_on(self, brightness=None, **kwargs) -> None: + async def async_turn_on(self, brightness=None, **kwargs) -> None: """Send the turn on command to the ISY994 light device.""" if self._restore_light_state and brightness is None and self._last_brightness: brightness = self._last_brightness # Special Case for ISY Z-Wave Devices using % instead of 0-255: if brightness is not None and self._node.uom == UOM_PERCENTAGE: brightness = round(brightness * 100.0 / 255.0) - if not self._node.turn_on(val=brightness): + if not await self._node.turn_on(val=brightness): _LOGGER.debug("Unable to turn on light") @property @@ -125,10 +127,10 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): ): self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS] - def set_on_level(self, value): + async def async_set_on_level(self, value): """Set the ON Level for a device.""" - self._node.set_on_level(value) + await self._node.set_on_level(value) - def set_ramp_rate(self, value): + async def async_set_ramp_rate(self, value): """Set the Ramp Rate for a device.""" - self._node.set_ramp_rate(value) + await self._node.set_ramp_rate(value) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 7e1296d2c86..c00a12d0096 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,4 +1,5 @@ """Support for ISY994 locks.""" + from pyisy.constants import ISY_VALUE_UNKNOWN from homeassistant.components.lock import DOMAIN as LOCK, LockEntity @@ -41,14 +42,14 @@ class ISYLockEntity(ISYNodeEntity, LockEntity): return None return VALUE_TO_STATE.get(self._node.status) - def lock(self, **kwargs) -> None: + async def async_lock(self, **kwargs) -> None: """Send the lock command to the ISY994 device.""" - if not self._node.secure_lock(): + if not await self._node.secure_lock(): _LOGGER.error("Unable to lock device") - def unlock(self, **kwargs) -> None: + async def async_unlock(self, **kwargs) -> None: """Send the unlock command to the ISY994 device.""" - if not self._node.secure_unlock(): + if not await self._node.secure_unlock(): _LOGGER.error("Unable to lock device") @@ -60,12 +61,12 @@ class ISYLockProgramEntity(ISYProgramEntity, LockEntity): """Return true if the device is locked.""" return bool(self._node.status) - def lock(self, **kwargs) -> None: + async def async_lock(self, **kwargs) -> None: """Lock the device.""" - if not self._actions.run_then(): + if not await self._actions.run_then(): _LOGGER.error("Unable to lock device") - def unlock(self, **kwargs) -> None: + async def async_unlock(self, **kwargs) -> None: """Unlock the device.""" - if not self._actions.run_else(): + if not await self._actions.run_else(): _LOGGER.error("Unable to unlock device") diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index a2326cbae5f..84f13ae4fc4 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -2,7 +2,7 @@ "domain": "isy994", "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==2.1.1"], + "requirements": ["pyisy==3.0.0"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ @@ -11,8 +11,6 @@ "deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1" } ], - "dhcp": [ - {"hostname":"isy*", "macaddress":"0021B9*"} - ], + "dhcp": [{ "hostname": "isy*", "macaddress": "0021B9*" }], "iot_class": "local_push" } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 908bbdc72e8..79c5663f964 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -83,6 +83,10 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): if uom in [UOM_INDEX, UOM_ON_OFF]: return self._node.formatted + # Check if this is an index type and get formatted value + if uom == UOM_INDEX and hasattr(self._node, "formatted"): + return self._node.formatted + # Handle ISY precision and rounding value = convert_isy_value_to_hass(value, uom, self._node.prec) @@ -123,7 +127,8 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity): return { "init_value": convert_isy_value_to_hass( self._node.init, "", self._node.prec - ) + ), + "last_edited": self._node.last_edited, } @property diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 6d93b53b912..03ecc3930bb 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -27,7 +27,7 @@ from .const import ( ISY994_PROGRAMS, ISY994_VARIABLES, PLATFORMS, - SUPPORTED_PROGRAM_PLATFORMS, + PROGRAM_PLATFORMS, ) # Common Services for All Platforms: @@ -183,12 +183,12 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 address, isy.configuration["uuid"], ) - await hass.async_add_executor_job(isy.query, address) + await isy.query(address) return _LOGGER.debug( "Requesting system query of ISY %s", isy.configuration["uuid"] ) - await hass.async_add_executor_job(isy.query) + await isy.query() async def async_run_network_resource_service_handler(service): """Handle a network resource service call.""" @@ -208,10 +208,10 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 if name: command = isy.networking.get_by_name(name) if command is not None: - await hass.async_add_executor_job(command.run) + await command.run() return _LOGGER.error( - "Could not run network resource command. Not found or enabled on the ISY" + "Could not run network resource command; not found or enabled on the ISY" ) async def async_send_program_command_service_handler(service): @@ -231,9 +231,9 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 if name: program = isy.programs.get_by_name(name) if program is not None: - await hass.async_add_executor_job(getattr(program, command)) + await getattr(program, command)() return - _LOGGER.error("Could not send program command. Not found or enabled on the ISY") + _LOGGER.error("Could not send program command; not found or enabled on the ISY") async def async_set_variable_service_handler(service): """Handle a set variable service call.""" @@ -254,9 +254,9 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 if address and vtype: variable = isy.variables.vobjs[vtype].get(address) if variable is not None: - await hass.async_add_executor_job(variable.set_value, value, init) + await variable.set_value(value, init) return - _LOGGER.error("Could not set variable value. Not found or enabled on the ISY") + _LOGGER.error("Could not set variable value; not found or enabled on the ISY") async def async_cleanup_registry_entries(service) -> None: """Remove extra entities that are no longer part of the integration.""" @@ -283,7 +283,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 if hasattr(node, "address"): current_unique_ids.append(f"{uuid}_{node.address}") - for platform in SUPPORTED_PROGRAM_PLATFORMS: + for platform in PROGRAM_PLATFORMS: for _, node, _ in hass_isy_data[ISY994_PROGRAMS][platform]: if hasattr(node, "address"): current_unique_ids.append(f"{uuid}_{node.address}") @@ -355,7 +355,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 async def _async_send_raw_node_command(call: ServiceCall): await hass.helpers.service.entity_service_call( - async_get_platforms(hass, DOMAIN), SERVICE_SEND_RAW_NODE_COMMAND, call + async_get_platforms(hass, DOMAIN), "async_send_raw_node_command", call ) hass.services.async_register( @@ -367,7 +367,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 async def _async_send_node_command(call: ServiceCall): await hass.helpers.service.entity_service_call( - async_get_platforms(hass, DOMAIN), SERVICE_SEND_NODE_COMMAND, call + async_get_platforms(hass, DOMAIN), "async_send_node_command", call ) hass.services.async_register( @@ -408,8 +408,8 @@ def async_setup_light_services(hass: HomeAssistant): platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( - SERVICE_SET_ON_LEVEL, SERVICE_SET_VALUE_SCHEMA, SERVICE_SET_ON_LEVEL + SERVICE_SET_ON_LEVEL, SERVICE_SET_VALUE_SCHEMA, "async_set_on_level" ) platform.async_register_entity_service( - SERVICE_SET_RAMP_RATE, SERVICE_SET_RAMP_RATE_SCHEMA, SERVICE_SET_RAMP_RATE + SERVICE_SET_RAMP_RATE, SERVICE_SET_RAMP_RATE_SCHEMA, "async_set_ramp_rate" ) diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 94d5a3cd89d..c163d78a173 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -57,19 +57,19 @@ send_node_command: selector: select: options: - - 'beep' - - 'brighten' - - 'dim' - - 'disable' - - 'enable' - - 'fade_down' - - 'fade_stop' - - 'fade_up' - - 'fast_off' - - 'fast_on' - - 'query' + - "beep" + - "brighten" + - "dim" + - "disable" + - "enable" + - "fade_down" + - "fade_stop" + - "fade_up" + - "fast_off" + - "fast_on" + - "query" set_on_level: - name: Set on level + name: Set On Level description: Send a ISY set_on_level command to a Node. target: entity: @@ -188,14 +188,14 @@ send_program_command: selector: select: options: - - 'disable' - - 'disable_run_at_startup' - - 'enable' - - 'enable_run_at_startup' - - 'run' - - 'run_else' - - 'run_then' - - 'stop' + - "disable" + - "disable_run_at_startup" + - "enable" + - "enable_run_at_startup" + - "run" + - "run_else" + - "run_then" + - "stop" isy: name: ISY description: If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same program name or address on multiple ISYs, omitting this will run the command on them all. diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 53056e45c7e..99bf6566b1b 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,4 +1,5 @@ """Support for ISY994 switches.""" + from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity @@ -39,14 +40,14 @@ class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): return None return bool(self._node.status) - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 switch.""" - if not self._node.turn_off(): + if not await self._node.turn_off(): _LOGGER.debug("Unable to turn off switch") - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Send the turn on command to the ISY994 switch.""" - if not self._node.turn_on(): + if not await self._node.turn_on(): _LOGGER.debug("Unable to turn on switch") @property @@ -65,14 +66,14 @@ class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity): """Get whether the ISY994 switch program is on.""" return bool(self._node.status) - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Send the turn on command to the ISY994 switch program.""" - if not self._actions.run_then(): + if not await self._actions.run_then(): _LOGGER.error("Unable to turn on switch") - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 switch program.""" - if not self._actions.run_else(): + if not await self._actions.run_else(): _LOGGER.error("Unable to turn off switch") @property diff --git a/requirements_all.txt b/requirements_all.txt index 120926d5123..db8026faffd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1491,7 +1491,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==2.1.1 +pyisy==3.0.0 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2894eea7c2d..dcca9bcb33f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -826,7 +826,7 @@ pyipp==0.11.0 pyiqvia==0.3.1 # homeassistant.components.isy994 -pyisy==2.1.1 +pyisy==3.0.0 # homeassistant.components.kira pykira==0.1.1 diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 51750d42718..e5458a3c96b 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Universal Devices ISY994 config flow.""" - +import re from unittest.mock import patch +from pyisy import ISYConnectionError, ISYInvalidAuthError + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import dhcp, ssdp -from homeassistant.components.isy994.config_flow import CannotConnect from homeassistant.components.isy994.const import ( CONF_IGNORE_STRING, CONF_RESTORE_LIGHT_STATE, @@ -63,12 +64,30 @@ MOCK_IMPORT_FULL_CONFIG = { MOCK_DEVICE_NAME = "Name of the device" MOCK_UUID = "ce:fb:72:31:b7:b9" MOCK_MAC = "cefb7231b7b9" -MOCK_VALIDATED_RESPONSE = {"name": MOCK_DEVICE_NAME, "uuid": MOCK_UUID} -PATCH_CONFIGURATION = "homeassistant.components.isy994.config_flow.Configuration" -PATCH_CONNECTION = "homeassistant.components.isy994.config_flow.Connection" -PATCH_ASYNC_SETUP = "homeassistant.components.isy994.async_setup" -PATCH_ASYNC_SETUP_ENTRY = "homeassistant.components.isy994.async_setup_entry" +MOCK_CONFIG_RESPONSE = """ + + 5.0.16C + ISY-C-994 + + ce:fb:72:31:b7:b9 + Name of the device + + + + 21040 + Networking Module + true + true + + + +""" + +INTEGRATION = "homeassistant.components.isy994" +PATCH_CONNECTION = f"{INTEGRATION}.config_flow.Connection.test_connection" +PATCH_ASYNC_SETUP = f"{INTEGRATION}.async_setup" +PATCH_ASYNC_SETUP_ENTRY = f"{INTEGRATION}.async_setup_entry" async def test_form(hass: HomeAssistant): @@ -80,17 +99,12 @@ async def test_form(hass: HomeAssistant): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( PATCH_ASYNC_SETUP, return_value=True ) as mock_setup, patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ) as mock_setup_entry: - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -129,9 +143,9 @@ async def test_form_invalid_auth(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(PATCH_CONFIGURATION), patch( + with patch( PATCH_CONNECTION, - side_effect=ValueError("PyISY could not connect to the ISY."), + side_effect=ISYInvalidAuthError(), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -142,14 +156,52 @@ async def test_form_invalid_auth(hass: HomeAssistant): assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass: HomeAssistant): - """Test we handle cannot connect error.""" +async def test_form_isy_connection_error(hass: HomeAssistant): + """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(PATCH_CONFIGURATION), patch( + with patch( PATCH_CONNECTION, - side_effect=CannotConnect, + side_effect=ISYConnectionError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_isy_parse_response_error(hass: HomeAssistant, caplog): + """Test we handle poorly formatted XML response from ISY.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + PATCH_CONNECTION, + return_value=MOCK_CONFIG_RESPONSE.rsplit("\n", 3)[0], # Test with invalid XML + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert "ISY Could not parse response, poorly formatted XML." in caplog.text + + +async def test_form_no_name_in_response(hass: HomeAssistant): + """Test we handle invalid response from ISY with name not set.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + PATCH_CONNECTION, + return_value=re.sub( + r"\.*\n", "", MOCK_CONFIG_RESPONSE + ), # Test with line removed. ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -170,12 +222,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class: - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -185,15 +232,12 @@ async def test_form_existing_config_entry(hass: HomeAssistant): async def test_import_flow_some_fields(hass: HomeAssistant) -> None: """Test import config flow with just the basic fields.""" - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ), patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ): - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -209,15 +253,12 @@ async def test_import_flow_some_fields(hass: HomeAssistant) -> None: async def test_import_flow_with_https(hass: HomeAssistant) -> None: """Test import config with https.""" - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ), patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ): - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -232,15 +273,12 @@ async def test_import_flow_with_https(hass: HomeAssistant) -> None: async def test_import_flow_all_fields(hass: HomeAssistant) -> None: """Test import config flow with all fields.""" - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ), patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ): - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -297,17 +335,12 @@ async def test_form_ssdp(hass: HomeAssistant): assert result["step_id"] == "user" assert result["errors"] == {} - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( PATCH_ASYNC_SETUP, return_value=True ) as mock_setup, patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ) as mock_setup_entry: - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -339,17 +372,12 @@ async def test_form_dhcp(hass: HomeAssistant): assert result["step_id"] == "user" assert result["errors"] == {} - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( PATCH_ASYNC_SETUP, return_value=True ) as mock_setup, patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ) as mock_setup_entry: - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, From 2cd2e46d73875b97e2f4dbddd5d92fbb81b092d5 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 18 May 2021 22:16:49 +0200 Subject: [PATCH 553/852] Disable AVM FRITZ!Box Tools device_tracker entities by default (#50791) --- homeassistant/components/fritz/device_tracker.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index d228d71ec10..c32afcdfc26 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -181,6 +181,11 @@ class FritzBoxTracker(ScannerEntity): return "mdi:lan-connect" return "mdi:lan-disconnect" + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False + @callback def async_process_update(self) -> None: """Update device.""" From c890966ce4e60094c7125a2399161be852eef620 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 18 May 2021 15:54:14 -0700 Subject: [PATCH 554/852] Updated frontend to 20210518.0 (#50842) --- 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 d7cecbe163a..73743f21d9d 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==20210517.0" + "home-assistant-frontend==20210518.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9755a5b2e5d..ebfdd4bbfe5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210517.0 +home-assistant-frontend==20210518.0 httpx==0.18.0 jinja2>=2.11.3 netdisco==2.8.3 diff --git a/requirements_all.txt b/requirements_all.txt index db8026faffd..18be73d5ed1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -768,7 +768,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210517.0 +home-assistant-frontend==20210518.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcca9bcb33f..5541ec10f7f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -432,7 +432,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210517.0 +home-assistant-frontend==20210518.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 0a49de75d962c0f0a7f920ea0e9c5e5e1f45b82d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 19 May 2021 00:11:31 +0000 Subject: [PATCH 555/852] [ci skip] Translation update --- .../components/aemet/translations/es.json | 9 +++++ .../components/aemet/translations/no.json | 9 +++++ .../components/bosch_shc/translations/no.json | 38 +++++++++++++++++++ .../components/cast/translations/no.json | 2 +- .../components/demo/translations/nl.json | 4 +- .../components/fritz/translations/no.json | 11 ++++++ .../garages_amsterdam/translations/no.json | 18 +++++++++ .../components/goalzero/translations/ca.json | 3 +- .../components/goalzero/translations/no.json | 10 ++++- .../growatt_server/translations/no.json | 2 +- .../components/kraken/translations/nl.json | 11 +++++- .../components/kraken/translations/no.json | 22 +++++++++++ .../components/nexia/translations/no.json | 1 + .../components/upnp/translations/no.json | 10 +++++ 14 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/bosch_shc/translations/no.json create mode 100644 homeassistant/components/garages_amsterdam/translations/no.json create mode 100644 homeassistant/components/kraken/translations/no.json diff --git a/homeassistant/components/aemet/translations/es.json b/homeassistant/components/aemet/translations/es.json index ffe4d524754..558d87886d9 100644 --- a/homeassistant/components/aemet/translations/es.json +++ b/homeassistant/components/aemet/translations/es.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Obtener datos de las estaciones meteorol\u00f3gicas de AEMET" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/no.json b/homeassistant/components/aemet/translations/no.json index 48cbc9916ca..fe36ff835ee 100644 --- a/homeassistant/components/aemet/translations/no.json +++ b/homeassistant/components/aemet/translations/no.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Samle inn data fra AEMET v\u00e6rstasjoner" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/no.json b/homeassistant/components/bosch_shc/translations/no.json new file mode 100644 index 00000000000..53d64519fd5 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/no.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "pairing_failed": "Paringen mislyktes. Kontroller at Bosch Smart Home Controller er i sammenkoblingsmodus (LED blinker) s\u00e5 vel som passordet ditt er riktig.", + "session_error": "\u00d8ktfeil: API returnerer ikke OK-resultat.", + "unknown": "Uventet feil" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Trykk p\u00e5 Bosch Smart Home Controller-frontknappen til LED-lampen begynner \u00e5 blinke.\n Klar til \u00e5 fortsette \u00e5 konfigurere {model} @ {host} med Home Assistant?" + }, + "credentials": { + "data": { + "password": "Passordet til Smart Home Controller" + } + }, + "reauth_confirm": { + "description": "Bosch_shc-integrasjonen m\u00e5 godkjenne kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Sett opp din Bosch Smart Home Controller for \u00e5 tillate overv\u00e5king og kontroll med Home Assistant.", + "title": "SHC-autentiseringsparametere" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/no.json b/homeassistant/components/cast/translations/no.json index 60fa7fae3f1..6c8d7cb5760 100644 --- a/homeassistant/components/cast/translations/no.json +++ b/homeassistant/components/cast/translations/no.json @@ -29,7 +29,7 @@ "ignore_cec": "Ignorer CEC", "uuid": "Tillatte UUIDer" }, - "description": "Tillatte UUID-er - En komma-separert liste over UUID-er for Cast-enheter \u00e5 legge til i Home Assistant. Bruk bare hvis du ikke vil legge til alle tilgjengelige cast-enheter.\n Ignorer CEC - En kommaseparert liste over Chromecasts som b\u00f8r ignorere CEC-data for \u00e5 bestemme den aktive inngangen. Dette vil bli sendt til pychromecast.IGNORE_CEC.", + "description": "Tillatte UUIDer - En kommadelt liste over UUIDer med Cast-enheter som skal legges til i Home Assistant. Bruk bare hvis du ikke vil legge til alle tilgjengelige cast-enheter.\nIgnorer CEC - En kommadelt liste over Chromecaster som b\u00f8r ignorere CEC-data for \u00e5 bestemme de aktive inngangene. Dette vil bli sendt til pychromecast. IGNORE_CEC.", "title": "Avansert Google Cast-konfigurasjon" }, "basic_options": { diff --git a/homeassistant/components/demo/translations/nl.json b/homeassistant/components/demo/translations/nl.json index 8e7c97f7c3f..37b23b2aaac 100644 --- a/homeassistant/components/demo/translations/nl.json +++ b/homeassistant/components/demo/translations/nl.json @@ -3,8 +3,8 @@ "step": { "init": { "data": { - "one": "Empty", - "other": "" + "one": "Leeg", + "other": "Leeg" } }, "options_1": { diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json index 44bb6d297cb..47c51349aca 100644 --- a/homeassistant/components/fritz/translations/no.json +++ b/homeassistant/components/fritz/translations/no.json @@ -8,6 +8,7 @@ "error": { "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes", "connection_error": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, @@ -38,6 +39,16 @@ }, "description": "Sett opp FRITZ!Box verkt\u00f8y for \u00e5 kontrollere fritz! Boksen.\nMinimum n\u00f8dvendig: brukernavn, passord.", "title": "Sett opp FRITZ!Box verkt\u00f8y - obligatorisk" + }, + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "description": "Sett opp FRITZ!Box verkt\u00f8y for \u00e5 kontrollere fritz! Boksen.\nMinimum n\u00f8dvendig: brukernavn, passord.", + "title": "Sett opp FRITZ!Box verkt\u00f8y" } } } diff --git a/homeassistant/components/garages_amsterdam/translations/no.json b/homeassistant/components/garages_amsterdam/translations/no.json new file mode 100644 index 00000000000..d93564e3f18 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "garage_name": "Garasjens navn" + }, + "title": "Velg en garasje \u00e5 overv\u00e5ke" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json index cc7dbb0ab85..0f9979eb378 100644 --- a/homeassistant/components/goalzero/translations/ca.json +++ b/homeassistant/components/goalzero/translations/ca.json @@ -12,6 +12,7 @@ }, "step": { "confirm_discovery": { + "description": "Es recomana que la reserva DHCP del router estigui configurada. Si no ho est\u00e0, pot ser que el dispositiu no estigui disponible mentre Home Assistant no detecti la nova IP. Consulta el manual del router.", "title": "Goal Zero Yeti" }, "user": { @@ -19,7 +20,7 @@ "host": "Amfitri\u00f3", "name": "Nom" }, - "description": "En primer lloc, has de baixar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el Yeti a la xarxa Wifi. Cal que la reserva DHCP del router estigui configurada per al teu dispositiu per garantir que la IP no canvi\u00ef. Si cal, consulta el manual del router.", + "description": "En primer lloc, has de descarregar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el Yeti al teu Wi-Fi. Es recomana que la reserva DHCP del router estigui configurada, si no ho est\u00e0, pot ser que el dispositiu no estigui disponible mentre Home Assistant no detecti la nova IP. Consulta el manual del router.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/no.json b/homeassistant/components/goalzero/translations/no.json index 4dfeadfcf6d..1bc2d6feae1 100644 --- a/homeassistant/components/goalzero/translations/no.json +++ b/homeassistant/components/goalzero/translations/no.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse", + "unknown": "Uventet feil" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -9,12 +11,16 @@ "unknown": "Uventet feil" }, "step": { + "confirm_discovery": { + "description": "DHCP-reservasjon p\u00e5 ruteren din anbefales. Hvis den ikke er konfigurert, kan enheten bli utilgjengelig til Home Assistant oppdager den nye ip-adressen. Se i brukerh\u00e5ndboken til ruteren.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Vert", "name": "Navn" }, - "description": "F\u00f8rst m\u00e5 du laste ned appen Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n F\u00f8lg instruksjonene for \u00e5 koble Yeti til Wifi-nettverket. DHCP-reservasjon m\u00e5 v\u00e6re satt opp i ruteren innstillinger for enheten for \u00e5 sikre at verts-IP ikke endres. Se i brukerh\u00e5ndboken til ruteren.", + "description": "F\u00f8rst m\u00e5 du laste ned Goal Zero-appen: https://www.goalzero.com/product-features/yeti-app/\n\nF\u00f8lg instruksjonene for \u00e5 koble Yeti til Wi-Fi-nettverket ditt. DHCP-reservasjon p\u00e5 ruteren anbefales. Hvis den ikke er konfigurert, kan enheten bli utilgjengelig til Home Assistant oppdager den nye IP-adressen. Se brukerh\u00e5ndboken for ruteren.", "title": "" } } diff --git a/homeassistant/components/growatt_server/translations/no.json b/homeassistant/components/growatt_server/translations/no.json index 03f2d6ee82d..dee1e989465 100644 --- a/homeassistant/components/growatt_server/translations/no.json +++ b/homeassistant/components/growatt_server/translations/no.json @@ -16,7 +16,7 @@ "user": { "data": { "name": "Navn", - "password": "Navn", + "password": "Passord", "username": "Brukernavn" }, "title": "Skriv inn Growatt-informasjonen din" diff --git a/homeassistant/components/kraken/translations/nl.json b/homeassistant/components/kraken/translations/nl.json index 8e63d5b5373..7de89d6b2dc 100644 --- a/homeassistant/components/kraken/translations/nl.json +++ b/homeassistant/components/kraken/translations/nl.json @@ -3,8 +3,16 @@ "abort": { "already_configured": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, + "error": { + "one": "Leeg", + "other": "Leeg" + }, "step": { "user": { + "data": { + "one": "Leeg", + "other": "Leeg" + }, "description": "Wil je beginnen met instellen?" } } @@ -13,7 +21,8 @@ "step": { "init": { "data": { - "scan_interval": "Update-interval" + "scan_interval": "Update-interval", + "tracked_asset_pairs": "Bijgehouden activaparen" } } } diff --git a/homeassistant/components/kraken/translations/no.json b/homeassistant/components/kraken/translations/no.json new file mode 100644 index 00000000000..a8f3b2cec2a --- /dev/null +++ b/homeassistant/components/kraken/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "user": { + "description": "Vil du starte oppsettet?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdateringsintervall", + "tracked_asset_pairs": "Sporede aktivapar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/no.json b/homeassistant/components/nexia/translations/no.json index a3f143898f8..4533b94e48e 100644 --- a/homeassistant/components/nexia/translations/no.json +++ b/homeassistant/components/nexia/translations/no.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Merke", "password": "Passord", "username": "Brukernavn" }, diff --git a/homeassistant/components/upnp/translations/no.json b/homeassistant/components/upnp/translations/no.json index f3875a2c3ef..c92144bf40d 100644 --- a/homeassistant/components/upnp/translations/no.json +++ b/homeassistant/components/upnp/translations/no.json @@ -21,9 +21,19 @@ "user": { "data": { "scan_interval": "Oppdateringsintervall (sekunder, minimum 30)", + "unique_id": "Enhet", "usn": "Enhet" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdateringsintervall (sekunder, minimum 30)" + } + } + } } } \ No newline at end of file From 26a99df0ea775e47beac95db7124f1c032945244 Mon Sep 17 00:00:00 2001 From: RogerSelwyn Date: Wed, 19 May 2021 01:33:37 +0100 Subject: [PATCH 556/852] Capture error when speedtest module fails to identify best server (#50821) * Capture error when speediest module fails to identify best server * Fix pylint error * Fix formatting with black. Co-authored-by: Rohan Kapoor --- .../components/speedtestdotnet/__init__.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index b8f80de1b52..2c906448510 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -165,14 +165,20 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): server_id = self.config_entry.options.get(CONF_SERVER_ID) self.api.get_servers(servers=[server_id]) - self.api.get_best_server() - _LOGGER.debug( - "Executing speedtest.net speed test with server_id: %s", self.api.best["id"] - ) + try: + self.api.get_best_server() + _LOGGER.debug( + "Executing speedtest.net speed test with server_id: %s", + self.api.best["id"], + ) - self.api.download() - self.api.upload() - return self.api.results.dict() + self.api.download() + self.api.upload() + return self.api.results.dict() + except speedtest.SpeedtestBestServerFailure as err: + raise UpdateFailed( + "Failed to retrieve best server for speedtest", err + ) from err async def async_update(self, *_): """Update Speedtest data.""" From 3d5b354defe1104fcc708de6cc4600a5b369c6a9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 18 May 2021 20:31:38 -0500 Subject: [PATCH 557/852] Bump pysonos to 0.0.49 (#50841) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index af3d79f96d6..7bd9efeda16 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.48"], + "requirements": ["pysonos==0.0.49"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 18be73d5ed1..6187e96bede 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1753,7 +1753,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.48 +pysonos==0.0.49 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5541ec10f7f..629f043f309 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.48 +pysonos==0.0.49 # homeassistant.components.spc pyspcwebgw==0.4.0 From ab86c7a135a25dbd7a94b46490fa1e3ce7a19b08 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 18 May 2021 22:15:16 -0500 Subject: [PATCH 558/852] Clean up Sonos resubscription failure logic and logging (#50831) --- homeassistant/components/sonos/speaker.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index e647fc2fd68..eb4e194403e 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -136,6 +136,7 @@ class SonosSpeaker: self._is_ready: bool = False self._subscriptions: list[SubscriptionBase] = [] + self._resubscription_lock: asyncio.Lock | None = None self._poll_timer: Callable | None = None self._seen_timer: Callable | None = None self._platforms_ready: set[str] = set() @@ -198,6 +199,7 @@ class SonosSpeaker: """Listen to new entities to trigger first subscription.""" self._platforms_ready.add(entity_type) if self._platforms_ready == PLATFORMS: + self._resubscription_lock = asyncio.Lock() await self.async_subscribe() self._is_ready = True @@ -313,11 +315,27 @@ class SonosSpeaker: self.async_write_entity_states() + async def async_resubscribe(self, exception: Exception) -> None: + """Attempt to resubscribe when a renewal failure is detected.""" + async with self._resubscription_lock: + if self.available: + if getattr(exception, "status", None) == 412: + _LOGGER.warning( + "Subscriptions for %s failed, speaker may have lost power", + self.zone_name, + ) + else: + _LOGGER.error( + "Subscription renewals for %s failed", + self.zone_name, + exc_info=exception, + ) + await self.async_unseen() + @callback def async_renew_failed(self, exception: Exception) -> None: """Handle a failed subscription renewal.""" - if self.available: - self.hass.async_add_job(self.async_unseen) + self.hass.async_create_task(self.async_resubscribe(exception)) async def async_unseen(self, now: datetime.datetime | None = None) -> None: """Make this player unavailable when it was not seen recently.""" From a58eae1bf1f83d0321196b8e972b92da1c87dacc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 19 May 2021 05:32:11 +0200 Subject: [PATCH 559/852] Bump brother library version (#50833) --- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 2555490721e..0365918a78b 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==1.0.1"], + "requirements": ["brother==1.0.2"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 6187e96bede..47ffdf99650 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ bravia-tv==1.0.11 broadlink==0.17.0 # homeassistant.components.brother -brother==1.0.1 +brother==1.0.2 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 629f043f309..7fa2ea4fdf8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -229,7 +229,7 @@ bravia-tv==1.0.11 broadlink==0.17.0 # homeassistant.components.brother -brother==1.0.1 +brother==1.0.2 # homeassistant.components.bsblan bsblan==0.4.0 From e37256570c1ed9b8579a665363a231544c924411 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 May 2021 22:49:10 -0500 Subject: [PATCH 560/852] Add missing return type in zeroconf (#50847) --- homeassistant/components/zeroconf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index ceaba3f02a1..192ba5ac97e 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -279,7 +279,7 @@ async def _async_register_hass_zc_service( class FlowDispatcher: """Dispatch discovery flows.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Init the discovery dispatcher.""" self.hass = hass self.pending_flows: list[ZeroconfFlow] = [] From f1d02bb137b3aab0287571775b5585896e235621 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 May 2021 00:20:56 -0500 Subject: [PATCH 561/852] Expand homekit zeroconf matching to use fnmatch (#50381) --- homeassistant/components/zeroconf/__init__.py | 4 +-- tests/components/zeroconf/test_init.py | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 192ba5ac97e..dba0c0f6aa8 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -467,8 +467,8 @@ def handle_homekit( for test_model in homekit_models: if ( model != test_model - and not model.startswith(f"{test_model} ") - and not model.startswith(f"{test_model}-") + and not model.startswith((f"{test_model} ", f"{test_model}-")) + and not fnmatch.fnmatch(model, test_model) ): continue diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index d8a9a94da96..2f66404f27b 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -488,6 +488,33 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "rachio" +async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf): + """Test matching homekit devices with fnmatch.""" + with patch.dict( + zc_gen.ZEROCONF, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), patch.dict(zc_gen.HOMEKIT, {"YLDP*": "yeelight"}, clear=True,), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("YLDP13YL", HOMEKIT_STATUS_UNPAIRED), + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "yeelight" + + async def test_homekit_match_full(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( From a2363f024397f72db847cdfc63567d246bc96733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 19 May 2021 09:24:04 +0300 Subject: [PATCH 562/852] Upgrade huawei-lte-api to 1.4.18 (#50828) https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.18 Closes https://github.com/home-assistant/core/issues/50777 --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index f48206a4802..5d3c15f634a 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.2", - "huawei-lte-api==1.4.17", + "huawei-lte-api==1.4.18", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index 47ffdf99650..072bb2e9ed9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -790,7 +790,7 @@ horimote==0.4.1 httplib2==0.19.0 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.17 +huawei-lte-api==1.4.18 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7fa2ea4fdf8..bf2a148f824 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -451,7 +451,7 @@ homepluscontrol==0.0.5 httplib2==0.19.0 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.17 +huawei-lte-api==1.4.18 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 From 3ed416ed4c51482ed09669031316c6c866976172 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 19 May 2021 08:47:06 +0200 Subject: [PATCH 563/852] Bump pyatmo to 4.2.3 (#50801) * Bump pyatmo to 4.2.3 * Fix typo and update test fixture --- homeassistant/components/netatmo/climate.py | 4 +- .../components/netatmo/manifest.json | 26 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/netatmo/test_climate.py | 2 +- tests/fixtures/netatmo/homesdata.json | 167 +----------------- 6 files changed, 25 insertions(+), 178 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 763ecdbc8ef..2dee518db59 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -578,9 +578,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): schedule_id = sid if not schedule_id: - _LOGGER.error( - "%s is not a invalid schedule", kwargs.get(ATTR_SCHEDULE_NAME) - ) + _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) return self._data.switch_home_schedule(home_id=self._home_id, schedule_id=schedule_id) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index bd33efb6ea1..090bc3dd9d6 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,13 +2,27 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==4.2.2"], - "after_dependencies": ["cloud", "media_source"], - "dependencies": ["webhook"], - "codeowners": ["@cgtobi"], + "requirements": [ + "pyatmo==4.2.3" + ], + "after_dependencies": [ + "cloud", + "media_source" + ], + "dependencies": [ + "webhook" + ], + "codeowners": [ + "@cgtobi" + ], "config_flow": true, "homekit": { - "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"] + "models": [ + "Healty Home Coach", + "Netatmo Relay", + "Presence", + "Welcome" + ] }, "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 072bb2e9ed9..7a09974804f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1295,7 +1295,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==4.2.2 +pyatmo==4.2.3 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf2a148f824..567f9ca54a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -720,7 +720,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==4.2.2 +pyatmo==4.2.3 # homeassistant.components.apple_tv pyatv==0.7.7 diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index a3cad1e6d81..16359c85498 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -432,7 +432,7 @@ async def test_service_schedule_thermostats(hass, climate_entry, caplog): await hass.async_block_till_done() mock_switch_home_schedule.assert_not_called() - assert "summer is not a invalid schedule" in caplog.text + assert "summer is not a valid schedule" in caplog.text async def test_service_preset_mode_already_boost_valves(hass, climate_entry): diff --git a/tests/fixtures/netatmo/homesdata.json b/tests/fixtures/netatmo/homesdata.json index aecab91550c..9c5e985218f 100644 --- a/tests/fixtures/netatmo/homesdata.json +++ b/tests/fixtures/netatmo/homesdata.json @@ -89,7 +89,7 @@ "room_id": "3688132631" } ], - "therm_schedules": [ + "schedules": [ { "zones": [ { @@ -398,171 +398,6 @@ "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" } ], - "schedules": [ - { - "zones": [ - { - "type": 0, - "name": "Komfort", - "rooms_temp": [ - { - "temp": 21, - "room_id": "2746182631" - } - ], - "id": 0, - "rooms": [ - { - "id": "2746182631", - "therm_setpoint_temperature": 21 - } - ] - }, - { - "type": 1, - "name": "Nacht", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 1, - "rooms": [ - { - "id": "2746182631", - "therm_setpoint_temperature": 17 - } - ] - }, - { - "type": 5, - "name": "Eco", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 4, - "rooms": [ - { - "id": "2746182631", - "therm_setpoint_temperature": 17 - } - ] - } - ], - "timetable": [ - { - "zone_id": 1, - "m_offset": 0 - }, - { - "zone_id": 0, - "m_offset": 360 - }, - { - "zone_id": 4, - "m_offset": 420 - }, - { - "zone_id": 0, - "m_offset": 960 - }, - { - "zone_id": 1, - "m_offset": 1410 - }, - { - "zone_id": 0, - "m_offset": 1800 - }, - { - "zone_id": 4, - "m_offset": 1860 - }, - { - "zone_id": 0, - "m_offset": 2400 - }, - { - "zone_id": 1, - "m_offset": 2850 - }, - { - "zone_id": 0, - "m_offset": 3240 - }, - { - "zone_id": 4, - "m_offset": 3300 - }, - { - "zone_id": 0, - "m_offset": 3840 - }, - { - "zone_id": 1, - "m_offset": 4290 - }, - { - "zone_id": 0, - "m_offset": 4680 - }, - { - "zone_id": 4, - "m_offset": 4740 - }, - { - "zone_id": 0, - "m_offset": 5280 - }, - { - "zone_id": 1, - "m_offset": 5730 - }, - { - "zone_id": 0, - "m_offset": 6120 - }, - { - "zone_id": 4, - "m_offset": 6180 - }, - { - "zone_id": 0, - "m_offset": 6720 - }, - { - "zone_id": 1, - "m_offset": 7170 - }, - { - "zone_id": 0, - "m_offset": 7620 - }, - { - "zone_id": 1, - "m_offset": 8610 - }, - { - "zone_id": 0, - "m_offset": 9060 - }, - { - "zone_id": 1, - "m_offset": 10050 - } - ], - "hg_temp": 7, - "away_temp": 14, - "name": "Default", - "id": "591b54a2764ff4d50d8b5795", - "selected": true, - "type": "therm" - } - ], "therm_mode": "schedule" }, { From ebe1059c34b3a7dd5f92a997a7927793544b9ffb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 May 2021 10:09:47 +0200 Subject: [PATCH 564/852] Move SolarEdge API init and add unload (#50823) * SolarEdge: Move API init, add unload * Slim down try-except block --- .../components/solaredge/__init__.py | 44 +++++++++++++++--- homeassistant/components/solaredge/const.py | 7 ++- homeassistant/components/solaredge/sensor.py | 46 ++++++------------- 3 files changed, 57 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 424371e9002..df38756cd69 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -1,18 +1,50 @@ -"""The solaredge integration.""" +"""The SolarEdge integration.""" from __future__ import annotations +from requests.exceptions import ConnectTimeout, HTTPError +from solaredge import Solaredge + from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from .const import DOMAIN +from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, LOGGER CONFIG_SCHEMA = cv.deprecated(DOMAIN) +PLATFORMS = ["sensor"] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Load the saved entities.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + """Set up SolarEdge from a config entry.""" + api = Solaredge(entry.data[CONF_API_KEY]) + + try: + response = await hass.async_add_executor_job( + api.get_details, entry.data[CONF_SITE_ID] + ) + except KeyError as ex: + LOGGER.error("Missing details data in SolarEdge response") + raise ConfigEntryNotReady from ex + except (ConnectTimeout, HTTPError) as ex: + LOGGER.error("Could not retrieve details from SolarEdge API") + raise ConfigEntryNotReady from ex + + if response["details"]["status"].lower() != "active": + LOGGER.error("SolarEdge site is not active") + return False + LOGGER.debug("Credentials correct and site is active") + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_API_CLIENT: api} + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload SolarEdge config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 6b4c0bc233f..258eafff304 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -1,13 +1,17 @@ """Constants for the SolarEdge Monitoring API.""" from datetime import timedelta +import logging from homeassistant.const import ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT DOMAIN = "solaredge" +LOGGER = logging.getLogger(__package__) + +DATA_API_CLIENT = "api_client" + # Config for solaredge monitoring api requests. CONF_SITE_ID = "site_id" - DEFAULT_NAME = "SolarEdge" OVERVIEW_UPDATE_DELAY = timedelta(minutes=15) @@ -18,6 +22,7 @@ ENERGY_DETAILS_DELAY = timedelta(minutes=15) SCAN_INTERVAL = timedelta(minutes=15) + # Supported overview sensor types: # Key: ['json_key', 'name', unit, icon, default] SENSOR_TYPES = { diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 82b0f427753..b19705edbd3 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -3,18 +3,15 @@ from __future__ import annotations from abc import abstractmethod from datetime import date, datetime, timedelta -import logging from typing import Any -from requests.exceptions import ConnectTimeout, HTTPError from solaredge import Solaredge from stringcase import snakecase from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER +from homeassistant.const import DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -24,16 +21,17 @@ from homeassistant.helpers.update_coordinator import ( from .const import ( CONF_SITE_ID, + DATA_API_CLIENT, DETAILS_UPDATE_DELAY, + DOMAIN, ENERGY_DETAILS_DELAY, INVENTORY_UPDATE_DELAY, + LOGGER, OVERVIEW_UPDATE_DELAY, POWER_FLOW_UPDATE_DELAY, SENSOR_TYPES, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -42,23 +40,7 @@ async def async_setup_entry( ) -> None: """Add an solarEdge entry.""" # Add the needed sensors to hass - api = Solaredge(entry.data[CONF_API_KEY]) - - # Check if api can be reached and site is active - try: - response = await hass.async_add_executor_job( - api.get_details, entry.data[CONF_SITE_ID] - ) - if response["details"]["status"].lower() != "active": - _LOGGER.error("SolarEdge site is not active") - return - _LOGGER.debug("Credentials correct and site is active") - except KeyError as ex: - _LOGGER.error("Missing details data in SolarEdge response") - raise ConfigEntryNotReady from ex - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Could not retrieve details from SolarEdge API") - raise ConfigEntryNotReady from ex + api: Solaredge = hass.data[DOMAIN][entry.entry_id][DATA_API_CLIENT] sensor_factory = SolarEdgeSensorFactory( hass, entry.title, entry.data[CONF_SITE_ID], api @@ -313,7 +295,7 @@ class SolarEdgeDataService: """Coordinator creation.""" self.coordinator = DataUpdateCoordinator( self.hass, - _LOGGER, + LOGGER, name=str(self), update_method=self.async_update_data, update_interval=self.update_interval, @@ -360,7 +342,7 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): data = value self.data[key] = data - _LOGGER.debug("Updated SolarEdge overview: %s", self.data) + LOGGER.debug("Updated SolarEdge overview: %s", self.data) class SolarEdgeDetailsDataService(SolarEdgeDataService): @@ -406,7 +388,7 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): elif key == "status": self.data = value - _LOGGER.debug("Updated SolarEdge details: %s, %s", self.data, self.attributes) + LOGGER.debug("Updated SolarEdge details: %s, %s", self.data, self.attributes) class SolarEdgeInventoryDataService(SolarEdgeDataService): @@ -432,7 +414,7 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService): self.data[key] = len(value) self.attributes[key] = {key: value} - _LOGGER.debug("Updated SolarEdge inventory: %s, %s", self.data, self.attributes) + LOGGER.debug("Updated SolarEdge inventory: %s, %s", self.data, self.attributes) class SolarEdgeEnergyDetailsService(SolarEdgeDataService): @@ -467,7 +449,7 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): raise UpdateFailed("Missing power flow data, skipping update") from ex if "meters" not in energy_details: - _LOGGER.debug( + LOGGER.debug( "Missing meters in energy details data. Assuming site does not have any" ) return @@ -491,7 +473,7 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): self.data[meter["type"]] = meter["values"][0]["value"] self.attributes[meter["type"]] = {"date": meter["values"][0]["date"]} - _LOGGER.debug( + LOGGER.debug( "Updated SolarEdge energy details: %s, %s", self.data, self.attributes ) @@ -522,7 +504,7 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService): power_to = [] if "connections" not in power_flow: - _LOGGER.debug( + LOGGER.debug( "Missing connections in power flow data. Assuming site does not have any" ) return @@ -551,6 +533,4 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService): self.attributes[key]["flow"] = "charge" if charge else "discharge" self.attributes[key]["soc"] = value["chargeLevel"] - _LOGGER.debug( - "Updated SolarEdge power flow: %s, %s", self.data, self.attributes - ) + LOGGER.debug("Updated SolarEdge power flow: %s, %s", self.data, self.attributes) From 4c7fcae5366c32de951657126ac04dbcc23a3de4 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 19 May 2021 10:13:48 +0200 Subject: [PATCH 565/852] Small bug fixes in modbus due to async (#50812) * Small bug fixes due to async. * _available is true in turn_on/turn_off * Remove double update. * Set _available. --- homeassistant/components/modbus/climate.py | 3 ++- homeassistant/components/modbus/cover.py | 6 ++++-- homeassistant/components/modbus/switch.py | 11 ++++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 501061b9e0b..e871a21a8ed 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -208,12 +208,13 @@ class ModbusThermostat(ClimateEntity): ) byte_string = struct.pack(self._structure, target_temperature) register_value = struct.unpack(">h", byte_string[0:2])[0] - self._available = await self._hub.async_pymodbus_call( + result = await self._hub.async_pymodbus_call( self._slave, self._target_temperature_register, register_value, CALL_TYPE_WRITE_REGISTERS, ) + self._available = result is not None await self.async_update() @property diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index b64a8d81777..7c0e9d33215 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -174,16 +174,18 @@ class ModbusCover(CoverEntity, RestoreEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" - self._available = await self._hub.async_pymodbus_call( + result = await self._hub.async_pymodbus_call( self._slave, self._register, self._state_open, self._write_type ) + self._available = result is not None self.async_update() async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - self._available = await self._hub.async_pymodbus_call( + result = await self._hub.async_pymodbus_call( self._slave, self._register, self._state_closed, self._write_type ) + self._available = result is not None self.async_update() async def async_update(self, now=None): diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 1589a22da4a..27e67bf2a3e 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -22,6 +22,8 @@ from homeassistant.helpers.typing import ConfigType from .const import ( CALL_TYPE_COIL, + CALL_TYPE_WRITE_COIL, + CALL_TYPE_WRITE_REGISTER, CONF_INPUT_TYPE, CONF_STATE_OFF, CONF_STATE_ON, @@ -59,7 +61,10 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): self._available = True self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) self._address = config[CONF_ADDRESS] - self._write_type = config[CONF_WRITE_TYPE] + if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: + self._write_type = CALL_TYPE_WRITE_COIL + else: + self._write_type = CALL_TYPE_WRITE_REGISTER self._command_on = config[CONF_COMMAND_ON] self._command_off = config[CONF_COMMAND_OFF] if CONF_VERIFY in config: @@ -111,7 +116,7 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): result = await self._hub.async_pymodbus_call( self._slave, self._address, self._command_on, self._write_type ) - if result is False: + if result is None: self._available = False self.async_write_ha_state() else: @@ -127,7 +132,7 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): result = await self._hub.async_pymodbus_call( self._slave, self._address, self._command_off, self._write_type ) - if result is False: + if result is None: self._available = False self.async_write_ha_state() else: From 62386c8676d95fa7f65c229c9b8c0ea64ef5ecb9 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Wed, 19 May 2021 09:36:26 +0100 Subject: [PATCH 566/852] Enable type checks for device_tracker (#50805) * Enable type checks for device_tracker * Fix MQTT test --- .../components/actiontec/device_tracker.py | 2 +- .../components/device_tracker/config_entry.py | 12 +- .../components/device_tracker/legacy.py | 137 ++++++++++-------- mypy.ini | 3 - script/hassfest/mypy_config.py | 1 - .../mqtt/test_device_tracker_discovery.py | 2 +- 6 files changed, 84 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index d7e6f5be494..8dc3f095437 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -55,7 +55,7 @@ class ActiontecDeviceScanner(DeviceScanner): self._update_info() return [client.mac_address for client in self.last_results] - def get_device_name(self, device: str) -> str | None: # type: ignore[override] + def get_device_name(self, device: str) -> str | None: """Return the name of the given device or None if we don't know.""" for client in self.last_results: if client.mac_address == device: diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 9a8c77686a1..6f4e608520c 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -82,19 +82,19 @@ class TrackerEntity(BaseTrackerEntity): return 0 @property - def location_name(self) -> str: + def location_name(self) -> str | None: """Return a location name for the current location of the device.""" return None @property - def latitude(self) -> float: + def latitude(self) -> float | None: """Return latitude value of the device.""" - return NotImplementedError + raise NotImplementedError @property - def longitude(self) -> float: + def longitude(self) -> float | None: """Return longitude value of the device.""" - return NotImplementedError + raise NotImplementedError @property def state(self): @@ -102,7 +102,7 @@ class TrackerEntity(BaseTrackerEntity): if self.location_name: return self.location_name - if self.latitude is not None: + if self.latitude is not None and self.longitude is not None: zone_state = zone.async_active_zone( self.hass, self.latitude, self.longitude, self.location_accuracy ) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index e1eb897f1ba..3427bf9595e 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -309,7 +309,7 @@ async def async_create_platform_type( def async_setup_scanner_platform( hass: HomeAssistant, config: ConfigType, - scanner: Any, + scanner: DeviceScanner, async_see_device: Callable, platform: str, ): @@ -324,7 +324,7 @@ def async_setup_scanner_platform( # Initial scan of each mac we also tell about host name for config seen: Any = set() - async def async_device_tracker_scan(now: dt_util.dt.datetime): + async def async_device_tracker_scan(now: dt_util.dt.datetime | None): """Handle interval matches.""" if update_lock.locked(): LOGGER.warning( @@ -424,21 +424,21 @@ class DeviceTracker: def see( self, - mac: str = None, - dev_id: str = None, - host_name: str = None, - location_name: str = None, - gps: GPSType = None, - gps_accuracy: int = None, - battery: int = None, - attributes: dict = None, + mac: str | None = None, + dev_id: str | None = None, + host_name: str | None = None, + location_name: str | None = None, + gps: GPSType | None = None, + gps_accuracy: int | None = None, + battery: int | None = None, + attributes: dict | None = None, source_type: str = SOURCE_TYPE_GPS, - picture: str = None, - icon: str = None, - consider_home: timedelta = None, + picture: str | None = None, + icon: str | None = None, + consider_home: timedelta | None = None, ): """Notify the device tracker that you see a device.""" - self.hass.add_job( + self.hass.create_task( self.async_see( mac, dev_id, @@ -457,19 +457,19 @@ class DeviceTracker: async def async_see( self, - mac: str = None, - dev_id: str = None, - host_name: str = None, - location_name: str = None, - gps: GPSType = None, - gps_accuracy: int = None, - battery: int = None, - attributes: dict = None, + mac: str | None = None, + dev_id: str | None = None, + host_name: str | None = None, + location_name: str | None = None, + gps: GPSType | None = None, + gps_accuracy: int | None = None, + battery: int | None = None, + attributes: dict | None = None, source_type: str = SOURCE_TYPE_GPS, - picture: str = None, - icon: str = None, - consider_home: timedelta = None, - ): + picture: str | None = None, + icon: str | None = None, + consider_home: timedelta | None = None, + ) -> None: """Notify the device tracker that you see a device. This method is a coroutine. @@ -480,13 +480,13 @@ class DeviceTracker: if mac is not None: mac = str(mac).upper() device = self.mac_to_dev.get(mac) - if not device: + if device is None: dev_id = util.slugify(host_name or "") or util.slugify(mac) else: dev_id = cv.slug(str(dev_id).lower()) device = self.devices.get(dev_id) - if device: + if device is not None: await device.async_seen( host_name, location_name, @@ -501,6 +501,9 @@ class DeviceTracker: device.async_write_ha_state() return + # If it's None then device is not None and we can't get here. + assert dev_id is not None + # Guard from calling see on entity registry entities. entity_id = f"{DOMAIN}.{dev_id}" if registry.async_is_registered(entity_id): @@ -598,15 +601,13 @@ class DeviceTracker: class Device(RestoreEntity): """Base class for a tracked device.""" - host_name: str = None - location_name: str = None - gps: GPSType = None + host_name: str | None = None + location_name: str | None = None + gps: GPSType | None = None gps_accuracy: int = 0 - last_seen: dt_util.dt.datetime = None - consider_home: dt_util.dt.timedelta = None - battery: int = None - attributes: dict = None - icon: str = None + last_seen: dt_util.dt.datetime | None = None + battery: int | None = None + attributes: dict | None = None # Track if the last update of this device was HOME. last_update_home = False @@ -618,11 +619,11 @@ class Device(RestoreEntity): consider_home: timedelta, track: bool, dev_id: str, - mac: str, - name: str = None, - picture: str = None, - gravatar: str = None, - icon: str = None, + mac: str | None, + name: str | None = None, + picture: str | None = None, + gravatar: str | None = None, + icon: str | None = None, ) -> None: """Initialize a device.""" self.hass = hass @@ -648,11 +649,11 @@ class Device(RestoreEntity): else: self.config_picture = picture - self.icon = icon + self._icon = icon - self.source_type = None + self.source_type: str | None = None - self._attributes = {} + self._attributes: dict[str, Any] = {} @property def name(self): @@ -686,21 +687,26 @@ class Device(RestoreEntity): return attributes @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device state attributes.""" return self._attributes + @property + def icon(self) -> str | None: + """Return device icon.""" + return self._icon + async def async_seen( self, - host_name: str = None, - location_name: str = None, - gps: GPSType = None, - gps_accuracy=0, - battery: int = None, - attributes: dict = None, + host_name: str | None = None, + location_name: str | None = None, + gps: GPSType | None = None, + gps_accuracy: int | None = None, + battery: int | None = None, + attributes: dict[str, Any] | None = None, source_type: str = SOURCE_TYPE_GPS, - consider_home: timedelta = None, - ): + consider_home: timedelta | None = None, + ) -> None: """Mark the device as seen.""" self.source_type = source_type self.last_seen = dt_util.utcnow() @@ -708,9 +714,9 @@ class Device(RestoreEntity): self.location_name = location_name self.consider_home = consider_home or self.consider_home - if battery: + if battery is not None: self.battery = battery - if attributes: + if attributes is not None: self._attributes.update(attributes) self.gps = None @@ -726,7 +732,7 @@ class Device(RestoreEntity): await self.async_update() - def stale(self, now: dt_util.dt.datetime = None): + def stale(self, now: dt_util.dt.datetime | None = None) -> bool: """Return if device state is stale. Async friendly. @@ -795,7 +801,7 @@ class Device(RestoreEntity): class DeviceScanner: """Device scanner object.""" - hass: HomeAssistant = None + hass: HomeAssistant | None = None def scan_devices(self) -> list[str]: """Scan for devices.""" @@ -803,14 +809,20 @@ class DeviceScanner: async def async_scan_devices(self) -> Any: """Scan for devices.""" + assert ( + self.hass is not None + ), "hass should be set by async_setup_scanner_platform" return await self.hass.async_add_executor_job(self.scan_devices) - def get_device_name(self, device: str) -> str: + def get_device_name(self, device: str) -> str | None: """Get the name of a device.""" raise NotImplementedError() - async def async_get_device_name(self, device: str) -> Any: + async def async_get_device_name(self, device: str) -> str | None: """Get the name of a device.""" + assert ( + self.hass is not None + ), "hass should be set by async_setup_scanner_platform" return await self.hass.async_add_executor_job(self.get_device_name, device) def get_extra_attributes(self, device: str) -> dict: @@ -819,6 +831,9 @@ class DeviceScanner: async def async_get_extra_attributes(self, device: str) -> Any: """Get the extra attributes of a device.""" + assert ( + self.hass is not None + ), "hass should be set by async_setup_scanner_platform" return await self.hass.async_add_executor_job(self.get_extra_attributes, device) @@ -868,7 +883,7 @@ async def async_load_config(path: str, hass: HomeAssistant, consider_home: timed def update_config(path: str, dev_id: str, device: Device): """Add device to YAML configuration file.""" with open(path, "a") as out: - device = { + device_config = { device.dev_id: { ATTR_NAME: device.name, ATTR_MAC: device.mac, @@ -878,7 +893,7 @@ def update_config(path: str, dev_id: str, device: Device): } } out.write("\n") - out.write(dump(device)) + out.write(dump(device_config)) def get_gravatar_for_email(email: str): diff --git a/mypy.ini b/mypy.ini index b2590348431..13f88680ae9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -816,9 +816,6 @@ ignore_errors = true [mypy-homeassistant.components.denonavr.*] ignore_errors = true -[mypy-homeassistant.components.device_tracker.*] -ignore_errors = true - [mypy-homeassistant.components.devolo_home_control.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index dd9e6c521fa..94f5bc81a0d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -44,7 +44,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.deconz.*", "homeassistant.components.demo.*", "homeassistant.components.denonavr.*", - "homeassistant.components.device_tracker.*", "homeassistant.components.devolo_home_control.*", "homeassistant.components.dhcp.*", "homeassistant.components.directv.*", diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index f158a878fcd..174db8f017a 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -359,4 +359,4 @@ async def test_setting_device_tracker_location_via_lat_lon_message( async_fire_mqtt_message(hass, "attributes-topic", '{"latitude":32.87336}') state = hass.states.get("device_tracker.test") assert state.attributes["latitude"] == 32.87336 - assert state.state == STATE_NOT_HOME + assert state.state == STATE_UNKNOWN From bce5f8ee05b8bd66c819de3adaf63518a6c1ff50 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 19 May 2021 10:37:16 +0200 Subject: [PATCH 567/852] Improve AccuWeather type annotations (#50616) * Improve type annotations * Remove unused argument * Simplify state logic * Fix uvindex state * Fix type for logger * Increase tests coverage * Fix pylint arguments-differ error * Suggested change * Suggested change * Remove unnecessary variable * Remove unnecessary conditions * Use int instead of list for forecast days * Add enabled to sensor types dicts * Fix request_remaining conversion and tests * Run hassfest * Suggested change * Suggested change * Do not use StateType --- .strict-typing | 1 + .../components/accuweather/__init__.py | 52 ++- .../components/accuweather/config_flow.py | 25 +- homeassistant/components/accuweather/const.py | 408 +++++++++--------- homeassistant/components/accuweather/model.py | 15 + .../components/accuweather/sensor.py | 146 ++++--- .../components/accuweather/system_health.py | 6 +- .../components/accuweather/weather.py | 79 ++-- mypy.ini | 11 + tests/components/accuweather/__init__.py | 6 +- .../accuweather/test_config_flow.py | 22 +- tests/components/accuweather/test_sensor.py | 27 +- tests/components/accuweather/test_weather.py | 12 +- 13 files changed, 473 insertions(+), 337 deletions(-) create mode 100644 homeassistant/components/accuweather/model.py diff --git a/.strict-typing b/.strict-typing index 6d8b22493b6..1fbaaa39c30 100644 --- a/.strict-typing +++ b/.strict-typing @@ -4,6 +4,7 @@ homeassistant.components homeassistant.components.acer_projector.* +homeassistant.components.accuweather.* homeassistant.components.actiontec.* homeassistant.components.aftership.* homeassistant.components.air_quality.* diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index f6f124b2d4d..27dd4b9c196 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -1,12 +1,18 @@ """The AccuWeather component.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any, Dict from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError +from aiohttp import ClientSession from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -23,11 +29,12 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor", "weather"] -async def async_setup_entry(hass, config_entry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AccuWeather as config entry.""" - api_key = config_entry.data[CONF_API_KEY] - location_key = config_entry.unique_id - forecast = config_entry.options.get(CONF_FORECAST, False) + api_key: str = entry.data[CONF_API_KEY] + assert entry.unique_id is not None + location_key = entry.unique_id + forecast: bool = entry.options.get(CONF_FORECAST, False) _LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast) @@ -38,41 +45,46 @@ async def async_setup_entry(hass, config_entry) -> bool: ) await coordinator.async_config_entry_first_refresh() - undo_listener = config_entry.add_update_listener(update_listener) + undo_listener = entry.add_update_listener(update_listener) - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok -async def update_listener(hass, config_entry): +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener.""" - await hass.config_entries.async_reload(config_entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) -class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator): +class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[Dict[str, Any]]): """Class to manage fetching AccuWeather data API.""" - def __init__(self, hass, session, api_key, location_key, forecast: bool): + def __init__( + self, + hass: HomeAssistant, + session: ClientSession, + api_key: str, + location_key: str, + forecast: bool, + ) -> None: """Initialize.""" self.location_key = location_key self.forecast = forecast @@ -87,11 +99,11 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator): update_interval = timedelta(minutes=40) if self.forecast: update_interval *= 2 - _LOGGER.debug("Data will be update every %s", update_interval) + _LOGGER.debug("Data will be update every %s", str(update_interval)) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - async def _async_update_data(self): + async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" try: async with timeout(10): @@ -108,5 +120,5 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator): RequestsExceededError, ) as error: raise UpdateFailed(error) from error - _LOGGER.debug("Requests remaining: %s", self.accuweather.requests_remaining) + _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining) return {**current, **{ATTR_FORECAST: forecast}} diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py index 999a54b11a7..b9244a3645c 100644 --- a/homeassistant/components/accuweather/config_flow.py +++ b/homeassistant/components/accuweather/config_flow.py @@ -1,5 +1,8 @@ """Adds config flow for AccuWeather.""" +from __future__ import annotations + import asyncio +from typing import Any from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from aiohttp import ClientError @@ -8,8 +11,10 @@ from async_timeout import timeout import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -21,7 +26,9 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" # Under the terms of use of the API, one user can use one free API key. Due to # the small number of requests allowed, we only allow one integration instance. @@ -77,7 +84,9 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> AccuWeatherOptionsFlowHandler: """Options callback for AccuWeather.""" return AccuWeatherOptionsFlowHandler(config_entry) @@ -85,15 +94,19 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow): """Config flow options for AccuWeather.""" - def __init__(self, config_entry): + def __init__(self, entry: ConfigEntry) -> None: """Initialize AccuWeather options flow.""" - self.config_entry = config_entry + self.config_entry = entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" return await self.async_step_user() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 60fdd48c8f4..e4ec49ce2ac 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -1,4 +1,8 @@ """Constants for AccuWeather integration.""" +from __future__ import annotations + +from typing import Final + from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -16,8 +20,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ) from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, CONCENTRATION_PARTS_PER_CUBIC_METER, DEVICE_CLASS_TEMPERATURE, LENGTH_FEET, @@ -33,18 +35,19 @@ from homeassistant.const import ( UV_INDEX, ) -ATTRIBUTION = "Data provided by AccuWeather" -ATTR_FORECAST = CONF_FORECAST = "forecast" -ATTR_LABEL = "label" -ATTR_UNIT_IMPERIAL = "Imperial" -ATTR_UNIT_METRIC = "Metric" -COORDINATOR = "coordinator" -DOMAIN = "accuweather" -MANUFACTURER = "AccuWeather, Inc." -NAME = "AccuWeather" -UNDO_UPDATE_LISTENER = "undo_update_listener" +from .model import SensorDescription -CONDITION_CLASSES = { +ATTRIBUTION: Final = "Data provided by AccuWeather" +ATTR_FORECAST: Final = "forecast" +CONF_FORECAST: Final = "forecast" +COORDINATOR: Final = "coordinator" +DOMAIN: Final = "accuweather" +MANUFACTURER: Final = "AccuWeather, Inc." +MAX_FORECAST_DAYS: Final = 4 +NAME: Final = "AccuWeather" +UNDO_UPDATE_LISTENER: Final = "undo_update_listener" + +CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37], ATTR_CONDITION_CLOUDY: [7, 8, 38], ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31], @@ -61,255 +64,264 @@ CONDITION_CLASSES = { ATTR_CONDITION_WINDY: [32], } -FORECAST_DAYS = [0, 1, 2, 3, 4] - -FORECAST_SENSOR_TYPES = { +FORECAST_SENSOR_TYPES: Final[dict[str, SensorDescription]] = { "CloudCoverDay": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-cloudy", - ATTR_LABEL: "Cloud Cover Day", - ATTR_UNIT_METRIC: PERCENTAGE, - ATTR_UNIT_IMPERIAL: PERCENTAGE, + "device_class": None, + "icon": "mdi:weather-cloudy", + "label": "Cloud Cover Day", + "unit_metric": PERCENTAGE, + "unit_imperial": PERCENTAGE, + "enabled": False, }, "CloudCoverNight": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-cloudy", - ATTR_LABEL: "Cloud Cover Night", - ATTR_UNIT_METRIC: PERCENTAGE, - ATTR_UNIT_IMPERIAL: PERCENTAGE, + "device_class": None, + "icon": "mdi:weather-cloudy", + "label": "Cloud Cover Night", + "unit_metric": PERCENTAGE, + "unit_imperial": PERCENTAGE, + "enabled": False, }, "Grass": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:grass", - ATTR_LABEL: "Grass Pollen", - ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + "device_class": None, + "icon": "mdi:grass", + "label": "Grass Pollen", + "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER, + "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER, + "enabled": False, }, "HoursOfSun": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-partly-cloudy", - ATTR_LABEL: "Hours Of Sun", - ATTR_UNIT_METRIC: TIME_HOURS, - ATTR_UNIT_IMPERIAL: TIME_HOURS, + "device_class": None, + "icon": "mdi:weather-partly-cloudy", + "label": "Hours Of Sun", + "unit_metric": TIME_HOURS, + "unit_imperial": TIME_HOURS, + "enabled": True, }, "Mold": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:blur", - ATTR_LABEL: "Mold Pollen", - ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + "device_class": None, + "icon": "mdi:blur", + "label": "Mold Pollen", + "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER, + "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER, + "enabled": False, }, "Ozone": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:vector-triangle", - ATTR_LABEL: "Ozone", - ATTR_UNIT_METRIC: None, - ATTR_UNIT_IMPERIAL: None, + "device_class": None, + "icon": "mdi:vector-triangle", + "label": "Ozone", + "unit_metric": None, + "unit_imperial": None, + "enabled": False, }, "Ragweed": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:sprout", - ATTR_LABEL: "Ragweed Pollen", - ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + "device_class": None, + "icon": "mdi:sprout", + "label": "Ragweed Pollen", + "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER, + "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER, + "enabled": False, }, "RealFeelTemperatureMax": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature Max", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": "RealFeel Temperature Max", + "unit_metric": TEMP_CELSIUS, + "unit_imperial": TEMP_FAHRENHEIT, + "enabled": True, }, "RealFeelTemperatureMin": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature Min", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": "RealFeel Temperature Min", + "unit_metric": TEMP_CELSIUS, + "unit_imperial": TEMP_FAHRENHEIT, + "enabled": True, }, "RealFeelTemperatureShadeMax": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature Shade Max", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": "RealFeel Temperature Shade Max", + "unit_metric": TEMP_CELSIUS, + "unit_imperial": TEMP_FAHRENHEIT, + "enabled": False, }, "RealFeelTemperatureShadeMin": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature Shade Min", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": "RealFeel Temperature Shade Min", + "unit_metric": TEMP_CELSIUS, + "unit_imperial": TEMP_FAHRENHEIT, + "enabled": False, }, "ThunderstormProbabilityDay": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-lightning", - ATTR_LABEL: "Thunderstorm Probability Day", - ATTR_UNIT_METRIC: PERCENTAGE, - ATTR_UNIT_IMPERIAL: PERCENTAGE, + "device_class": None, + "icon": "mdi:weather-lightning", + "label": "Thunderstorm Probability Day", + "unit_metric": PERCENTAGE, + "unit_imperial": PERCENTAGE, + "enabled": True, }, "ThunderstormProbabilityNight": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-lightning", - ATTR_LABEL: "Thunderstorm Probability Night", - ATTR_UNIT_METRIC: PERCENTAGE, - ATTR_UNIT_IMPERIAL: PERCENTAGE, + "device_class": None, + "icon": "mdi:weather-lightning", + "label": "Thunderstorm Probability Night", + "unit_metric": PERCENTAGE, + "unit_imperial": PERCENTAGE, + "enabled": True, }, "Tree": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:tree-outline", - ATTR_LABEL: "Tree Pollen", - ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + "device_class": None, + "icon": "mdi:tree-outline", + "label": "Tree Pollen", + "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER, + "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER, + "enabled": False, }, "UVIndex": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-sunny", - ATTR_LABEL: "UV Index", - ATTR_UNIT_METRIC: UV_INDEX, - ATTR_UNIT_IMPERIAL: UV_INDEX, + "device_class": None, + "icon": "mdi:weather-sunny", + "label": "UV Index", + "unit_metric": UV_INDEX, + "unit_imperial": UV_INDEX, + "enabled": True, }, "WindGustDay": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Gust Day", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + "device_class": None, + "icon": "mdi:weather-windy", + "label": "Wind Gust Day", + "unit_metric": SPEED_KILOMETERS_PER_HOUR, + "unit_imperial": SPEED_MILES_PER_HOUR, + "enabled": False, }, "WindGustNight": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Gust Night", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + "device_class": None, + "icon": "mdi:weather-windy", + "label": "Wind Gust Night", + "unit_metric": SPEED_KILOMETERS_PER_HOUR, + "unit_imperial": SPEED_MILES_PER_HOUR, + "enabled": False, }, "WindDay": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Day", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + "device_class": None, + "icon": "mdi:weather-windy", + "label": "Wind Day", + "unit_metric": SPEED_KILOMETERS_PER_HOUR, + "unit_imperial": SPEED_MILES_PER_HOUR, + "enabled": True, }, "WindNight": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Night", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + "device_class": None, + "icon": "mdi:weather-windy", + "label": "Wind Night", + "unit_metric": SPEED_KILOMETERS_PER_HOUR, + "unit_imperial": SPEED_MILES_PER_HOUR, + "enabled": True, }, } -OPTIONAL_SENSORS = ( - "ApparentTemperature", - "CloudCover", - "CloudCoverDay", - "CloudCoverNight", - "DewPoint", - "Grass", - "Mold", - "Ozone", - "Ragweed", - "RealFeelTemperatureShade", - "RealFeelTemperatureShadeMax", - "RealFeelTemperatureShadeMin", - "Tree", - "WetBulbTemperature", - "WindChillTemperature", - "WindGust", - "WindGustDay", - "WindGustNight", -) - -SENSOR_TYPES = { +SENSOR_TYPES: Final[dict[str, SensorDescription]] = { "ApparentTemperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Apparent Temperature", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": "Apparent Temperature", + "unit_metric": TEMP_CELSIUS, + "unit_imperial": TEMP_FAHRENHEIT, + "enabled": False, }, "Ceiling": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-fog", - ATTR_LABEL: "Cloud Ceiling", - ATTR_UNIT_METRIC: LENGTH_METERS, - ATTR_UNIT_IMPERIAL: LENGTH_FEET, + "device_class": None, + "icon": "mdi:weather-fog", + "label": "Cloud Ceiling", + "unit_metric": LENGTH_METERS, + "unit_imperial": LENGTH_FEET, + "enabled": True, }, "CloudCover": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-cloudy", - ATTR_LABEL: "Cloud Cover", - ATTR_UNIT_METRIC: PERCENTAGE, - ATTR_UNIT_IMPERIAL: PERCENTAGE, + "device_class": None, + "icon": "mdi:weather-cloudy", + "label": "Cloud Cover", + "unit_metric": PERCENTAGE, + "unit_imperial": PERCENTAGE, + "enabled": False, }, "DewPoint": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Dew Point", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": "Dew Point", + "unit_metric": TEMP_CELSIUS, + "unit_imperial": TEMP_FAHRENHEIT, + "enabled": False, }, "RealFeelTemperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": "RealFeel Temperature", + "unit_metric": TEMP_CELSIUS, + "unit_imperial": TEMP_FAHRENHEIT, + "enabled": True, }, "RealFeelTemperatureShade": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature Shade", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": "RealFeel Temperature Shade", + "unit_metric": TEMP_CELSIUS, + "unit_imperial": TEMP_FAHRENHEIT, + "enabled": False, }, "Precipitation": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-rainy", - ATTR_LABEL: "Precipitation", - ATTR_UNIT_METRIC: LENGTH_MILLIMETERS, - ATTR_UNIT_IMPERIAL: LENGTH_INCHES, + "device_class": None, + "icon": "mdi:weather-rainy", + "label": "Precipitation", + "unit_metric": LENGTH_MILLIMETERS, + "unit_imperial": LENGTH_INCHES, + "enabled": True, }, "PressureTendency": { - ATTR_DEVICE_CLASS: "accuweather__pressure_tendency", - ATTR_ICON: "mdi:gauge", - ATTR_LABEL: "Pressure Tendency", - ATTR_UNIT_METRIC: None, - ATTR_UNIT_IMPERIAL: None, + "device_class": "accuweather__pressure_tendency", + "icon": "mdi:gauge", + "label": "Pressure Tendency", + "unit_metric": None, + "unit_imperial": None, + "enabled": True, }, "UVIndex": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-sunny", - ATTR_LABEL: "UV Index", - ATTR_UNIT_METRIC: UV_INDEX, - ATTR_UNIT_IMPERIAL: UV_INDEX, + "device_class": None, + "icon": "mdi:weather-sunny", + "label": "UV Index", + "unit_metric": UV_INDEX, + "unit_imperial": UV_INDEX, + "enabled": True, }, "WetBulbTemperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Wet Bulb Temperature", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": "Wet Bulb Temperature", + "unit_metric": TEMP_CELSIUS, + "unit_imperial": TEMP_FAHRENHEIT, + "enabled": False, }, "WindChillTemperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Wind Chill Temperature", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": "Wind Chill Temperature", + "unit_metric": TEMP_CELSIUS, + "unit_imperial": TEMP_FAHRENHEIT, + "enabled": False, }, "Wind": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + "device_class": None, + "icon": "mdi:weather-windy", + "label": "Wind", + "unit_metric": SPEED_KILOMETERS_PER_HOUR, + "unit_imperial": SPEED_MILES_PER_HOUR, + "enabled": True, }, "WindGust": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Gust", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + "device_class": None, + "icon": "mdi:weather-windy", + "label": "Wind Gust", + "unit_metric": SPEED_KILOMETERS_PER_HOUR, + "unit_imperial": SPEED_MILES_PER_HOUR, + "enabled": False, }, } diff --git a/homeassistant/components/accuweather/model.py b/homeassistant/components/accuweather/model.py new file mode 100644 index 00000000000..cc51efbd0e2 --- /dev/null +++ b/homeassistant/components/accuweather/model.py @@ -0,0 +1,15 @@ +"""Type definitions for AccuWeather integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class SensorDescription(TypedDict): + """Sensor description class.""" + + device_class: str | None + icon: str | None + label: str + unit_metric: str | None + unit_imperial: str | None + enabled: bool diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 722dd8869be..9f2d9ed78bd 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -1,44 +1,50 @@ """Support for the AccuWeather service.""" +from __future__ import annotations + +from typing import Any, cast + from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, - CONF_NAME, - DEVICE_CLASS_TEMPERATURE, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TEMPERATURE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import AccuWeatherDataUpdateCoordinator from .const import ( ATTR_FORECAST, - ATTR_ICON, - ATTR_LABEL, ATTRIBUTION, COORDINATOR, DOMAIN, - FORECAST_DAYS, FORECAST_SENSOR_TYPES, MANUFACTURER, + MAX_FORECAST_DAYS, NAME, - OPTIONAL_SENSORS, SENSOR_TYPES, ) PARALLEL_UPDATES = 1 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Add AccuWeather entities from a config_entry.""" - name = config_entry.data[CONF_NAME] + name: str = entry.data[CONF_NAME] - coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + COORDINATOR + ] - sensors = [] + sensors: list[AccuWeatherSensor] = [] for sensor in SENSOR_TYPES: sensors.append(AccuWeatherSensor(name, sensor, coordinator)) if coordinator.forecast: for sensor in FORECAST_SENSOR_TYPES: - for day in FORECAST_DAYS: + for day in range(MAX_FORECAST_DAYS + 1): # Some air quality/allergy sensors are only available for certain # locations. if sensor in coordinator.data[ATTR_FORECAST][0]: @@ -46,38 +52,56 @@ async def async_setup_entry(hass, config_entry, async_add_entities): AccuWeatherSensor(name, sensor, coordinator, forecast_day=day) ) - async_add_entities(sensors, False) + async_add_entities(sensors) class AccuWeatherSensor(CoordinatorEntity, SensorEntity): """Define an AccuWeather entity.""" - def __init__(self, name, kind, coordinator, forecast_day=None): + coordinator: AccuWeatherDataUpdateCoordinator + + def __init__( + self, + name: str, + kind: str, + coordinator: AccuWeatherDataUpdateCoordinator, + forecast_day: int | None = None, + ) -> None: """Initialize.""" super().__init__(coordinator) + if forecast_day is None: + self._description = SENSOR_TYPES[kind] + self._sensor_data: dict[str, Any] + if kind == "Precipitation": + self._sensor_data = coordinator.data["PrecipitationSummary"][kind] + else: + self._sensor_data = coordinator.data[kind] + else: + self._description = FORECAST_SENSOR_TYPES[kind] + self._sensor_data = coordinator.data[ATTR_FORECAST][forecast_day][kind] + self._unit_system = "Metric" if coordinator.is_metric else "Imperial" self._name = name self.kind = kind self._device_class = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} - self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial" self.forecast_day = forecast_day @property - def name(self): + def name(self) -> str: """Return the name.""" if self.forecast_day is not None: - return f"{self._name} {FORECAST_SENSOR_TYPES[self.kind][ATTR_LABEL]} {self.forecast_day}d" - return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + return f"{self._name} {self._description['label']} {self.forecast_day}d" + return f"{self._name} {self._description['label']}" @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" if self.forecast_day is not None: return f"{self.coordinator.location_key}-{self.kind}-{self.forecast_day}".lower() return f"{self.coordinator.location_key}-{self.kind}".lower() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "identifiers": {(DOMAIN, self.coordinator.location_key)}, @@ -87,72 +111,54 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): } @property - def state(self): + def state(self) -> StateType: """Return the state.""" if self.forecast_day is not None: - if ( - FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] - == DEVICE_CLASS_TEMPERATURE - ): - return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ - self.kind - ]["Value"] - if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: - return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ - self.kind - ]["Speed"]["Value"] - if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]: - return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ - self.kind - ]["Value"] - return self.coordinator.data[ATTR_FORECAST][self.forecast_day][self.kind] + if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE: + return cast(float, self._sensor_data["Value"]) + if self.kind == "UVIndex": + return cast(int, self._sensor_data["Value"]) + if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "Ozone"]: + return cast(int, self._sensor_data["Value"]) if self.kind == "Ceiling": - return round(self.coordinator.data[self.kind][self._unit_system]["Value"]) + return round(self._sensor_data[self._unit_system]["Value"]) if self.kind == "PressureTendency": - return self.coordinator.data[self.kind]["LocalizedText"].lower() - if SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE: - return self.coordinator.data[self.kind][self._unit_system]["Value"] + return cast(str, self._sensor_data["LocalizedText"].lower()) + if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE: + return cast(float, self._sensor_data[self._unit_system]["Value"]) if self.kind == "Precipitation": - return self.coordinator.data["PrecipitationSummary"][self.kind][ - self._unit_system - ]["Value"] + return cast(float, self._sensor_data[self._unit_system]["Value"]) if self.kind in ["Wind", "WindGust"]: - return self.coordinator.data[self.kind]["Speed"][self._unit_system]["Value"] - return self.coordinator.data[self.kind] + return cast(float, self._sensor_data["Speed"][self._unit_system]["Value"]) + if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: + return cast(StateType, self._sensor_data["Speed"]["Value"]) + return cast(StateType, self._sensor_data) @property - def icon(self): + def icon(self) -> str | None: """Return the icon.""" - if self.forecast_day is not None: - return FORECAST_SENSOR_TYPES[self.kind][ATTR_ICON] - return SENSOR_TYPES[self.kind][ATTR_ICON] + return self._description["icon"] @property - def device_class(self): + def device_class(self) -> str | None: """Return the device_class.""" - if self.forecast_day is not None: - return FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] - return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + return self._description["device_class"] @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" - if self.forecast_day is not None: - return FORECAST_SENSOR_TYPES[self.kind][self._unit_system] - return SENSOR_TYPES[self.kind][self._unit_system] + if self.coordinator.is_metric: + return self._description["unit_metric"] + return self._description["unit_imperial"] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" if self.forecast_day is not None: if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: - self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][ - self.forecast_day - ][self.kind]["Direction"]["English"] + self._attrs["direction"] = self._sensor_data["Direction"]["English"] elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]: - self._attrs["level"] = self.coordinator.data[ATTR_FORECAST][ - self.forecast_day - ][self.kind]["Category"] + self._attrs["level"] = self._sensor_data["Category"] return self._attrs if self.kind == "UVIndex": self._attrs["level"] = self.coordinator.data["UVIndexText"] @@ -161,6 +167,6 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): return self._attrs @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 bool(self.kind not in OPTIONAL_SENSORS) + return self._description["enabled"] diff --git a/homeassistant/components/accuweather/system_health.py b/homeassistant/components/accuweather/system_health.py index 58c9ba35881..5feed5c1f34 100644 --- a/homeassistant/components/accuweather/system_health.py +++ b/homeassistant/components/accuweather/system_health.py @@ -1,4 +1,8 @@ """Provide info to system health.""" +from __future__ import annotations + +from typing import Any + from accuweather.const import ENDPOINT from homeassistant.components import system_health @@ -15,7 +19,7 @@ def async_register( register.async_register_info(system_health_info) -async def system_health_info(hass): +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" remaining_requests = list(hass.data[DOMAIN].values())[0][ COORDINATOR diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 3c0dcfedf43..e4745537c4f 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -1,5 +1,8 @@ """Support for the AccuWeather service.""" +from __future__ import annotations + from statistics import mean +from typing import Any, cast from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -12,10 +15,15 @@ from homeassistant.components.weather import ( ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utc_from_timestamp +from . import AccuWeatherDataUpdateCoordinator from .const import ( ATTR_FORECAST, ATTRIBUTION, @@ -29,42 +37,49 @@ from .const import ( PARALLEL_UPDATES = 1 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Add a AccuWeather weather entity from a config_entry.""" - name = config_entry.data[CONF_NAME] + name: str = entry.data[CONF_NAME] - coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + COORDINATOR + ] - async_add_entities([AccuWeatherEntity(name, coordinator)], False) + async_add_entities([AccuWeatherEntity(name, coordinator)]) class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): """Define an AccuWeather entity.""" - def __init__(self, name, coordinator): + coordinator: AccuWeatherDataUpdateCoordinator + + def __init__( + self, name: str, coordinator: AccuWeatherDataUpdateCoordinator + ) -> None: """Initialize.""" super().__init__(coordinator) self._name = name - self._attrs = {} self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial" @property - def name(self): + def name(self) -> str: """Return the name.""" return self._name @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return ATTRIBUTION @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" return self.coordinator.location_key @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "identifiers": {(DOMAIN, self.coordinator.location_key)}, @@ -74,7 +89,7 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): } @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" try: return [ @@ -86,52 +101,60 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): return None @property - def temperature(self): + def temperature(self) -> float: """Return the temperature.""" - return self.coordinator.data["Temperature"][self._unit_system]["Value"] + return cast( + float, self.coordinator.data["Temperature"][self._unit_system]["Value"] + ) @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT @property - def pressure(self): + def pressure(self) -> float: """Return the pressure.""" - return self.coordinator.data["Pressure"][self._unit_system]["Value"] + return cast( + float, self.coordinator.data["Pressure"][self._unit_system]["Value"] + ) @property - def humidity(self): + def humidity(self) -> int: """Return the humidity.""" - return self.coordinator.data["RelativeHumidity"] + return cast(int, self.coordinator.data["RelativeHumidity"]) @property - def wind_speed(self): + def wind_speed(self) -> float: """Return the wind speed.""" - return self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"] + return cast( + float, self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"] + ) @property - def wind_bearing(self): + def wind_bearing(self) -> int: """Return the wind bearing.""" - return self.coordinator.data["Wind"]["Direction"]["Degrees"] + return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"]) @property - def visibility(self): + def visibility(self) -> float: """Return the visibility.""" - return self.coordinator.data["Visibility"][self._unit_system]["Value"] + return cast( + float, self.coordinator.data["Visibility"][self._unit_system]["Value"] + ) @property - def ozone(self): + def ozone(self) -> int | None: """Return the ozone level.""" # We only have ozone data for certain locations and only in the forecast data. if self.coordinator.forecast and self.coordinator.data[ATTR_FORECAST][0].get( "Ozone" ): - return self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"] + return cast(int, self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"]) return None @property - def forecast(self): + def forecast(self) -> list[dict[str, Any]] | None: """Return the forecast array.""" if not self.coordinator.forecast: return None @@ -161,7 +184,7 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): return forecast @staticmethod - def _calc_precipitation(day: dict) -> float: + def _calc_precipitation(day: dict[str, Any]) -> float: """Return sum of the precipitation.""" precip_sum = 0 precip_types = ["Rain", "Snow", "Ice"] diff --git a/mypy.ini b/mypy.ini index 13f88680ae9..0ca5b618fc6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -55,6 +55,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.accuweather.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.actiontec.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/accuweather/__init__.py b/tests/components/accuweather/__init__.py index d78eac4269b..3e0c6c2b875 100644 --- a/tests/components/accuweather/__init__.py +++ b/tests/components/accuweather/__init__.py @@ -1,6 +1,6 @@ """Tests for AccuWeather.""" import json -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from homeassistant.components.accuweather.const import DOMAIN @@ -40,6 +40,10 @@ async def init_integration( ), patch( "homeassistant.components.accuweather.AccuWeather.async_get_forecast", return_value=forecast, + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index 1d9feecda3c..c8f2d3c8c89 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -1,6 +1,6 @@ """Define tests for the AccuWeather config flow.""" import json -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError @@ -50,7 +50,7 @@ async def test_api_key_too_short(hass): async def test_invalid_api_key(hass): """Test that errors are shown when API key is invalid.""" with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", side_effect=InvalidApiKeyError("Invalid API key"), ): @@ -66,7 +66,7 @@ async def test_invalid_api_key(hass): async def test_api_error(hass): """Test API error.""" with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", side_effect=ApiError("Invalid response from AccuWeather API"), ): @@ -82,7 +82,7 @@ async def test_api_error(hass): async def test_requests_exceeded_error(hass): """Test requests exceeded error.""" with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", side_effect=RequestsExceededError( "The allowed number of requests has been exceeded" ), @@ -100,7 +100,7 @@ async def test_requests_exceeded_error(hass): async def test_integration_already_exists(hass): """Test we only allow a single config flow.""" with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", return_value=json.loads(load_fixture("accuweather/location_data.json")), ): MockConfigEntry( @@ -122,7 +122,7 @@ async def test_integration_already_exists(hass): async def test_create_entry(hass): """Test that the user step works.""" with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", return_value=json.loads(load_fixture("accuweather/location_data.json")), ), patch( "homeassistant.components.accuweather.async_setup_entry", return_value=True @@ -152,15 +152,19 @@ async def test_options_flow(hass): config_entry.add_to_hass(hass) with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", return_value=json.loads(load_fixture("accuweather/location_data.json")), ), patch( - "accuweather.AccuWeather.async_get_current_conditions", + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", return_value=json.loads( load_fixture("accuweather/current_conditions_data.json") ), ), patch( - "accuweather.AccuWeather.async_get_forecast" + "homeassistant.components.accuweather.AccuWeather.async_get_forecast" + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index a4436445340..482fae696c0 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -1,7 +1,7 @@ """Test sensor of AccuWeather integration.""" from datetime import timedelta import json -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_PARTS_PER_CUBIC_METER, DEVICE_CLASS_TEMPERATURE, + LENGTH_FEET, LENGTH_METERS, LENGTH_MILLIMETERS, PERCENTAGE, @@ -25,6 +26,7 @@ from homeassistant.const import ( from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from tests.common import async_fire_time_changed, load_fixture from tests.components.accuweather import init_integration @@ -616,6 +618,10 @@ async def test_availability(hass): return_value=json.loads( load_fixture("accuweather/current_conditions_data.json") ), + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -641,7 +647,11 @@ async def test_manual_update_entity(hass): ) as mock_current, patch( "homeassistant.components.accuweather.AccuWeather.async_get_forecast", return_value=forecast, - ) as mock_forecast: + ) as mock_forecast, patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ): await hass.services.async_call( "homeassistant", "update_entity", @@ -650,3 +660,16 @@ async def test_manual_update_entity(hass): ) assert mock_current.call_count == 1 assert mock_forecast.call_count == 1 + + +async def test_sensor_imperial_units(hass): + """Test states of the sensor without forecast.""" + hass.config.units = IMPERIAL_SYSTEM + await init_integration(hass) + + state = hass.states.get("sensor.home_cloud_ceiling") + assert state + assert state.state == "10500" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_FEET diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 8190d96e634..b1c87c7d404 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -1,7 +1,7 @@ """Test weather of AccuWeather integration.""" from datetime import timedelta import json -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from homeassistant.components.accuweather.const import ATTRIBUTION from homeassistant.components.weather import ( @@ -112,6 +112,10 @@ async def test_availability(hass): return_value=json.loads( load_fixture("accuweather/current_conditions_data.json") ), + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -137,7 +141,11 @@ async def test_manual_update_entity(hass): ) as mock_current, patch( "homeassistant.components.accuweather.AccuWeather.async_get_forecast", return_value=forecast, - ) as mock_forecast: + ) as mock_forecast, patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ): await hass.services.async_call( "homeassistant", "update_entity", From f14f7134b3382b2171c63197c3da2c2cffc50468 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 May 2021 12:12:20 +0300 Subject: [PATCH 568/852] Bump home-assistant/wheels from 2021.05.3 to 2021.05.4 (#50809) Bumps [home-assistant/wheels](https://github.com/home-assistant/wheels) from 2021.05.3 to 2021.05.4. - [Release notes](https://github.com/home-assistant/wheels/releases) - [Commits](https://github.com/home-assistant/wheels/compare/2021.05.3...2021.05.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 0497ac32e86..ab506274585 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -82,7 +82,7 @@ jobs: name: requirements_diff - name: Build wheels - uses: home-assistant/wheels@2021.05.3 + uses: home-assistant/wheels@2021.05.4 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} @@ -152,7 +152,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2021.05.3 + uses: home-assistant/wheels@2021.05.4 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} From b7fc537cd5cb68447cbc031628b7488878b375bf Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 19 May 2021 11:39:53 +0200 Subject: [PATCH 569/852] Remove non pymodbus_call from modbus.py. (#50813) --- homeassistant/components/modbus/modbus.py | 40 ++++++----------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 1a8da35f6fe..943e4a81d54 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -92,12 +92,12 @@ async def async_modbus_setup( service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB ) if isinstance(value, list): - await hub_collect[client_name].async_write_registers( - unit, address, [int(float(i)) for i in value] + await hub_collect[client_name].async_pymodbus_call( + unit, address, [int(float(i)) for i in value], CALL_TYPE_WRITE_REGISTERS ) else: - await hub_collect[client_name].async_write_register( - unit, address, int(float(value)) + await hub_collect[client_name].async_pymodbus_call( + unit, address, int(float(value)), CALL_TYPE_WRITE_REGISTER ) hass.services.async_register( @@ -116,9 +116,13 @@ async def async_modbus_setup( service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB ) if isinstance(state, list): - await hub_collect[client_name].async_write_coils(unit, address, state) + await hub_collect[client_name].async_pymodbus_call( + unit, address, state, CALL_TYPE_WRITE_COILS + ) else: - await hub_collect[client_name].async_write_coil(unit, address, state) + await hub_collect[client_name].async_pymodbus_call( + unit, address, state, CALL_TYPE_WRITE_COIL + ) hass.services.async_register( DOMAIN, SERVICE_WRITE_COIL, async_write_coil, schema=service_write_coil_schema @@ -326,27 +330,3 @@ class ModbusHub: return await self.hass.async_add_executor_job( self._pymodbus_call, unit, address, value, use_call ) - - async def async_write_coil(self, unit, address, value) -> bool: - """Write coil.""" - return await self.async_pymodbus_call( - unit, address, value, CALL_TYPE_WRITE_COIL - ) - - async def async_write_coils(self, unit, address, values) -> bool: - """Write coil.""" - return await self.async_pymodbus_call( - unit, address, values, CALL_TYPE_WRITE_COILS - ) - - async def async_write_register(self, unit, address, value) -> bool: - """Write register.""" - return await self.async_pymodbus_call( - unit, address, value, CALL_TYPE_WRITE_REGISTER - ) - - async def async_write_registers(self, unit, address, values) -> bool: - """Write registers.""" - return await self.async_pymodbus_call( - unit, address, values, CALL_TYPE_WRITE_REGISTERS - ) From 28e9b9e01df76801b8cf6169f21398e63ab27cf5 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 19 May 2021 10:41:20 +0100 Subject: [PATCH 570/852] Add evohome water_heater service calls, bump client to 0.3.15 (#50803) --- homeassistant/components/evohome/__init__.py | 12 +++++++----- homeassistant/components/evohome/manifest.json | 2 +- homeassistant/components/evohome/water_heater.py | 8 ++++++++ requirements_all.txt | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index cadeefa3c3a..4084045b1fb 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -180,7 +180,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def load_auth_tokens(store) -> tuple[dict, dict | None]: app_storage = await store.async_load() - tokens = dict(app_storage if app_storage else {}) + tokens = dict(app_storage or {}) if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]: # any tokens won't be valid, and store might be be corrupt @@ -406,10 +406,12 @@ class EvoBroker: # evohomeasync2 uses naive/local datetimes access_token_expires = _dt_local_to_aware(self.client.access_token_expires) - app_storage = {CONF_USERNAME: self.client.username} - app_storage[REFRESH_TOKEN] = self.client.refresh_token - app_storage[ACCESS_TOKEN] = self.client.access_token - app_storage[ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat() + app_storage = { + CONF_USERNAME: self.client.username, + REFRESH_TOKEN: self.client.refresh_token, + ACCESS_TOKEN: self.client.access_token, + ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(), + } if self.client_v1 and self.client_v1.user_data: app_storage[USER_DATA] = { diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index b9f93c295d6..09f9cf81cd1 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -2,7 +2,7 @@ "domain": "evohome", "name": "Honeywell Total Connect Comfort (Europe)", "documentation": "https://www.home-assistant.io/integrations/evohome", - "requirements": ["evohome-async==0.3.8"], + "requirements": ["evohome-async==0.3.15"], "codeowners": ["@zxdavb"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 692c4dbbc49..495df9e697e 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -112,6 +112,14 @@ class EvoDHW(EvoChild, WaterHeaterEntity): """Turn away mode off.""" await self._evo_broker.call_client_api(self._evo_device.set_dhw_auto()) + async def async_turn_on(self): + """Turn on.""" + await self._evo_broker.call_client_api(self._evo_device.set_dhw_on()) + + async def async_turn_off(self): + """Turn off.""" + await self._evo_broker.call_client_api(self._evo_device.set_dhw_off()) + async def async_update(self) -> None: """Get the latest state data for a DHW controller.""" await super().async_update() diff --git a/requirements_all.txt b/requirements_all.txt index 7a09974804f..45f42348920 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -581,7 +581,7 @@ eternalegypt==0.0.12 # evdev==1.4.0 # homeassistant.components.evohome -evohome-async==0.3.8 +evohome-async==0.3.15 # homeassistant.components.faa_delays faadelays==0.0.7 From 456c60061734ddb3492404248c03ba22a6c36d97 Mon Sep 17 00:00:00 2001 From: RogerSelwyn Date: Wed, 19 May 2021 10:43:41 +0100 Subject: [PATCH 571/852] Correct positioning of except statement in speedtestdotnet (#50852) --- .../components/speedtestdotnet/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 2c906448510..71e51c0959d 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -167,19 +167,19 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): try: self.api.get_best_server() - _LOGGER.debug( - "Executing speedtest.net speed test with server_id: %s", - self.api.best["id"], - ) - - self.api.download() - self.api.upload() - return self.api.results.dict() except speedtest.SpeedtestBestServerFailure as err: raise UpdateFailed( "Failed to retrieve best server for speedtest", err ) from err + _LOGGER.debug( + "Executing speedtest.net speed test with server_id: %s", + self.api.best["id"], + ) + self.api.download() + self.api.upload() + return self.api.results.dict() + async def async_update(self, *_): """Update Speedtest data.""" try: From d9a5e2cb68864a05951d5e698d21f88f79830fe4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 May 2021 12:38:29 +0200 Subject: [PATCH 572/852] Bump actions/stale from 3.0.18 to 3.0.19 (#50810) Bumps [actions/stale](https://github.com/actions/stale) from 3.0.18 to 3.0.19. - [Release notes](https://github.com/actions/stale/releases) - [Commits](https://github.com/actions/stale/compare/v3.0.18...v3.0.19) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 3ff0f47cedc..d41deb9ec92 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: # - No PRs marked as no-stale # - No issues marked as no-stale or help-wanted - name: 90 days stale issues & PRs policy - uses: actions/stale@v3.0.18 + uses: actions/stale@v3.0.19 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 @@ -53,7 +53,7 @@ jobs: # - No PRs marked as no-stale or new-integrations # - No issues (-1) - name: 30 days stale PRs policy - uses: actions/stale@v3.0.18 + uses: actions/stale@v3.0.19 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 @@ -78,7 +78,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v3.0.18 + uses: actions/stale@v3.0.19 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "needs-more-information" From c4ced2b351adcce8d470190bb9af64df92f25061 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 19 May 2021 13:53:26 +0300 Subject: [PATCH 573/852] Bump aioshelly to 0.6.3 (#50857) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 222d5b8b11f..fed55b2096a 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.6.2"], + "requirements": ["aioshelly==0.6.3"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 45f42348920..971f637bc39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -227,7 +227,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.4 # homeassistant.components.shelly -aioshelly==0.6.2 +aioshelly==0.6.3 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 567f9ca54a5..a3159aa20e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -149,7 +149,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.4 # homeassistant.components.shelly -aioshelly==0.6.2 +aioshelly==0.6.3 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From f1927026486f91207282cea82982a7188232f530 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 19 May 2021 13:20:11 +0200 Subject: [PATCH 574/852] Add Nettigo Air Monitor uptime sensor (#50834) --- homeassistant/components/nam/__init__.py | 2 +- homeassistant/components/nam/air_quality.py | 4 ++-- homeassistant/components/nam/const.py | 8 +++++++ homeassistant/components/nam/manifest.json | 2 +- homeassistant/components/nam/sensor.py | 25 ++++++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nam/__init__.py | 1 + tests/components/nam/test_sensor.py | 21 +++++++++++++++++ 9 files changed, 58 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 04458967bed..7dc6701217d 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -31,7 +31,7 @@ PLATFORMS = ["air_quality", "sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nettigo as config entry.""" - host = entry.data[CONF_HOST] + host: str = entry.data[CONF_HOST] websession = async_get_clientsession(hass) diff --git a/homeassistant/components/nam/air_quality.py b/homeassistant/components/nam/air_quality.py index 7823ffb110e..c39ad2bea73 100644 --- a/homeassistant/components/nam/air_quality.py +++ b/homeassistant/components/nam/air_quality.py @@ -19,9 +19,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add a Nettigo Air Monitor entities from a config_entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities = [] + entities: list[NAMAirQuality] = [] for sensor in AIR_QUALITY_SENSORS: if f"{sensor}{SUFFIX_P1}" in coordinator.data: entities.append(NAMAirQuality(coordinator, sensor)) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index b14bcaa6fa1..3800057d5d7 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -10,6 +10,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -127,4 +128,11 @@ SENSORS: Final[dict[str, SensorDescription]] = { "icon": None, "enabled": True, }, + "uptime": { + "label": f"{DEFAULT_NAME} Uptime", + "unit": None, + "device_class": DEVICE_CLASS_TIMESTAMP, + "icon": None, + "enabled": False, + }, } diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 80a31fe1596..3e03a0ad787 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -3,7 +3,7 @@ "name": "Nettigo Air Monitor", "documentation": "https://www.home-assistant.io/integrations/nam", "codeowners": ["@bieniu"], - "requirements": ["nettigo-air-monitor==0.2.5"], + "requirements": ["nettigo-air-monitor==0.2.6"], "zeroconf": [{"type": "_http._tcp.local.", "name": "nam-*"}], "config_flow": true, "quality_scale": "platinum", diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 39da7742bed..026e40483bd 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -1,6 +1,7 @@ """Support for the Nettigo Air Monitor service.""" from __future__ import annotations +from datetime import timedelta from typing import Any from homeassistant.components.sensor import SensorEntity @@ -9,6 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.dt import utcnow from . import NAMDataUpdateCoordinator from .const import DOMAIN, SENSORS @@ -20,12 +22,15 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add a Nettigo Air Monitor entities from a config_entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - sensors = [] + sensors: list[NAMSensor | NAMSensorUptime] = [] for sensor in SENSORS: if sensor in coordinator.data: - sensors.append(NAMSensor(coordinator, sensor)) + if sensor == "uptime": + sensors.append(NAMSensorUptime(coordinator, sensor)) + else: + sensors.append(NAMSensor(coordinator, sensor)) async_add_entities(sensors, False) @@ -92,3 +97,17 @@ class NAMSensor(CoordinatorEntity, SensorEntity): return available and bool( getattr(self.coordinator.data, self.sensor_type, None) ) + + +class NAMSensorUptime(NAMSensor): + """Define an Nettigo Air Monitor uptime sensor.""" + + @property + def state(self) -> str: + """Return the state.""" + uptime_sec = getattr(self.coordinator.data, self.sensor_type) + return ( + (utcnow() - timedelta(seconds=uptime_sec)) + .replace(microsecond=0) + .isoformat() + ) diff --git a/requirements_all.txt b/requirements_all.txt index 971f637bc39..b802a152efb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -997,7 +997,7 @@ netdata==0.2.0 netdisco==2.8.3 # homeassistant.components.nam -nettigo-air-monitor==0.2.5 +nettigo-air-monitor==0.2.6 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3159aa20e9..8bf2a07c97a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -551,7 +551,7 @@ nessclient==0.9.15 netdisco==2.8.3 # homeassistant.components.nam -nettigo-air-monitor==0.2.5 +nettigo-air-monitor==0.2.6 # homeassistant.components.nexia nexia==0.9.7 diff --git a/tests/components/nam/__init__.py b/tests/components/nam/__init__.py index 1b6f89b76df..b4a6ebbf792 100644 --- a/tests/components/nam/__init__.py +++ b/tests/components/nam/__init__.py @@ -12,6 +12,7 @@ INCOMPLETE_NAM_DATA = { nam_data = { "software_version": "NAMF-2020-36", + "uptime": "456987", "sensordatavalues": [ {"value_type": "SDS_P1", "value": "18.65"}, {"value_type": "SDS_P2", "value": "11.03"}, diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 148b048da90..2dfdc8987bc 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -42,6 +43,14 @@ async def test_sensor(hass): disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aa:bb:cc:dd:ee:ff-uptime", + suggested_object_id="nettigo_air_monitor_uptime", + disabled_by=None, + ) + await init_integration(hass) state = hass.states.get("sensor.nettigo_air_monitor_bme280_humidity") @@ -167,6 +176,18 @@ async def test_sensor(hass): assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-signal" + state = hass.states.get("sensor.nettigo_air_monitor_uptime") + assert state + assert ( + state.state + == (utcnow() - timedelta(seconds=456987)).replace(microsecond=0).isoformat() + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + + entry = registry.async_get("sensor.nettigo_air_monitor_uptime") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-uptime" + async def test_sensor_disabled(hass): """Test sensor disabled by default.""" From 109b08bb577a47a6dc4f2bfa39b5c497b4d104b2 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 19 May 2021 15:34:20 +0300 Subject: [PATCH 575/852] Remove MQTT cover deprecated options (#50263) * Remove MQTT cover deprecated options * Fix pylint --- homeassistant/components/mqtt/cover.py | 53 ++--- tests/components/mqtt/test_cover.py | 313 +++++++++++++------------ 2 files changed, 185 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index eb7c5a79da9..a8de06ff1ca 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -71,7 +71,6 @@ CONF_STATE_OPEN = "state_open" CONF_STATE_OPENING = "state_opening" CONF_STATE_STOPPED = "state_stopped" CONF_TILT_CLOSED_POSITION = "tilt_closed_value" -CONF_TILT_INVERT_STATE = "tilt_invert_state" CONF_TILT_MAX = "tilt_max" CONF_TILT_MIN = "tilt_min" CONF_TILT_OPEN_POSITION = "tilt_opened_value" @@ -90,7 +89,6 @@ DEFAULT_POSITION_OPEN = 100 DEFAULT_RETAIN = False DEFAULT_STATE_STOPPED = "stopped" DEFAULT_TILT_CLOSED_POSITION = 0 -DEFAULT_TILT_INVERT_STATE = False DEFAULT_TILT_MAX = 100 DEFAULT_TILT_MIN = 0 DEFAULT_TILT_OPEN_POSITION = 100 @@ -112,25 +110,34 @@ def validate_options(value): """ if CONF_SET_POSITION_TOPIC in value and CONF_GET_POSITION_TOPIC not in value: raise vol.Invalid( - "'set_position_topic' must be set together with 'position_topic'." + f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) - if ( - CONF_GET_POSITION_TOPIC in value - and CONF_STATE_TOPIC not in value - and CONF_VALUE_TEMPLATE in value - ): - _LOGGER.warning( - "Using 'value_template' for 'position_topic' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please replace it with 'position_template'" + # if templates are set make sure the topic for the template is also set + + if CONF_VALUE_TEMPLATE in value and CONF_STATE_TOPIC not in value: + raise vol.Invalid( + f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." ) - if CONF_TILT_INVERT_STATE in value: - _LOGGER.warning( - "'tilt_invert_state' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please invert tilt using 'tilt_min' & 'tilt_max'" + if CONF_GET_POSITION_TEMPLATE in value and CONF_GET_POSITION_TOPIC not in value: + raise vol.Invalid( + f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." + ) + + if CONF_SET_POSITION_TEMPLATE in value and CONF_SET_POSITION_TOPIC not in value: + raise vol.Invalid( + f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." + ) + + if CONF_TILT_COMMAND_TEMPLATE in value and CONF_TILT_COMMAND_TOPIC not in value: + raise vol.Invalid( + f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." + ) + + if CONF_TILT_STATUS_TEMPLATE in value and CONF_TILT_STATUS_TOPIC not in value: + raise vol.Invalid( + f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." ) return value @@ -164,7 +171,6 @@ PLATFORM_SCHEMA = vol.All( CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION ): int, vol.Optional(CONF_TILT_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_TILT_INVERT_STATE): cv.boolean, vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int, vol.Optional(CONF_TILT_MIN, default=DEFAULT_TILT_MIN): int, vol.Optional( @@ -332,12 +338,6 @@ class MqttCover(MqttEntity, CoverEntity): payload = msg.payload template = self._config.get(CONF_GET_POSITION_TEMPLATE) - - # To be removed in 2021.6: - # allow using `value_template` as position template if no `state_topic` - if template is None and self._config.get(CONF_STATE_TOPIC) is None: - template = self._config.get(CONF_VALUE_TEMPLATE) - if template is not None: variables = { "entity_id": self.entity_id, @@ -665,8 +665,7 @@ class MqttCover(MqttEntity, CoverEntity): max_percent = 100 min_percent = 0 position_percentage = min(max(position_percentage, min_percent), max_percent) - if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE): - return 100 - position_percentage + return position_percentage def find_in_range_from_percent(self, percentage, range_type=TILT_PAYLOAD): @@ -689,8 +688,6 @@ class MqttCover(MqttEntity, CoverEntity): position = round(current_range * (percentage / 100.0)) position += offset - if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE): - position = max_range - position + offset return position def tilt_payload_received(self, _payload): diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index d0665fba318..c9dd30038ab 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -10,10 +10,22 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, ) -from homeassistant.components.mqtt.cover import MqttCover +from homeassistant.components.mqtt.const import CONF_STATE_TOPIC +from homeassistant.components.mqtt.cover import ( + CONF_GET_POSITION_TEMPLATE, + CONF_GET_POSITION_TOPIC, + CONF_SET_POSITION_TEMPLATE, + CONF_SET_POSITION_TOPIC, + CONF_TILT_COMMAND_TEMPLATE, + CONF_TILT_COMMAND_TOPIC, + CONF_TILT_STATUS_TEMPLATE, + CONF_TILT_STATUS_TOPIC, + MqttCover, +) from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, + CONF_VALUE_TEMPLATE, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, @@ -338,43 +350,6 @@ async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): ) in caplog.text -async def test_position_via_template(hass, mqtt_mock): - """Test the controlling state via topic.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "qos": 0, - "value_template": "{{ (value | multiply(0.01)) | int }}", - } - }, - ) - await hass.async_block_till_done() - - state = hass.states.get("cover.test") - assert state.state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, "get-position-topic", "10000") - - state = hass.states.get("cover.test") - assert state.state == STATE_OPEN - - async_fire_mqtt_message(hass, "get-position-topic", "5000") - - state = hass.states.get("cover.test") - assert state.state == STATE_OPEN - - async_fire_mqtt_message(hass, "get-position-topic", "99") - - state = hass.states.get("cover.test") - assert state.state == STATE_CLOSED - - async def test_position_via_template_and_entity_id(hass, mqtt_mock): """Test the controlling state via topic.""" assert await async_setup_component( @@ -1899,7 +1874,6 @@ async def test_find_percentage_in_range_defaults(hass, mqtt_mock): "tilt_min": 0, "tilt_max": 100, "tilt_optimistic": False, - "tilt_invert_state": False, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -1943,7 +1917,6 @@ async def test_find_percentage_in_range_altered(hass, mqtt_mock): "tilt_min": 80, "tilt_max": 180, "tilt_optimistic": False, - "tilt_invert_state": False, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -1984,10 +1957,9 @@ async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): "value_template": None, "tilt_open_position": 100, "tilt_closed_position": 0, - "tilt_min": 0, - "tilt_max": 100, + "tilt_min": 100, + "tilt_max": 0, "tilt_optimistic": False, - "tilt_invert_state": True, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -2028,10 +2000,9 @@ async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): "value_template": None, "tilt_open_position": 180, "tilt_closed_position": 80, - "tilt_min": 80, - "tilt_max": 180, + "tilt_min": 180, + "tilt_max": 80, "tilt_optimistic": False, - "tilt_invert_state": True, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -2075,7 +2046,6 @@ async def test_find_in_range_defaults(hass, mqtt_mock): "tilt_min": 0, "tilt_max": 100, "tilt_optimistic": False, - "tilt_invert_state": False, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -2119,7 +2089,6 @@ async def test_find_in_range_altered(hass, mqtt_mock): "tilt_min": 80, "tilt_max": 180, "tilt_optimistic": False, - "tilt_invert_state": False, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -2160,10 +2129,9 @@ async def test_find_in_range_defaults_inverted(hass, mqtt_mock): "value_template": None, "tilt_open_position": 100, "tilt_closed_position": 0, - "tilt_min": 0, - "tilt_max": 100, + "tilt_min": 100, + "tilt_max": 0, "tilt_optimistic": False, - "tilt_invert_state": True, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -2204,10 +2172,9 @@ async def test_find_in_range_altered_inverted(hass, mqtt_mock): "value_template": None, "tilt_open_position": 180, "tilt_closed_position": 80, - "tilt_min": 80, - "tilt_max": 180, + "tilt_min": 180, + "tilt_max": 80, "tilt_optimistic": False, - "tilt_invert_state": True, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -2430,105 +2397,6 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_deprecated_value_template_for_position_topic_warning( - hass, caplog, mqtt_mock -): - """Test warning when value_template is used for position_topic.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "position-topic", - "value_template": "{{100-62}}", - } - }, - ) - await hass.async_block_till_done() - - assert ( - "Using 'value_template' for 'position_topic' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please replace it with 'position_template'" - ) in caplog.text - - -async def test_deprecated_tilt_invert_state_warning(hass, caplog, mqtt_mock): - """Test warning when tilt_invert_state is used.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "tilt_invert_state": True, - } - }, - ) - await hass.async_block_till_done() - - assert ( - "'tilt_invert_state' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please invert tilt using 'tilt_min' & 'tilt_max'" - ) in caplog.text - - -async def test_no_deprecated_tilt_invert_state_warning(hass, caplog, mqtt_mock): - """Test warning when tilt_invert_state is used.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - } - }, - ) - await hass.async_block_till_done() - - assert ( - "'tilt_invert_state' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please invert tilt using 'tilt_min' & 'tilt_max'" - ) not in caplog.text - - -async def test_no_deprecated_warning_for_position_topic_using_position_template( - hass, caplog, mqtt_mock -): - """Test no warning when position_template is used for position_topic.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "position-topic", - "position_template": "{{100-62}}", - } - }, - ) - await hass.async_block_till_done() - - assert ( - "using 'value_template' for 'position_topic' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please replace it with 'position_template'" - ) not in caplog.text - - async def test_state_and_position_topics_state_not_set_via_position_topic( hass, mqtt_mock ): @@ -2969,3 +2837,142 @@ async def test_position_via_position_topic_template_return_invalid_json( async_fire_mqtt_message(hass, "get-position-topic", "55") assert ("Payload '{'position': Undefined}' is not numeric") in caplog.text + + +async def test_set_position_topic_without_get_position_topic_error( + hass, caplog, mqtt_mock +): + """Test error when set_position_topic is used without position_topic.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "value_template": "{{100-62}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." + ) in caplog.text + + +async def test_value_template_without_state_topic_error(hass, caplog, mqtt_mock): + """Test error when value_template is used and state_topic is missing.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "value_template": "{{100-62}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." + ) in caplog.text + + +async def test_position_template_without_position_topic_error(hass, caplog, mqtt_mock): + """Test error when position_template is used and position_topic is missing.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "position_template": "{{100-52}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." + in caplog.text + ) + + +async def test_set_position_template_without_set_position_topic( + hass, caplog, mqtt_mock +): + """Test error when set_position_template is used and set_position_topic is missing.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "set_position_template": "{{100-42}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." + in caplog.text + ) + + +async def test_tilt_command_template_without_tilt_command_topic( + hass, caplog, mqtt_mock +): + """Test error when tilt_command_template is used and tilt_command_topic is missing.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "tilt_command_template": "{{100-32}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." + in caplog.text + ) + + +async def test_tilt_status_template_without_tilt_status_topic_topic( + hass, caplog, mqtt_mock +): + """Test error when tilt_status_template is used and tilt_status_topic is missing.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "tilt_status_template": "{{100-22}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." + in caplog.text + ) From 7c7432a582801d32f2973e92828bd7d7707c6a62 Mon Sep 17 00:00:00 2001 From: Michael Klamminger <6277211+m1ch@users.noreply.github.com> Date: Wed, 19 May 2021 14:38:18 +0200 Subject: [PATCH 576/852] Add entity_id to mqtt sensor templates (#50773) * Add entity_id to mqtt sensor * update test comment --- homeassistant/components/mqtt/sensor.py | 5 ++++- tests/components/mqtt/test_sensor.py | 28 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 2dcdce9e019..ca399161b25 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -125,8 +125,11 @@ class MqttSensor(MqttEntity, SensorEntity): template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: + variables = {"entity_id": self.entity_id} payload = template.async_render_with_possible_json_value( - payload, self._state + payload, + self._state, + variables=variables, ) self._state = payload self.async_write_ha_state() diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 373048f6f1a..c6ebbe98dc4 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -640,3 +640,31 @@ async def test_entity_disabled_by_default(hass, mqtt_mock): await help_test_entity_disabled_by_default( hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG ) + + +async def test_value_template_with_entity_id(hass, mqtt_mock): + """Test the access to attributes in value_template via the entity_id.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "value_template": '\ + {% if state_attr(entity_id, "friendly_name") == "test" %} \ + {{ value | int + 1 }} \ + {% else %} \ + {{ value }} \ + {% endif %}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test-topic", "100") + state = hass.states.get("sensor.test") + + assert state.state == "101" From 892a2a0372f0d1bdcac4b2854d7a200c590a1e85 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 19 May 2021 15:05:29 +0200 Subject: [PATCH 577/852] Push modbus switch to 100% test coverage (#50324) push modbus switch to 100% test coverage. --- .coveragerc | 1 - tests/components/modbus/test_modbus_switch.py | 235 ++++++++++-------- 2 files changed, 138 insertions(+), 98 deletions(-) diff --git a/.coveragerc b/.coveragerc index f51217c40a2..d4116e3ab46 100644 --- a/.coveragerc +++ b/.coveragerc @@ -631,7 +631,6 @@ omit = homeassistant/components/mochad/* homeassistant/components/modbus/climate.py homeassistant/components/modbus/cover.py - homeassistant/components/modbus/switch.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py index 876259d1fc2..8cb1cf69dc9 100644 --- a/tests/components/modbus/test_modbus_switch.py +++ b/tests/components/modbus/test_modbus_switch.py @@ -1,4 +1,5 @@ """The tests for the Modbus switch component.""" +from pymodbus.exceptions import ModbusException import pytest from homeassistant.components.modbus.const import ( @@ -6,12 +7,12 @@ from homeassistant.components.modbus.const import ( CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, - CONF_COILS, CONF_INPUT_TYPE, CONF_STATE_OFF, CONF_STATE_ON, CONF_VERIFY, CONF_WRITE_TYPE, + MODBUS_DOMAIN, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -19,15 +20,23 @@ from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_DEVICE_CLASS, + CONF_HOST, CONF_NAME, + CONF_PORT, CONF_SLAVE, CONF_SWITCHES, + CONF_TYPE, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) +from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from tests.common import mock_restore_cache + @pytest.mark.parametrize( "do_config", @@ -65,6 +74,27 @@ from .conftest import ReadResult, base_config_test, base_test, prepare_service_u CONF_STATE_ON: 1, }, }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_DEVICE_CLASS: "switch", + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_DEVICE_CLASS: "switch", + CONF_VERIFY: None, + }, ], ) async def test_config_switch(hass, do_config): @@ -87,80 +117,38 @@ async def test_config_switch(hass, do_config): ) +@pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) @pytest.mark.parametrize( - "regs,expected", + "regs,verify,expected", [ ( [0x00], - STATE_OFF, - ), - ( - [0x80], - STATE_OFF, - ), - ( - [0xFE], - STATE_OFF, - ), - ( - [0xFF], - STATE_ON, - ), - ( - [0x01], - STATE_ON, - ), - ], -) -async def test_coil_switch(hass, regs, expected): - """Run test for given config.""" - switch_name = "modbus_test_switch" - state = await base_test( - hass, - { - CONF_NAME: switch_name, - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, - CONF_VERIFY: {}, - }, - switch_name, - SWITCH_DOMAIN, - CONF_SWITCHES, - CONF_COILS, - regs, - expected, - method_discovery=True, - scan_interval=5, - ) - assert state == expected - - -@pytest.mark.parametrize( - "regs,expected", - [ - ( - [0x00], - STATE_OFF, - ), - ( - [0x80], - STATE_OFF, - ), - ( - [0xFE], - STATE_OFF, - ), - ( - [0xFF], + {CONF_VERIFY: {}}, STATE_OFF, ), ( [0x01], + {CONF_VERIFY: {}}, STATE_ON, ), + ( + [0xFE], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + None, + {CONF_VERIFY: {}}, + STATE_UNAVAILABLE, + ), + ( + None, + {}, + STATE_OFF, + ), ], ) -async def test_register_switch(hass, regs, expected): +async def test_all_switch(hass, call_type, regs, verify, expected): """Run test for given config.""" switch_name = "modbus_test_switch" state = await base_test( @@ -169,9 +157,8 @@ async def test_register_switch(hass, regs, expected): CONF_NAME: switch_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY: {}, + CONF_WRITE_TYPE: call_type, + **verify, }, switch_name, SWITCH_DOMAIN, @@ -185,42 +172,96 @@ async def test_register_switch(hass, regs, expected): assert state == expected -@pytest.mark.parametrize( - "regs,expected", - [ - ( - [0x40], - STATE_ON, - ), - ( - [0x04], - STATE_OFF, - ), - ], -) -async def test_register_state_switch(hass, regs, expected): - """Run test for given config.""" - switch_name = "modbus_test_switch" - state = await base_test( +async def test_restore_state_switch(hass): + """Run test for sensor restore state.""" + + switch_name = "test_switch" + entity_id = f"{SWITCH_DOMAIN}.{switch_name}" + test_value = STATE_ON + config_switch = {CONF_NAME: switch_name, CONF_ADDRESS: 17} + mock_restore_cache( hass, - { - CONF_NAME: switch_name, - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x04, - CONF_COMMAND_ON: 0x40, - CONF_VERIFY: {}, - }, + (State(f"{entity_id}", test_value),), + ) + await base_config_test( + hass, + config_switch, switch_name, SWITCH_DOMAIN, CONF_SWITCHES, None, - regs, - expected, method_discovery=True, - scan_interval=5, ) - assert state == expected + assert hass.states.get(entity_id).state == test_value + + +async def test_switch_service_turn(hass, caplog, mock_pymodbus): + """Run test for service turn_on/turn_off.""" + + entity_id1 = f"{SWITCH_DOMAIN}.switch1" + entity_id2 = f"{SWITCH_DOMAIN}.switch2" + config = { + MODBUS_DOMAIN: { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_SWITCHES: [ + { + CONF_NAME: "switch1", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + { + CONF_NAME: "switch2", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_VERIFY: {}, + }, + ], + }, + } + assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True + await hass.async_block_till_done() + assert MODBUS_DOMAIN in hass.config.components + + assert hass.states.get(entity_id1).state == STATE_OFF + await hass.services.async_call( + "switch", "turn_on", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_ON + await hass.services.async_call( + "switch", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_OFF + + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + assert hass.states.get(entity_id2).state == STATE_OFF + await hass.services.async_call( + "switch", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_ON + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + await hass.services.async_call( + "switch", "turn_off", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_OFF + + mock_pymodbus.write_register.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "switch", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE + mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "switch", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE async def test_service_switch_update(hass, mock_pymodbus): @@ -233,7 +274,7 @@ async def test_service_switch_update(hass, mock_pymodbus): CONF_NAME: "test", CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, - CONF_VERIFY: {CONF_INPUT_TYPE: CALL_TYPE_DISCRETE}, + CONF_VERIFY: {}, } ] } @@ -246,7 +287,7 @@ async def test_service_switch_update(hass, mock_pymodbus): "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == STATE_ON - mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x00]) + mock_pymodbus.read_coils.return_value = ReadResult([0x00]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) From 7573adda7f28d0c00a13fe40fa9077991e3fe748 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 19 May 2021 18:31:38 +0200 Subject: [PATCH 578/852] Add `Final` type for all core constants (#50858) * Add Final type for all constants * Add Final for one missing const * Suggested change --- homeassistant/const.py | 956 +++++++++++++++++++++-------------------- 1 file changed, 479 insertions(+), 477 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8b965995754..b0ebd6781de 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,294 +1,296 @@ """Constants used by Home Assistant components.""" +from __future__ import annotations + from typing import Final -MAJOR_VERSION = 2021 -MINOR_VERSION = 6 -PATCH_VERSION = "0.dev0" -__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" -__version__ = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER = (3, 8, 0) +MAJOR_VERSION: Final = 2021 +MINOR_VERSION: Final = 6 +PATCH_VERSION: Final = "0.dev0" +__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" +__version__: Final = f"{__short_version__}.{PATCH_VERSION}" +REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_VER = (3, 9, 0) -REQUIRED_NEXT_PYTHON_DATE = "" +REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) +REQUIRED_NEXT_PYTHON_DATE: Final = "" # Format for platform files -PLATFORM_FORMAT = "{platform}.{domain}" +PLATFORM_FORMAT: Final = "{platform}.{domain}" # Can be used to specify a catch all when registering state or event listeners. -MATCH_ALL = "*" +MATCH_ALL: Final = "*" # Entity target all constant -ENTITY_MATCH_NONE = "none" -ENTITY_MATCH_ALL = "all" +ENTITY_MATCH_NONE: Final = "none" +ENTITY_MATCH_ALL: Final = "all" # If no name is specified -DEVICE_DEFAULT_NAME = "Unnamed Device" +DEVICE_DEFAULT_NAME: Final = "Unnamed Device" # Max characters for an event_type (changing this requires a recorder # database migration) -MAX_LENGTH_EVENT_TYPE = 64 +MAX_LENGTH_EVENT_TYPE: Final = 64 # Sun events -SUN_EVENT_SUNSET = "sunset" -SUN_EVENT_SUNRISE = "sunrise" +SUN_EVENT_SUNSET: Final = "sunset" +SUN_EVENT_SUNRISE: Final = "sunrise" # #### CONFIG #### -CONF_ABOVE = "above" -CONF_ACCESS_TOKEN = "access_token" -CONF_ADDRESS = "address" -CONF_AFTER = "after" -CONF_ALIAS = "alias" -CONF_ALLOWLIST_EXTERNAL_URLS = "allowlist_external_urls" -CONF_API_KEY = "api_key" -CONF_API_TOKEN = "api_token" -CONF_API_VERSION = "api_version" -CONF_ARMING_TIME = "arming_time" -CONF_AT = "at" -CONF_ATTRIBUTE = "attribute" -CONF_AUTH_MFA_MODULES = "auth_mfa_modules" -CONF_AUTH_PROVIDERS = "auth_providers" -CONF_AUTHENTICATION = "authentication" -CONF_BASE = "base" -CONF_BEFORE = "before" -CONF_BELOW = "below" -CONF_BINARY_SENSORS = "binary_sensors" -CONF_BRIGHTNESS = "brightness" -CONF_BROADCAST_ADDRESS = "broadcast_address" -CONF_BROADCAST_PORT = "broadcast_port" -CONF_CHOOSE = "choose" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" -CONF_CODE = "code" -CONF_COLOR_TEMP = "color_temp" -CONF_COMMAND = "command" -CONF_COMMAND_CLOSE = "command_close" -CONF_COMMAND_OFF = "command_off" -CONF_COMMAND_ON = "command_on" -CONF_COMMAND_OPEN = "command_open" -CONF_COMMAND_STATE = "command_state" -CONF_COMMAND_STOP = "command_stop" -CONF_CONDITION = "condition" -CONF_CONDITIONS = "conditions" -CONF_CONTINUE_ON_TIMEOUT = "continue_on_timeout" -CONF_COUNT = "count" -CONF_COVERS = "covers" -CONF_CURRENCY = "currency" -CONF_CUSTOMIZE = "customize" -CONF_CUSTOMIZE_DOMAIN = "customize_domain" -CONF_CUSTOMIZE_GLOB = "customize_glob" -CONF_DEFAULT = "default" -CONF_DELAY = "delay" -CONF_DELAY_TIME = "delay_time" -CONF_DESCRIPTION = "description" -CONF_DEVICE = "device" -CONF_DEVICES = "devices" -CONF_DEVICE_CLASS = "device_class" -CONF_DEVICE_ID = "device_id" -CONF_DISARM_AFTER_TRIGGER = "disarm_after_trigger" -CONF_DISCOVERY = "discovery" -CONF_DISKS = "disks" -CONF_DISPLAY_CURRENCY = "display_currency" -CONF_DISPLAY_OPTIONS = "display_options" -CONF_DOMAIN = "domain" -CONF_DOMAINS = "domains" -CONF_EFFECT = "effect" -CONF_ELEVATION = "elevation" -CONF_EMAIL = "email" -CONF_ENTITIES = "entities" -CONF_ENTITY_ID = "entity_id" -CONF_ENTITY_NAMESPACE = "entity_namespace" -CONF_ENTITY_PICTURE_TEMPLATE = "entity_picture_template" -CONF_EVENT = "event" -CONF_EVENT_DATA = "event_data" -CONF_EVENT_DATA_TEMPLATE = "event_data_template" -CONF_EXCLUDE = "exclude" -CONF_EXTERNAL_URL = "external_url" -CONF_FILENAME = "filename" -CONF_FILE_PATH = "file_path" -CONF_FOR = "for" -CONF_FORCE_UPDATE = "force_update" -CONF_FRIENDLY_NAME = "friendly_name" -CONF_FRIENDLY_NAME_TEMPLATE = "friendly_name_template" -CONF_HEADERS = "headers" -CONF_HOST = "host" -CONF_HOSTS = "hosts" -CONF_HS = "hs" -CONF_ICON = "icon" -CONF_ICON_TEMPLATE = "icon_template" -CONF_ID = "id" -CONF_INCLUDE = "include" -CONF_INTERNAL_URL = "internal_url" -CONF_IP_ADDRESS = "ip_address" -CONF_LATITUDE = "latitude" -CONF_LEGACY_TEMPLATES = "legacy_templates" -CONF_LIGHTS = "lights" -CONF_LONGITUDE = "longitude" -CONF_MAC = "mac" -CONF_MAXIMUM = "maximum" -CONF_MEDIA_DIRS = "media_dirs" -CONF_METHOD = "method" -CONF_MINIMUM = "minimum" -CONF_MODE = "mode" -CONF_MONITORED_CONDITIONS = "monitored_conditions" -CONF_MONITORED_VARIABLES = "monitored_variables" -CONF_NAME = "name" -CONF_OFFSET = "offset" -CONF_OPTIMISTIC = "optimistic" -CONF_PACKAGES = "packages" -CONF_PARAMS = "params" -CONF_PASSWORD = "password" -CONF_PATH = "path" -CONF_PAYLOAD = "payload" -CONF_PAYLOAD_OFF = "payload_off" -CONF_PAYLOAD_ON = "payload_on" -CONF_PENDING_TIME = "pending_time" -CONF_PIN = "pin" -CONF_PLATFORM = "platform" -CONF_PORT = "port" -CONF_PREFIX = "prefix" -CONF_PROFILE_NAME = "profile_name" -CONF_PROTOCOL = "protocol" -CONF_PROXY_SSL = "proxy_ssl" -CONF_QUOTE = "quote" -CONF_RADIUS = "radius" -CONF_RECIPIENT = "recipient" -CONF_REGION = "region" -CONF_REPEAT = "repeat" -CONF_RESOURCE = "resource" -CONF_RESOURCES = "resources" -CONF_RESOURCE_TEMPLATE = "resource_template" -CONF_RGB = "rgb" -CONF_ROOM = "room" -CONF_SCAN_INTERVAL = "scan_interval" -CONF_SCENE = "scene" -CONF_SELECTOR = "selector" -CONF_SENDER = "sender" -CONF_SENSORS = "sensors" -CONF_SENSOR_TYPE = "sensor_type" -CONF_SEQUENCE = "sequence" -CONF_SERVICE = "service" -CONF_SERVICE_DATA = "data" -CONF_SERVICE_TEMPLATE = "service_template" -CONF_SHOW_ON_MAP = "show_on_map" -CONF_SLAVE = "slave" -CONF_SOURCE = "source" -CONF_SSL = "ssl" -CONF_STATE = "state" -CONF_STATE_TEMPLATE = "state_template" -CONF_STRUCTURE = "structure" -CONF_SWITCHES = "switches" -CONF_TARGET = "target" -CONF_TEMPERATURE_UNIT = "temperature_unit" -CONF_TIMEOUT = "timeout" -CONF_TIME_ZONE = "time_zone" -CONF_TOKEN = "token" -CONF_TRIGGER_TIME = "trigger_time" -CONF_TTL = "ttl" -CONF_TYPE = "type" -CONF_UNIQUE_ID = "unique_id" -CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement" -CONF_UNIT_SYSTEM = "unit_system" -CONF_UNTIL = "until" -CONF_URL = "url" -CONF_USERNAME = "username" -CONF_VALUE_TEMPLATE = "value_template" -CONF_VARIABLES = "variables" -CONF_VERIFY_SSL = "verify_ssl" -CONF_WAIT_FOR_TRIGGER = "wait_for_trigger" -CONF_WAIT_TEMPLATE = "wait_template" -CONF_WEBHOOK_ID = "webhook_id" -CONF_WEEKDAY = "weekday" -CONF_WHILE = "while" -CONF_WHITELIST = "whitelist" -CONF_ALLOWLIST_EXTERNAL_DIRS = "allowlist_external_dirs" -LEGACY_CONF_WHITELIST_EXTERNAL_DIRS = "whitelist_external_dirs" -CONF_WHITE_VALUE = "white_value" -CONF_XY = "xy" -CONF_ZONE = "zone" +CONF_ABOVE: Final = "above" +CONF_ACCESS_TOKEN: Final = "access_token" +CONF_ADDRESS: Final = "address" +CONF_AFTER: Final = "after" +CONF_ALIAS: Final = "alias" +CONF_ALLOWLIST_EXTERNAL_URLS: Final = "allowlist_external_urls" +CONF_API_KEY: Final = "api_key" +CONF_API_TOKEN: Final = "api_token" +CONF_API_VERSION: Final = "api_version" +CONF_ARMING_TIME: Final = "arming_time" +CONF_AT: Final = "at" +CONF_ATTRIBUTE: Final = "attribute" +CONF_AUTH_MFA_MODULES: Final = "auth_mfa_modules" +CONF_AUTH_PROVIDERS: Final = "auth_providers" +CONF_AUTHENTICATION: Final = "authentication" +CONF_BASE: Final = "base" +CONF_BEFORE: Final = "before" +CONF_BELOW: Final = "below" +CONF_BINARY_SENSORS: Final = "binary_sensors" +CONF_BRIGHTNESS: Final = "brightness" +CONF_BROADCAST_ADDRESS: Final = "broadcast_address" +CONF_BROADCAST_PORT: Final = "broadcast_port" +CONF_CHOOSE: Final = "choose" +CONF_CLIENT_ID: Final = "client_id" +CONF_CLIENT_SECRET: Final = "client_secret" +CONF_CODE: Final = "code" +CONF_COLOR_TEMP: Final = "color_temp" +CONF_COMMAND: Final = "command" +CONF_COMMAND_CLOSE: Final = "command_close" +CONF_COMMAND_OFF: Final = "command_off" +CONF_COMMAND_ON: Final = "command_on" +CONF_COMMAND_OPEN: Final = "command_open" +CONF_COMMAND_STATE: Final = "command_state" +CONF_COMMAND_STOP: Final = "command_stop" +CONF_CONDITION: Final = "condition" +CONF_CONDITIONS: Final = "conditions" +CONF_CONTINUE_ON_TIMEOUT: Final = "continue_on_timeout" +CONF_COUNT: Final = "count" +CONF_COVERS: Final = "covers" +CONF_CURRENCY: Final = "currency" +CONF_CUSTOMIZE: Final = "customize" +CONF_CUSTOMIZE_DOMAIN: Final = "customize_domain" +CONF_CUSTOMIZE_GLOB: Final = "customize_glob" +CONF_DEFAULT: Final = "default" +CONF_DELAY: Final = "delay" +CONF_DELAY_TIME: Final = "delay_time" +CONF_DESCRIPTION: Final = "description" +CONF_DEVICE: Final = "device" +CONF_DEVICES: Final = "devices" +CONF_DEVICE_CLASS: Final = "device_class" +CONF_DEVICE_ID: Final = "device_id" +CONF_DISARM_AFTER_TRIGGER: Final = "disarm_after_trigger" +CONF_DISCOVERY: Final = "discovery" +CONF_DISKS: Final = "disks" +CONF_DISPLAY_CURRENCY: Final = "display_currency" +CONF_DISPLAY_OPTIONS: Final = "display_options" +CONF_DOMAIN: Final = "domain" +CONF_DOMAINS: Final = "domains" +CONF_EFFECT: Final = "effect" +CONF_ELEVATION: Final = "elevation" +CONF_EMAIL: Final = "email" +CONF_ENTITIES: Final = "entities" +CONF_ENTITY_ID: Final = "entity_id" +CONF_ENTITY_NAMESPACE: Final = "entity_namespace" +CONF_ENTITY_PICTURE_TEMPLATE: Final = "entity_picture_template" +CONF_EVENT: Final = "event" +CONF_EVENT_DATA: Final = "event_data" +CONF_EVENT_DATA_TEMPLATE: Final = "event_data_template" +CONF_EXCLUDE: Final = "exclude" +CONF_EXTERNAL_URL: Final = "external_url" +CONF_FILENAME: Final = "filename" +CONF_FILE_PATH: Final = "file_path" +CONF_FOR: Final = "for" +CONF_FORCE_UPDATE: Final = "force_update" +CONF_FRIENDLY_NAME: Final = "friendly_name" +CONF_FRIENDLY_NAME_TEMPLATE: Final = "friendly_name_template" +CONF_HEADERS: Final = "headers" +CONF_HOST: Final = "host" +CONF_HOSTS: Final = "hosts" +CONF_HS: Final = "hs" +CONF_ICON: Final = "icon" +CONF_ICON_TEMPLATE: Final = "icon_template" +CONF_ID: Final = "id" +CONF_INCLUDE: Final = "include" +CONF_INTERNAL_URL: Final = "internal_url" +CONF_IP_ADDRESS: Final = "ip_address" +CONF_LATITUDE: Final = "latitude" +CONF_LEGACY_TEMPLATES: Final = "legacy_templates" +CONF_LIGHTS: Final = "lights" +CONF_LONGITUDE: Final = "longitude" +CONF_MAC: Final = "mac" +CONF_MAXIMUM: Final = "maximum" +CONF_MEDIA_DIRS: Final = "media_dirs" +CONF_METHOD: Final = "method" +CONF_MINIMUM: Final = "minimum" +CONF_MODE: Final = "mode" +CONF_MONITORED_CONDITIONS: Final = "monitored_conditions" +CONF_MONITORED_VARIABLES: Final = "monitored_variables" +CONF_NAME: Final = "name" +CONF_OFFSET: Final = "offset" +CONF_OPTIMISTIC: Final = "optimistic" +CONF_PACKAGES: Final = "packages" +CONF_PARAMS: Final = "params" +CONF_PASSWORD: Final = "password" +CONF_PATH: Final = "path" +CONF_PAYLOAD: Final = "payload" +CONF_PAYLOAD_OFF: Final = "payload_off" +CONF_PAYLOAD_ON: Final = "payload_on" +CONF_PENDING_TIME: Final = "pending_time" +CONF_PIN: Final = "pin" +CONF_PLATFORM: Final = "platform" +CONF_PORT: Final = "port" +CONF_PREFIX: Final = "prefix" +CONF_PROFILE_NAME: Final = "profile_name" +CONF_PROTOCOL: Final = "protocol" +CONF_PROXY_SSL: Final = "proxy_ssl" +CONF_QUOTE: Final = "quote" +CONF_RADIUS: Final = "radius" +CONF_RECIPIENT: Final = "recipient" +CONF_REGION: Final = "region" +CONF_REPEAT: Final = "repeat" +CONF_RESOURCE: Final = "resource" +CONF_RESOURCES: Final = "resources" +CONF_RESOURCE_TEMPLATE: Final = "resource_template" +CONF_RGB: Final = "rgb" +CONF_ROOM: Final = "room" +CONF_SCAN_INTERVAL: Final = "scan_interval" +CONF_SCENE: Final = "scene" +CONF_SELECTOR: Final = "selector" +CONF_SENDER: Final = "sender" +CONF_SENSORS: Final = "sensors" +CONF_SENSOR_TYPE: Final = "sensor_type" +CONF_SEQUENCE: Final = "sequence" +CONF_SERVICE: Final = "service" +CONF_SERVICE_DATA: Final = "data" +CONF_SERVICE_TEMPLATE: Final = "service_template" +CONF_SHOW_ON_MAP: Final = "show_on_map" +CONF_SLAVE: Final = "slave" +CONF_SOURCE: Final = "source" +CONF_SSL: Final = "ssl" +CONF_STATE: Final = "state" +CONF_STATE_TEMPLATE: Final = "state_template" +CONF_STRUCTURE: Final = "structure" +CONF_SWITCHES: Final = "switches" +CONF_TARGET: Final = "target" +CONF_TEMPERATURE_UNIT: Final = "temperature_unit" +CONF_TIMEOUT: Final = "timeout" +CONF_TIME_ZONE: Final = "time_zone" +CONF_TOKEN: Final = "token" +CONF_TRIGGER_TIME: Final = "trigger_time" +CONF_TTL: Final = "ttl" +CONF_TYPE: Final = "type" +CONF_UNIQUE_ID: Final = "unique_id" +CONF_UNIT_OF_MEASUREMENT: Final = "unit_of_measurement" +CONF_UNIT_SYSTEM: Final = "unit_system" +CONF_UNTIL: Final = "until" +CONF_URL: Final = "url" +CONF_USERNAME: Final = "username" +CONF_VALUE_TEMPLATE: Final = "value_template" +CONF_VARIABLES: Final = "variables" +CONF_VERIFY_SSL: Final = "verify_ssl" +CONF_WAIT_FOR_TRIGGER: Final = "wait_for_trigger" +CONF_WAIT_TEMPLATE: Final = "wait_template" +CONF_WEBHOOK_ID: Final = "webhook_id" +CONF_WEEKDAY: Final = "weekday" +CONF_WHILE: Final = "while" +CONF_WHITELIST: Final = "whitelist" +CONF_ALLOWLIST_EXTERNAL_DIRS: Final = "allowlist_external_dirs" +LEGACY_CONF_WHITELIST_EXTERNAL_DIRS: Final = "whitelist_external_dirs" +CONF_WHITE_VALUE: Final = "white_value" +CONF_XY: Final = "xy" +CONF_ZONE: Final = "zone" # #### EVENTS #### -EVENT_CALL_SERVICE = "call_service" -EVENT_COMPONENT_LOADED = "component_loaded" -EVENT_CORE_CONFIG_UPDATE = "core_config_updated" -EVENT_HOMEASSISTANT_CLOSE = "homeassistant_close" -EVENT_HOMEASSISTANT_START = "homeassistant_start" -EVENT_HOMEASSISTANT_STARTED = "homeassistant_started" -EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" -EVENT_HOMEASSISTANT_FINAL_WRITE = "homeassistant_final_write" -EVENT_LOGBOOK_ENTRY = "logbook_entry" -EVENT_SERVICE_REGISTERED = "service_registered" -EVENT_SERVICE_REMOVED = "service_removed" -EVENT_STATE_CHANGED = "state_changed" -EVENT_THEMES_UPDATED = "themes_updated" -EVENT_TIMER_OUT_OF_SYNC = "timer_out_of_sync" -EVENT_TIME_CHANGED = "time_changed" +EVENT_CALL_SERVICE: Final = "call_service" +EVENT_COMPONENT_LOADED: Final = "component_loaded" +EVENT_CORE_CONFIG_UPDATE: Final = "core_config_updated" +EVENT_HOMEASSISTANT_CLOSE: Final = "homeassistant_close" +EVENT_HOMEASSISTANT_START: Final = "homeassistant_start" +EVENT_HOMEASSISTANT_STARTED: Final = "homeassistant_started" +EVENT_HOMEASSISTANT_STOP: Final = "homeassistant_stop" +EVENT_HOMEASSISTANT_FINAL_WRITE: Final = "homeassistant_final_write" +EVENT_LOGBOOK_ENTRY: Final = "logbook_entry" +EVENT_SERVICE_REGISTERED: Final = "service_registered" +EVENT_SERVICE_REMOVED: Final = "service_removed" +EVENT_STATE_CHANGED: Final = "state_changed" +EVENT_THEMES_UPDATED: Final = "themes_updated" +EVENT_TIMER_OUT_OF_SYNC: Final = "timer_out_of_sync" +EVENT_TIME_CHANGED: Final = "time_changed" # #### DEVICE CLASSES #### -DEVICE_CLASS_BATTERY = "battery" -DEVICE_CLASS_CO = "carbon_monoxide" -DEVICE_CLASS_CO2 = "carbon_dioxide" -DEVICE_CLASS_HUMIDITY = "humidity" -DEVICE_CLASS_ILLUMINANCE = "illuminance" -DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" -DEVICE_CLASS_TEMPERATURE = "temperature" -DEVICE_CLASS_TIMESTAMP = "timestamp" -DEVICE_CLASS_PRESSURE = "pressure" -DEVICE_CLASS_POWER = "power" -DEVICE_CLASS_CURRENT = "current" -DEVICE_CLASS_ENERGY = "energy" -DEVICE_CLASS_POWER_FACTOR = "power_factor" -DEVICE_CLASS_VOLTAGE = "voltage" +DEVICE_CLASS_BATTERY: Final = "battery" +DEVICE_CLASS_CO: Final = "carbon_monoxide" +DEVICE_CLASS_CO2: Final = "carbon_dioxide" +DEVICE_CLASS_HUMIDITY: Final = "humidity" +DEVICE_CLASS_ILLUMINANCE: Final = "illuminance" +DEVICE_CLASS_SIGNAL_STRENGTH: Final = "signal_strength" +DEVICE_CLASS_TEMPERATURE: Final = "temperature" +DEVICE_CLASS_TIMESTAMP: Final = "timestamp" +DEVICE_CLASS_PRESSURE: Final = "pressure" +DEVICE_CLASS_POWER: Final = "power" +DEVICE_CLASS_CURRENT: Final = "current" +DEVICE_CLASS_ENERGY: Final = "energy" +DEVICE_CLASS_POWER_FACTOR: Final = "power_factor" +DEVICE_CLASS_VOLTAGE: Final = "voltage" # #### STATES #### -STATE_ON = "on" -STATE_OFF = "off" -STATE_HOME = "home" -STATE_NOT_HOME = "not_home" -STATE_UNKNOWN = "unknown" -STATE_OPEN = "open" -STATE_OPENING = "opening" -STATE_CLOSED = "closed" -STATE_CLOSING = "closing" -STATE_PLAYING = "playing" -STATE_PAUSED = "paused" -STATE_IDLE = "idle" -STATE_STANDBY = "standby" -STATE_ALARM_DISARMED = "disarmed" -STATE_ALARM_ARMED_HOME = "armed_home" -STATE_ALARM_ARMED_AWAY = "armed_away" -STATE_ALARM_ARMED_NIGHT = "armed_night" -STATE_ALARM_ARMED_CUSTOM_BYPASS = "armed_custom_bypass" -STATE_ALARM_PENDING = "pending" -STATE_ALARM_ARMING = "arming" -STATE_ALARM_DISARMING = "disarming" -STATE_ALARM_TRIGGERED = "triggered" -STATE_LOCKED = "locked" -STATE_UNLOCKED = "unlocked" -STATE_UNAVAILABLE = "unavailable" -STATE_OK = "ok" -STATE_PROBLEM = "problem" +STATE_ON: Final = "on" +STATE_OFF: Final = "off" +STATE_HOME: Final = "home" +STATE_NOT_HOME: Final = "not_home" +STATE_UNKNOWN: Final = "unknown" +STATE_OPEN: Final = "open" +STATE_OPENING: Final = "opening" +STATE_CLOSED: Final = "closed" +STATE_CLOSING: Final = "closing" +STATE_PLAYING: Final = "playing" +STATE_PAUSED: Final = "paused" +STATE_IDLE: Final = "idle" +STATE_STANDBY: Final = "standby" +STATE_ALARM_DISARMED: Final = "disarmed" +STATE_ALARM_ARMED_HOME: Final = "armed_home" +STATE_ALARM_ARMED_AWAY: Final = "armed_away" +STATE_ALARM_ARMED_NIGHT: Final = "armed_night" +STATE_ALARM_ARMED_CUSTOM_BYPASS: Final = "armed_custom_bypass" +STATE_ALARM_PENDING: Final = "pending" +STATE_ALARM_ARMING: Final = "arming" +STATE_ALARM_DISARMING: Final = "disarming" +STATE_ALARM_TRIGGERED: Final = "triggered" +STATE_LOCKED: Final = "locked" +STATE_UNLOCKED: Final = "unlocked" +STATE_UNAVAILABLE: Final = "unavailable" +STATE_OK: Final = "ok" +STATE_PROBLEM: Final = "problem" # #### STATE AND EVENT ATTRIBUTES #### # Attribution -ATTR_ATTRIBUTION = "attribution" +ATTR_ATTRIBUTION: Final = "attribution" # Credentials -ATTR_CREDENTIALS = "credentials" +ATTR_CREDENTIALS: Final = "credentials" # Contains time-related attributes -ATTR_NOW = "now" -ATTR_DATE = "date" -ATTR_TIME = "time" -ATTR_SECONDS = "seconds" +ATTR_NOW: Final = "now" +ATTR_DATE: Final = "date" +ATTR_TIME: Final = "time" +ATTR_SECONDS: Final = "seconds" # Contains domain, service for a SERVICE_CALL event -ATTR_DOMAIN = "domain" -ATTR_SERVICE = "service" -ATTR_SERVICE_DATA = "service_data" +ATTR_DOMAIN: Final = "domain" +ATTR_SERVICE: Final = "service" +ATTR_SERVICE_DATA: Final = "service_data" # IDs -ATTR_ID = "id" +ATTR_ID: Final = "id" # Name ATTR_NAME: Final = "name" @@ -297,86 +299,86 @@ ATTR_NAME: Final = "name" ATTR_ENTITY_ID: Final = "entity_id" # Contains one string or a list of strings, each being an area id -ATTR_AREA_ID = "area_id" +ATTR_AREA_ID: Final = "area_id" # Contains one string, the device ID -ATTR_DEVICE_ID = "device_id" +ATTR_DEVICE_ID: Final = "device_id" # String with a friendly name for the entity -ATTR_FRIENDLY_NAME = "friendly_name" +ATTR_FRIENDLY_NAME: Final = "friendly_name" # A picture to represent entity -ATTR_ENTITY_PICTURE = "entity_picture" +ATTR_ENTITY_PICTURE: Final = "entity_picture" ATTR_IDENTIFIERS: Final = "identifiers" # Icon to use in the frontend -ATTR_ICON = "icon" +ATTR_ICON: Final = "icon" # The unit of measurement if applicable ATTR_UNIT_OF_MEASUREMENT: Final = "unit_of_measurement" -CONF_UNIT_SYSTEM_METRIC: str = "metric" -CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial" +CONF_UNIT_SYSTEM_METRIC: Final = "metric" +CONF_UNIT_SYSTEM_IMPERIAL: Final = "imperial" # Electrical attributes -ATTR_VOLTAGE = "voltage" +ATTR_VOLTAGE: Final = "voltage" # Location of the device/sensor -ATTR_LOCATION = "location" +ATTR_LOCATION: Final = "location" -ATTR_MODE = "mode" +ATTR_MODE: Final = "mode" ATTR_MANUFACTURER: Final = "manufacturer" ATTR_MODEL: Final = "model" ATTR_SW_VERSION: Final = "sw_version" -ATTR_BATTERY_CHARGING = "battery_charging" +ATTR_BATTERY_CHARGING: Final = "battery_charging" ATTR_BATTERY_LEVEL: Final = "battery_level" -ATTR_WAKEUP = "wake_up_interval" +ATTR_WAKEUP: Final = "wake_up_interval" # For devices which support a code attribute -ATTR_CODE = "code" -ATTR_CODE_FORMAT = "code_format" +ATTR_CODE: Final = "code" +ATTR_CODE_FORMAT: Final = "code_format" # For calling a device specific command -ATTR_COMMAND = "command" +ATTR_COMMAND: Final = "command" # For devices which support an armed state -ATTR_ARMED = "device_armed" +ATTR_ARMED: Final = "device_armed" # For devices which support a locked state -ATTR_LOCKED = "locked" +ATTR_LOCKED: Final = "locked" # For sensors that support 'tripping', eg. motion and door sensors -ATTR_TRIPPED = "device_tripped" +ATTR_TRIPPED: Final = "device_tripped" # For sensors that support 'tripping' this holds the most recent # time the device was tripped -ATTR_LAST_TRIP_TIME = "last_tripped_time" +ATTR_LAST_TRIP_TIME: Final = "last_tripped_time" # For all entity's, this hold whether or not it should be hidden -ATTR_HIDDEN = "hidden" +ATTR_HIDDEN: Final = "hidden" # Location of the entity -ATTR_LATITUDE = "latitude" -ATTR_LONGITUDE = "longitude" +ATTR_LATITUDE: Final = "latitude" +ATTR_LONGITUDE: Final = "longitude" # Accuracy of location in meters -ATTR_GPS_ACCURACY = "gps_accuracy" +ATTR_GPS_ACCURACY: Final = "gps_accuracy" # If state is assumed -ATTR_ASSUMED_STATE = "assumed_state" -ATTR_STATE = "state" +ATTR_ASSUMED_STATE: Final = "assumed_state" +ATTR_STATE: Final = "state" -ATTR_EDITABLE = "editable" -ATTR_OPTION = "option" +ATTR_EDITABLE: Final = "editable" +ATTR_OPTION: Final = "option" # The entity has been restored with restore state -ATTR_RESTORED = "restored" +ATTR_RESTORED: Final = "restored" # Bitfield of supported component features for the entity -ATTR_SUPPORTED_FEATURES = "supported_features" +ATTR_SUPPORTED_FEATURES: Final = "supported_features" # Class of device within its domain ATTR_DEVICE_CLASS: Final = "device_class" @@ -386,278 +388,278 @@ ATTR_TEMPERATURE: Final = "temperature" # #### UNITS OF MEASUREMENT #### # Power units -POWER_WATT = "W" -POWER_KILO_WATT = "kW" +POWER_WATT: Final = "W" +POWER_KILO_WATT: Final = "kW" # Voltage units -VOLT = "V" +VOLT: Final = "V" # Energy units -ENERGY_WATT_HOUR = "Wh" -ENERGY_KILO_WATT_HOUR = "kWh" +ENERGY_WATT_HOUR: Final = "Wh" +ENERGY_KILO_WATT_HOUR: Final = "kWh" # Electrical units -ELECTRICAL_CURRENT_AMPERE = "A" -ELECTRICAL_VOLT_AMPERE = "VA" +ELECTRICAL_CURRENT_AMPERE: Final = "A" +ELECTRICAL_VOLT_AMPERE: Final = "VA" # Degree units -DEGREE = "°" +DEGREE: Final = "°" # Currency units -CURRENCY_EURO = "€" -CURRENCY_DOLLAR = "$" -CURRENCY_CENT = "¢" +CURRENCY_EURO: Final = "€" +CURRENCY_DOLLAR: Final = "$" +CURRENCY_CENT: Final = "¢" # Temperature units -TEMP_CELSIUS = "°C" -TEMP_FAHRENHEIT = "°F" -TEMP_KELVIN = "K" +TEMP_CELSIUS: Final = "°C" +TEMP_FAHRENHEIT: Final = "°F" +TEMP_KELVIN: Final = "K" # Time units -TIME_MICROSECONDS = "μs" -TIME_MILLISECONDS = "ms" -TIME_SECONDS = "s" -TIME_MINUTES = "min" -TIME_HOURS = "h" -TIME_DAYS = "d" -TIME_WEEKS = "w" -TIME_MONTHS = "m" -TIME_YEARS = "y" +TIME_MICROSECONDS: Final = "μs" +TIME_MILLISECONDS: Final = "ms" +TIME_SECONDS: Final = "s" +TIME_MINUTES: Final = "min" +TIME_HOURS: Final = "h" +TIME_DAYS: Final = "d" +TIME_WEEKS: Final = "w" +TIME_MONTHS: Final = "m" +TIME_YEARS: Final = "y" # Length units -LENGTH_MILLIMETERS: str = "mm" -LENGTH_CENTIMETERS: str = "cm" -LENGTH_METERS: str = "m" -LENGTH_KILOMETERS: str = "km" +LENGTH_MILLIMETERS: Final = "mm" +LENGTH_CENTIMETERS: Final = "cm" +LENGTH_METERS: Final = "m" +LENGTH_KILOMETERS: Final = "km" -LENGTH_INCHES: str = "in" -LENGTH_FEET: str = "ft" -LENGTH_YARD: str = "yd" -LENGTH_MILES: str = "mi" +LENGTH_INCHES: Final = "in" +LENGTH_FEET: Final = "ft" +LENGTH_YARD: Final = "yd" +LENGTH_MILES: Final = "mi" # Frequency units -FREQUENCY_HERTZ = "Hz" -FREQUENCY_GIGAHERTZ = "GHz" +FREQUENCY_HERTZ: Final = "Hz" +FREQUENCY_GIGAHERTZ: Final = "GHz" # Pressure units -PRESSURE_PA: str = "Pa" -PRESSURE_HPA: str = "hPa" -PRESSURE_BAR: str = "bar" -PRESSURE_MBAR: str = "mbar" -PRESSURE_INHG: str = "inHg" -PRESSURE_PSI: str = "psi" +PRESSURE_PA: Final = "Pa" +PRESSURE_HPA: Final = "hPa" +PRESSURE_BAR: Final = "bar" +PRESSURE_MBAR: Final = "mbar" +PRESSURE_INHG: Final = "inHg" +PRESSURE_PSI: Final = "psi" # Volume units -VOLUME_LITERS: str = "L" -VOLUME_MILLILITERS: str = "mL" -VOLUME_CUBIC_METERS = "m³" -VOLUME_CUBIC_FEET = "ft³" +VOLUME_LITERS: Final = "L" +VOLUME_MILLILITERS: Final = "mL" +VOLUME_CUBIC_METERS: Final = "m³" +VOLUME_CUBIC_FEET: Final = "ft³" -VOLUME_GALLONS: str = "gal" -VOLUME_FLUID_OUNCE: str = "fl. oz." +VOLUME_GALLONS: Final = "gal" +VOLUME_FLUID_OUNCE: Final = "fl. oz." # Volume Flow Rate units -VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR = "m³/h" -VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE = "ft³/m" +VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final = "m³/h" +VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: Final = "ft³/m" # Area units -AREA_SQUARE_METERS = "m²" +AREA_SQUARE_METERS: Final = "m²" # Mass units -MASS_GRAMS: str = "g" -MASS_KILOGRAMS: str = "kg" -MASS_MILLIGRAMS = "mg" -MASS_MICROGRAMS = "µg" +MASS_GRAMS: Final = "g" +MASS_KILOGRAMS: Final = "kg" +MASS_MILLIGRAMS: Final = "mg" +MASS_MICROGRAMS: Final = "µg" -MASS_OUNCES: str = "oz" -MASS_POUNDS: str = "lb" +MASS_OUNCES: Final = "oz" +MASS_POUNDS: Final = "lb" # Conductivity units -CONDUCTIVITY: str = "µS/cm" +CONDUCTIVITY: Final = "µS/cm" # Light units -LIGHT_LUX: str = "lx" +LIGHT_LUX: Final = "lx" # UV Index units -UV_INDEX: str = "UV index" +UV_INDEX: Final = "UV index" # Percentage units -PERCENTAGE = "%" +PERCENTAGE: Final = "%" # Irradiation units -IRRADIATION_WATTS_PER_SQUARE_METER = "W/m²" +IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²" # Precipitation units -PRECIPITATION_MILLIMETERS_PER_HOUR = "mm/h" +PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" # Concentration units -CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = "µg/m³" -CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER = "mg/m³" -CONCENTRATION_PARTS_PER_CUBIC_METER = "p/m³" -CONCENTRATION_PARTS_PER_MILLION = "ppm" -CONCENTRATION_PARTS_PER_BILLION = "ppb" +CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" +CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³" +CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³" +CONCENTRATION_PARTS_PER_MILLION: Final = "ppm" +CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" # Speed units -SPEED_MILLIMETERS_PER_DAY = "mm/d" -SPEED_INCHES_PER_DAY = "in/d" -SPEED_METERS_PER_SECOND = "m/s" -SPEED_INCHES_PER_HOUR = "in/h" -SPEED_KILOMETERS_PER_HOUR = "km/h" -SPEED_MILES_PER_HOUR = "mph" +SPEED_MILLIMETERS_PER_DAY: Final = "mm/d" +SPEED_INCHES_PER_DAY: Final = "in/d" +SPEED_METERS_PER_SECOND: Final = "m/s" +SPEED_INCHES_PER_HOUR: Final = "in/h" +SPEED_KILOMETERS_PER_HOUR: Final = "km/h" +SPEED_MILES_PER_HOUR: Final = "mph" # Signal_strength units -SIGNAL_STRENGTH_DECIBELS = "dB" -SIGNAL_STRENGTH_DECIBELS_MILLIWATT = "dBm" +SIGNAL_STRENGTH_DECIBELS: Final = "dB" +SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm" # Data units -DATA_BITS = "bit" -DATA_KILOBITS = "kbit" -DATA_MEGABITS = "Mbit" -DATA_GIGABITS = "Gbit" -DATA_BYTES = "B" -DATA_KILOBYTES = "kB" -DATA_MEGABYTES = "MB" -DATA_GIGABYTES = "GB" -DATA_TERABYTES = "TB" -DATA_PETABYTES = "PB" -DATA_EXABYTES = "EB" -DATA_ZETTABYTES = "ZB" -DATA_YOTTABYTES = "YB" -DATA_KIBIBYTES = "KiB" -DATA_MEBIBYTES = "MiB" -DATA_GIBIBYTES = "GiB" -DATA_TEBIBYTES = "TiB" -DATA_PEBIBYTES = "PiB" -DATA_EXBIBYTES = "EiB" -DATA_ZEBIBYTES = "ZiB" -DATA_YOBIBYTES = "YiB" -DATA_RATE_BITS_PER_SECOND = "bit/s" -DATA_RATE_KILOBITS_PER_SECOND = "kbit/s" -DATA_RATE_MEGABITS_PER_SECOND = "Mbit/s" -DATA_RATE_GIGABITS_PER_SECOND = "Gbit/s" -DATA_RATE_BYTES_PER_SECOND = "B/s" -DATA_RATE_KILOBYTES_PER_SECOND = "kB/s" -DATA_RATE_MEGABYTES_PER_SECOND = "MB/s" -DATA_RATE_GIGABYTES_PER_SECOND = "GB/s" -DATA_RATE_KIBIBYTES_PER_SECOND = "KiB/s" -DATA_RATE_MEBIBYTES_PER_SECOND = "MiB/s" -DATA_RATE_GIBIBYTES_PER_SECOND = "GiB/s" +DATA_BITS: Final = "bit" +DATA_KILOBITS: Final = "kbit" +DATA_MEGABITS: Final = "Mbit" +DATA_GIGABITS: Final = "Gbit" +DATA_BYTES: Final = "B" +DATA_KILOBYTES: Final = "kB" +DATA_MEGABYTES: Final = "MB" +DATA_GIGABYTES: Final = "GB" +DATA_TERABYTES: Final = "TB" +DATA_PETABYTES: Final = "PB" +DATA_EXABYTES: Final = "EB" +DATA_ZETTABYTES: Final = "ZB" +DATA_YOTTABYTES: Final = "YB" +DATA_KIBIBYTES: Final = "KiB" +DATA_MEBIBYTES: Final = "MiB" +DATA_GIBIBYTES: Final = "GiB" +DATA_TEBIBYTES: Final = "TiB" +DATA_PEBIBYTES: Final = "PiB" +DATA_EXBIBYTES: Final = "EiB" +DATA_ZEBIBYTES: Final = "ZiB" +DATA_YOBIBYTES: Final = "YiB" +DATA_RATE_BITS_PER_SECOND: Final = "bit/s" +DATA_RATE_KILOBITS_PER_SECOND: Final = "kbit/s" +DATA_RATE_MEGABITS_PER_SECOND: Final = "Mbit/s" +DATA_RATE_GIGABITS_PER_SECOND: Final = "Gbit/s" +DATA_RATE_BYTES_PER_SECOND: Final = "B/s" +DATA_RATE_KILOBYTES_PER_SECOND: Final = "kB/s" +DATA_RATE_MEGABYTES_PER_SECOND: Final = "MB/s" +DATA_RATE_GIGABYTES_PER_SECOND: Final = "GB/s" +DATA_RATE_KIBIBYTES_PER_SECOND: Final = "KiB/s" +DATA_RATE_MEBIBYTES_PER_SECOND: Final = "MiB/s" +DATA_RATE_GIBIBYTES_PER_SECOND: Final = "GiB/s" # #### SERVICES #### -SERVICE_HOMEASSISTANT_STOP = "stop" -SERVICE_HOMEASSISTANT_RESTART = "restart" +SERVICE_HOMEASSISTANT_STOP: Final = "stop" +SERVICE_HOMEASSISTANT_RESTART: Final = "restart" -SERVICE_TURN_ON = "turn_on" -SERVICE_TURN_OFF = "turn_off" -SERVICE_TOGGLE = "toggle" -SERVICE_RELOAD = "reload" +SERVICE_TURN_ON: Final = "turn_on" +SERVICE_TURN_OFF: Final = "turn_off" +SERVICE_TOGGLE: Final = "toggle" +SERVICE_RELOAD: Final = "reload" -SERVICE_VOLUME_UP = "volume_up" -SERVICE_VOLUME_DOWN = "volume_down" -SERVICE_VOLUME_MUTE = "volume_mute" -SERVICE_VOLUME_SET = "volume_set" -SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause" -SERVICE_MEDIA_PLAY = "media_play" -SERVICE_MEDIA_PAUSE = "media_pause" -SERVICE_MEDIA_STOP = "media_stop" -SERVICE_MEDIA_NEXT_TRACK = "media_next_track" -SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track" -SERVICE_MEDIA_SEEK = "media_seek" -SERVICE_REPEAT_SET = "repeat_set" -SERVICE_SHUFFLE_SET = "shuffle_set" +SERVICE_VOLUME_UP: Final = "volume_up" +SERVICE_VOLUME_DOWN: Final = "volume_down" +SERVICE_VOLUME_MUTE: Final = "volume_mute" +SERVICE_VOLUME_SET: Final = "volume_set" +SERVICE_MEDIA_PLAY_PAUSE: Final = "media_play_pause" +SERVICE_MEDIA_PLAY: Final = "media_play" +SERVICE_MEDIA_PAUSE: Final = "media_pause" +SERVICE_MEDIA_STOP: Final = "media_stop" +SERVICE_MEDIA_NEXT_TRACK: Final = "media_next_track" +SERVICE_MEDIA_PREVIOUS_TRACK: Final = "media_previous_track" +SERVICE_MEDIA_SEEK: Final = "media_seek" +SERVICE_REPEAT_SET: Final = "repeat_set" +SERVICE_SHUFFLE_SET: Final = "shuffle_set" -SERVICE_ALARM_DISARM = "alarm_disarm" -SERVICE_ALARM_ARM_HOME = "alarm_arm_home" -SERVICE_ALARM_ARM_AWAY = "alarm_arm_away" -SERVICE_ALARM_ARM_NIGHT = "alarm_arm_night" -SERVICE_ALARM_ARM_CUSTOM_BYPASS = "alarm_arm_custom_bypass" -SERVICE_ALARM_TRIGGER = "alarm_trigger" +SERVICE_ALARM_DISARM: Final = "alarm_disarm" +SERVICE_ALARM_ARM_HOME: Final = "alarm_arm_home" +SERVICE_ALARM_ARM_AWAY: Final = "alarm_arm_away" +SERVICE_ALARM_ARM_NIGHT: Final = "alarm_arm_night" +SERVICE_ALARM_ARM_CUSTOM_BYPASS: Final = "alarm_arm_custom_bypass" +SERVICE_ALARM_TRIGGER: Final = "alarm_trigger" -SERVICE_LOCK = "lock" -SERVICE_UNLOCK = "unlock" +SERVICE_LOCK: Final = "lock" +SERVICE_UNLOCK: Final = "unlock" -SERVICE_OPEN = "open" -SERVICE_CLOSE = "close" +SERVICE_OPEN: Final = "open" +SERVICE_CLOSE: Final = "close" -SERVICE_CLOSE_COVER = "close_cover" -SERVICE_CLOSE_COVER_TILT = "close_cover_tilt" -SERVICE_OPEN_COVER = "open_cover" -SERVICE_OPEN_COVER_TILT = "open_cover_tilt" -SERVICE_SET_COVER_POSITION = "set_cover_position" -SERVICE_SET_COVER_TILT_POSITION = "set_cover_tilt_position" -SERVICE_STOP_COVER = "stop_cover" -SERVICE_STOP_COVER_TILT = "stop_cover_tilt" -SERVICE_TOGGLE_COVER_TILT = "toggle_cover_tilt" +SERVICE_CLOSE_COVER: Final = "close_cover" +SERVICE_CLOSE_COVER_TILT: Final = "close_cover_tilt" +SERVICE_OPEN_COVER: Final = "open_cover" +SERVICE_OPEN_COVER_TILT: Final = "open_cover_tilt" +SERVICE_SET_COVER_POSITION: Final = "set_cover_position" +SERVICE_SET_COVER_TILT_POSITION: Final = "set_cover_tilt_position" +SERVICE_STOP_COVER: Final = "stop_cover" +SERVICE_STOP_COVER_TILT: Final = "stop_cover_tilt" +SERVICE_TOGGLE_COVER_TILT: Final = "toggle_cover_tilt" -SERVICE_SELECT_OPTION = "select_option" +SERVICE_SELECT_OPTION: Final = "select_option" # #### API / REMOTE #### SERVER_PORT: Final = 8123 -URL_ROOT = "/" -URL_API = "/api/" -URL_API_STREAM = "/api/stream" -URL_API_CONFIG = "/api/config" -URL_API_DISCOVERY_INFO = "/api/discovery_info" -URL_API_STATES = "/api/states" -URL_API_STATES_ENTITY = "/api/states/{}" -URL_API_EVENTS = "/api/events" -URL_API_EVENTS_EVENT = "/api/events/{}" -URL_API_SERVICES = "/api/services" -URL_API_SERVICES_SERVICE = "/api/services/{}/{}" -URL_API_COMPONENTS = "/api/components" -URL_API_ERROR_LOG = "/api/error_log" -URL_API_LOG_OUT = "/api/log_out" -URL_API_TEMPLATE = "/api/template" +URL_ROOT: Final = "/" +URL_API: Final = "/api/" +URL_API_STREAM: Final = "/api/stream" +URL_API_CONFIG: Final = "/api/config" +URL_API_DISCOVERY_INFO: Final = "/api/discovery_info" +URL_API_STATES: Final = "/api/states" +URL_API_STATES_ENTITY: Final = "/api/states/{}" +URL_API_EVENTS: Final = "/api/events" +URL_API_EVENTS_EVENT: Final = "/api/events/{}" +URL_API_SERVICES: Final = "/api/services" +URL_API_SERVICES_SERVICE: Final = "/api/services/{}/{}" +URL_API_COMPONENTS: Final = "/api/components" +URL_API_ERROR_LOG: Final = "/api/error_log" +URL_API_LOG_OUT: Final = "/api/log_out" +URL_API_TEMPLATE: Final = "/api/template" -HTTP_OK = 200 -HTTP_CREATED = 201 -HTTP_ACCEPTED = 202 -HTTP_MOVED_PERMANENTLY = 301 -HTTP_BAD_REQUEST = 400 -HTTP_UNAUTHORIZED = 401 -HTTP_FORBIDDEN = 403 -HTTP_NOT_FOUND = 404 -HTTP_METHOD_NOT_ALLOWED = 405 -HTTP_UNPROCESSABLE_ENTITY = 422 -HTTP_TOO_MANY_REQUESTS = 429 -HTTP_INTERNAL_SERVER_ERROR = 500 -HTTP_BAD_GATEWAY = 502 -HTTP_SERVICE_UNAVAILABLE = 503 +HTTP_OK: Final = 200 +HTTP_CREATED: Final = 201 +HTTP_ACCEPTED: Final = 202 +HTTP_MOVED_PERMANENTLY: Final = 301 +HTTP_BAD_REQUEST: Final = 400 +HTTP_UNAUTHORIZED: Final = 401 +HTTP_FORBIDDEN: Final = 403 +HTTP_NOT_FOUND: Final = 404 +HTTP_METHOD_NOT_ALLOWED: Final = 405 +HTTP_UNPROCESSABLE_ENTITY: Final = 422 +HTTP_TOO_MANY_REQUESTS: Final = 429 +HTTP_INTERNAL_SERVER_ERROR: Final = 500 +HTTP_BAD_GATEWAY: Final = 502 +HTTP_SERVICE_UNAVAILABLE: Final = 503 -HTTP_BASIC_AUTHENTICATION = "basic" -HTTP_DIGEST_AUTHENTICATION = "digest" +HTTP_BASIC_AUTHENTICATION: Final = "basic" +HTTP_DIGEST_AUTHENTICATION: Final = "digest" -HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With" +HTTP_HEADER_X_REQUESTED_WITH: Final = "X-Requested-With" -CONTENT_TYPE_JSON = "application/json" -CONTENT_TYPE_MULTIPART = "multipart/x-mixed-replace; boundary={}" -CONTENT_TYPE_TEXT_PLAIN = "text/plain" +CONTENT_TYPE_JSON: Final = "application/json" +CONTENT_TYPE_MULTIPART: Final = "multipart/x-mixed-replace; boundary={}" +CONTENT_TYPE_TEXT_PLAIN: Final = "text/plain" # The exit code to send to request a restart -RESTART_EXIT_CODE = 100 +RESTART_EXIT_CODE: Final = 100 -UNIT_NOT_RECOGNIZED_TEMPLATE: str = "{} is not a recognized {} unit." +UNIT_NOT_RECOGNIZED_TEMPLATE: Final = "{} is not a recognized {} unit." -LENGTH: str = "length" -MASS: str = "mass" -PRESSURE: str = "pressure" -VOLUME: str = "volume" -TEMPERATURE: str = "temperature" -SPEED_MS: str = "speed_ms" -ILLUMINANCE: str = "illuminance" +LENGTH: Final = "length" +MASS: Final = "mass" +PRESSURE: Final = "pressure" +VOLUME: Final = "volume" +TEMPERATURE: Final = "temperature" +SPEED_MS: Final = "speed_ms" +ILLUMINANCE: Final = "illuminance" -WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] +WEEKDAYS: Final[list[str]] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] # The degree of precision for platforms -PRECISION_WHOLE = 1 -PRECISION_HALVES = 0.5 -PRECISION_TENTHS = 0.1 +PRECISION_WHOLE: Final = 1 +PRECISION_HALVES: Final = 0.5 +PRECISION_TENTHS: Final = 0.1 # Static list of entities that will never be exposed to # cloud, alexa, or google_home components -CLOUD_NEVER_EXPOSED_ENTITIES = ["group.all_locks"] +CLOUD_NEVER_EXPOSED_ENTITIES: Final[list[str]] = ["group.all_locks"] # The ID of the Home Assistant Cast App -CAST_APP_ID_HOMEASSISTANT = "B12CE3CA" +CAST_APP_ID_HOMEASSISTANT: Final = "B12CE3CA" From 5ee362bc3469b88094ff25089b105b0b8e508f1e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 19 May 2021 21:28:00 +0200 Subject: [PATCH 579/852] Store sensor last_reset attribute as a string, not a datetime (#50851) * Store last_reset attribute as a string, not a datetime * Update tests --- homeassistant/components/sensor/__init__.py | 2 +- tests/components/utility_meter/test_sensor.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index f3b9a24a15d..75fea9e044b 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -119,6 +119,6 @@ class SensorEntity(Entity): def state_attributes(self) -> dict[str, Any] | None: """Return state attributes.""" if last_reset := self.last_reset: - return {ATTR_LAST_RESET: last_reset} + return {ATTR_LAST_RESET: last_reset.isoformat()} return None diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 9157ba738c7..54854ac9668 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -200,12 +200,12 @@ async def test_restore_state(hass): state = hass.states.get("sensor.energy_bill_onpeak") assert state.state == "3" assert state.attributes.get("status") == PAUSED - assert state.attributes.get("last_reset") == dt_util.parse_datetime(last_reset) + assert state.attributes.get("last_reset") == last_reset state = hass.states.get("sensor.energy_bill_offpeak") assert state.state == "6" assert state.attributes.get("status") == COLLECTING - assert state.attributes.get("last_reset") == dt_util.parse_datetime(last_reset) + assert state.attributes.get("last_reset") == last_reset # utility_meter is loaded, now set sensors according to utility_meter: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -348,12 +348,13 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): state = hass.states.get("sensor.energy_bill") if expect_reset: assert state.attributes.get("last_period") == "2" - assert state.attributes.get("last_reset") == now + assert state.attributes.get("last_reset") == now.isoformat() assert state.state == "3" else: assert state.attributes.get("last_period") == 0 assert state.state == "5" - assert state.attributes.get("last_reset") == dt_util.parse_datetime(start_time) + start_time_str = dt_util.parse_datetime(start_time).isoformat() + assert state.attributes.get("last_reset") == start_time_str async def test_self_reset_quarter_hourly(hass, legacy_patchable_time): From 9f754f1643b8724d7a75c66437fbbe2ba42f48c6 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 19 May 2021 16:30:31 -0400 Subject: [PATCH 580/852] bump envoy_reader to 0.19.0 (#50827) --- homeassistant/components/enphase_envoy/manifest.json | 10 +++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 3e31ac5dc63..a682f53bc44 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,8 +2,12 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": ["envoy_reader==0.18.4"], - "codeowners": ["@gtdiehl"], + "requirements": [ + "envoy_reader==0.19.0" + ], + "codeowners": [ + "@gtdiehl" + ], "config_flow": true, "zeroconf": [ { @@ -11,4 +15,4 @@ } ], "iot_class": "local_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index b802a152efb..ae8445262c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -563,7 +563,7 @@ env_canada==0.2.5 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.18.4 +envoy_reader==0.19.0 # homeassistant.components.season ephem==3.7.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8bf2a07c97a..67ee6f0122b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,7 +309,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.enphase_envoy -envoy_reader==0.18.4 +envoy_reader==0.19.0 # homeassistant.components.season ephem==3.7.7.0 From ab9aa4466e9390b356387a441483755aa4adf5e8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 May 2021 23:15:00 +0200 Subject: [PATCH 581/852] Fix SolarEdge active check on entry setup (#50873) --- homeassistant/components/solaredge/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index df38756cd69..cb56817fe87 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -25,17 +25,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: response = await hass.async_add_executor_job( api.get_details, entry.data[CONF_SITE_ID] ) - except KeyError as ex: - LOGGER.error("Missing details data in SolarEdge response") - raise ConfigEntryNotReady from ex except (ConnectTimeout, HTTPError) as ex: LOGGER.error("Could not retrieve details from SolarEdge API") raise ConfigEntryNotReady from ex - if response["details"]["status"].lower() != "active": + if "details" not in response: + LOGGER.error("Missing details data in SolarEdge response") + raise ConfigEntryNotReady + + if response["details"].get("status", "").lower() != "active": LOGGER.error("SolarEdge site is not active") return False - LOGGER.debug("Credentials correct and site is active") hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_API_CLIENT: api} hass.config_entries.async_setup_platforms(entry, PLATFORMS) From f44efb1eeaef9820ac1c516fd0a1b0de9c0e67d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 May 2021 00:12:27 +0200 Subject: [PATCH 582/852] Upgrade watchdog to 2.1.2 (#50863) --- homeassistant/components/folder_watcher/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 8907d37b472..9f89045b28e 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==2.1.1"], + "requirements": ["watchdog==2.1.2"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index ae8445262c6..dfcb906b0c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2332,7 +2332,7 @@ wakeonlan==2.0.1 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.1.1 +watchdog==2.1.2 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67ee6f0122b..6025a78b6ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1253,7 +1253,7 @@ vultr==0.1.2 wakeonlan==2.0.1 # homeassistant.components.folder_watcher -watchdog==2.1.1 +watchdog==2.1.2 # homeassistant.components.wiffi wiffi==1.0.1 From cdf18bd4b1f98367b5225dbde195aa19d06bd2a7 Mon Sep 17 00:00:00 2001 From: shbatm Date: Wed, 19 May 2021 19:08:35 -0500 Subject: [PATCH 583/852] Add Z-Wave Parameter and Node Rename Services to ISY994 (#50844) --- homeassistant/components/isy994/entity.py | 21 ++++++ homeassistant/components/isy994/services.py | 52 ++++++++++++++ homeassistant/components/isy994/services.yaml | 70 +++++++++++++++++++ 3 files changed, 143 insertions(+) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 6dab5b2ed65..69714dd9f4b 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -179,6 +179,27 @@ class ISYNodeEntity(ISYEntity): ) await self._node.send_cmd(command, value, unit_of_measurement, parameters) + async def async_get_zwave_parameter(self, parameter): + """Repsond to an entity service command to request a Z-Wave device parameter from the ISY.""" + if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: + raise HomeAssistantError( + f"Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave device {self.entity_id}" + ) + await self._node.get_zwave_parameter(parameter) + + async def async_set_zwave_parameter(self, parameter, value, size): + """Repsond to an entity service command to set a Z-Wave device parameter via the ISY.""" + if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: + raise HomeAssistantError( + f"Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave device {self.entity_id}" + ) + await self._node.set_zwave_parameter(parameter, value, size) + await self._node.get_zwave_parameter(parameter) + + async def async_rename_node(self, name): + """Repsond to an entity service command to rename a node on the ISY.""" + await self._node.rename(name) + class ISYProgramEntity(ISYEntity): """Representation of an ISY994 program base.""" diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 03ecc3930bb..c34e8a1c67b 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -48,15 +48,20 @@ INTEGRATION_SERVICES = [ # Entity specific methods (valid for most Groups/ISY Scenes, Lights, Switches, Fans) SERVICE_SEND_RAW_NODE_COMMAND = "send_raw_node_command" SERVICE_SEND_NODE_COMMAND = "send_node_command" +SERVICE_GET_ZWAVE_PARAMETER = "get_zwave_parameter" +SERVICE_SET_ZWAVE_PARAMETER = "set_zwave_parameter" +SERVICE_RENAME_NODE = "rename_node" # Services valid only for dimmable lights. SERVICE_SET_ON_LEVEL = "set_on_level" SERVICE_SET_RAMP_RATE = "set_ramp_rate" +CONF_PARAMETER = "parameter" CONF_PARAMETERS = "parameters" CONF_VALUE = "value" CONF_INIT = "init" CONF_ISY = "isy" +CONF_SIZE = "size" VALID_NODE_COMMANDS = [ "beep", @@ -81,6 +86,7 @@ VALID_PROGRAM_COMMANDS = [ "enable_run_at_startup", "disable_run_at_startup", ] +VALID_PARAMETER_SIZES = [1, 2, 4] def valid_isy_commands(value: Any) -> str: @@ -116,6 +122,16 @@ SERVICE_SEND_NODE_COMMAND_SCHEMA = { vol.Required(CONF_COMMAND): vol.In(VALID_NODE_COMMANDS) } +SERVICE_RENAME_NODE_SCHEMA = {vol.Required(CONF_NAME): cv.string} + +SERVICE_GET_ZWAVE_PARAMETER_SCHEMA = {vol.Required(CONF_PARAMETER): vol.Coerce(int)} + +SERVICE_SET_ZWAVE_PARAMETER_SCHEMA = { + vol.Required(CONF_PARAMETER): vol.Coerce(int), + vol.Required(CONF_VALUE): vol.Coerce(int), + vol.Required(CONF_SIZE): vol.All(vol.Coerce(int), vol.In(VALID_PARAMETER_SIZES)), +} + SERVICE_SET_VARIABLE_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_ADDRESS, CONF_TYPE, CONF_NAME), vol.Schema( @@ -377,6 +393,42 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 service_func=_async_send_node_command, ) + async def _async_get_zwave_parameter(call: ServiceCall): + await hass.helpers.service.entity_service_call( + async_get_platforms(hass, DOMAIN), "async_get_zwave_parameter", call + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_GET_ZWAVE_PARAMETER, + schema=cv.make_entity_service_schema(SERVICE_GET_ZWAVE_PARAMETER_SCHEMA), + service_func=_async_get_zwave_parameter, + ) + + async def _async_set_zwave_parameter(call: ServiceCall): + await hass.helpers.service.entity_service_call( + async_get_platforms(hass, DOMAIN), "async_set_zwave_parameter", call + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_ZWAVE_PARAMETER, + schema=cv.make_entity_service_schema(SERVICE_SET_ZWAVE_PARAMETER_SCHEMA), + service_func=_async_set_zwave_parameter, + ) + + async def _async_rename_node(call: ServiceCall): + await hass.helpers.service.entity_service_call( + async_get_platforms(hass, DOMAIN), "async_rename_node", call + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_RENAME_NODE, + schema=cv.make_entity_service_schema(SERVICE_RENAME_NODE_SCHEMA), + service_func=_async_rename_node, + ) + @callback def async_unload_services(hass: HomeAssistant): diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index c163d78a173..73e0a675e49 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -68,6 +68,76 @@ send_node_command: - "fast_off" - "fast_on" - "query" +get_zwave_parameter: + name: Get Z-Wave Parameter + description: >- + Request a Z-Wave Device parameter via the ISY. The parameter value will be returned as a entity extra state attribute with the name "ZW_#" + where "#" is the parameter number. + target: + entity: + integration: isy994 + fields: + parameter: + name: Parameter + description: The parameter number to retrieve from the device. + example: 8 + selector: + number: + min: 1 + max: 255 +set_zwave_parameter: + name: Set Z-Wave Parameter + description: >- + Update a Z-Wave Device parameter via the ISY. The parameter value will also be returned as a entity extra state attribute with the name "ZW_#" + where "#" is the parameter number. + target: + entity: + integration: isy994 + fields: + parameter: + name: Parameter + description: The parameter number to set on the end device. + required: true + example: 8 + selector: + number: + min: 1 + max: 255 + value: + name: Value + description: The value to set for the parameter. May be an integer or byte string (e.g. "0xFFFF"). + required: true + example: 33491663 + selector: + text: + size: + name: Size + description: The size of the parameter, either 1, 2, or 4 bytes. + required: true + example: 4 + selector: + select: + options: + - "1" + - "2" + - "4" +rename_node: + name: Rename Node on ISY994 + description: >- + Rename a node or group (scene) on the ISY994. Note: this will not automatically change the Home Assistant Entity Name or Entity ID to match. + The entity name and ID will only be updated after calling `isy994.reload` or restarting Home Assistant, and ONLY IF you have not already customized the + name within Home Assistant. + target: + entity: + integration: isy994 + fields: + name: + name: New Name + description: The new name to use within the ISY994. + required: true + example: "Front Door Light" + selector: + text: set_on_level: name: Set On Level description: Send a ISY set_on_level command to a Node. From a49d5c426637c14138aa77cd16026a32b89145e7 Mon Sep 17 00:00:00 2001 From: shbatm Date: Wed, 19 May 2021 19:10:09 -0500 Subject: [PATCH 584/852] Add ISY994 System Health Info (#50840) Co-authored-by: J. Nick Koston --- homeassistant/components/isy994/strings.json | 8 ++ .../components/isy994/system_health.py | 36 ++++++++ .../components/isy994/translations/en.json | 10 ++- tests/components/isy994/test_system_health.py | 86 +++++++++++++++++++ 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/isy994/system_health.py create mode 100644 tests/components/isy994/test_system_health.py diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index 08092c2482c..eaba5f7a9da 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -36,5 +36,13 @@ } } } + }, + "system_health": { + "info": { + "host_reachable": "Host Reachable", + "device_connected": "ISY Connected", + "last_heartbeat": "Last Heartbeat Time", + "websocket_status": "Event Socket Status" + } } } diff --git a/homeassistant/components/isy994/system_health.py b/homeassistant/components/isy994/system_health.py new file mode 100644 index 00000000000..f550b8ed07b --- /dev/null +++ b/homeassistant/components/isy994/system_health.py @@ -0,0 +1,36 @@ +"""Provide info to system health.""" +from pyisy import ISY + +from homeassistant.components import system_health +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN, ISY994_ISY, ISY_URL_POSTFIX + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass): + """Get info for the info page.""" + + health_info = {} + config_entry_id = next( + iter(hass.data[DOMAIN]) + ) # Only first ISY is supported for now + isy: ISY = hass.data[DOMAIN][config_entry_id][ISY994_ISY] + + entry = hass.config_entries.async_get_entry(config_entry_id) + health_info["host_reachable"] = await system_health.async_check_can_reach_url( + hass, f"{entry.data[CONF_HOST]}{ISY_URL_POSTFIX}" + ) + health_info["device_connected"] = isy.connected + health_info["last_heartbeat"] = isy.websocket.last_heartbeat + health_info["websocket_status"] = isy.websocket.status + + return health_info diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json index 724160c6d3d..e413affaa50 100644 --- a/homeassistant/components/isy994/translations/en.json +++ b/homeassistant/components/isy994/translations/en.json @@ -36,5 +36,13 @@ "title": "ISY994 Options" } } - } + }, + "system_health": { + "info": { + "host_reachable": "Host Reachable", + "device_connected": "Connected to Device", + "last_heartbeat": "Last Heartbeat Time", + "websocket_status": "Event Socket Status" + } + } } \ No newline at end of file diff --git a/tests/components/isy994/test_system_health.py b/tests/components/isy994/test_system_health.py new file mode 100644 index 00000000000..63810b10464 --- /dev/null +++ b/tests/components/isy994/test_system_health.py @@ -0,0 +1,86 @@ +"""Test ISY994 system health.""" +import asyncio +from unittest.mock import Mock + +from aiohttp import ClientError + +from homeassistant.components.isy994.const import DOMAIN, ISY994_ISY, ISY_URL_POSTFIX +from homeassistant.const import CONF_HOST +from homeassistant.setup import async_setup_component + +from .test_config_flow import MOCK_HOSTNAME, MOCK_UUID + +from tests.common import MockConfigEntry, get_system_health_info + +MOCK_ENTRY_ID = "cad4af20b811990e757588519917d6af" +MOCK_CONNECTED = "connected" +MOCK_HEARTBEAT = "2021-05-01T00:00:00.000000" + + +async def test_system_health(hass, aioclient_mock): + """Test system health.""" + aioclient_mock.get(f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", text="") + + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + MockConfigEntry( + domain=DOMAIN, + entry_id=MOCK_ENTRY_ID, + data={CONF_HOST: f"http://{MOCK_HOSTNAME}"}, + unique_id=MOCK_UUID, + ).add_to_hass(hass) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][MOCK_ENTRY_ID] = {} + hass.data[DOMAIN][MOCK_ENTRY_ID][ISY994_ISY] = Mock( + connected=True, + websocket=Mock( + last_heartbeat=MOCK_HEARTBEAT, + status=MOCK_CONNECTED, + ), + ) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info["host_reachable"] == "ok" + assert info["device_connected"] + assert info["last_heartbeat"] == MOCK_HEARTBEAT + assert info["websocket_status"] == MOCK_CONNECTED + + +async def test_system_health_failed_connect(hass, aioclient_mock): + """Test system health.""" + aioclient_mock.get(f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", exc=ClientError) + + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + MockConfigEntry( + domain=DOMAIN, + entry_id=MOCK_ENTRY_ID, + data={CONF_HOST: f"http://{MOCK_HOSTNAME}"}, + unique_id=MOCK_UUID, + ).add_to_hass(hass) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][MOCK_ENTRY_ID] = {} + hass.data[DOMAIN][MOCK_ENTRY_ID][ISY994_ISY] = Mock( + connected=True, + websocket=Mock( + last_heartbeat=MOCK_HEARTBEAT, + status=MOCK_CONNECTED, + ), + ) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info["host_reachable"] == {"error": "unreachable", "type": "failed"} From a021fe301cfddd6ac8677df28335def53c4014b3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 20 May 2021 00:11:53 +0000 Subject: [PATCH 585/852] [ci skip] Translation update --- homeassistant/components/isy994/translations/en.json | 4 ++-- .../components/upnp/translations/zh-Hant.json | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json index e413affaa50..dbeaa75acf4 100644 --- a/homeassistant/components/isy994/translations/en.json +++ b/homeassistant/components/isy994/translations/en.json @@ -39,10 +39,10 @@ }, "system_health": { "info": { + "device_connected": "ISY Connected", "host_reachable": "Host Reachable", - "device_connected": "Connected to Device", "last_heartbeat": "Last Heartbeat Time", "websocket_status": "Event Socket Status" } - } + } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/zh-Hant.json b/homeassistant/components/upnp/translations/zh-Hant.json index aad03a803b0..80c92662d22 100644 --- a/homeassistant/components/upnp/translations/zh-Hant.json +++ b/homeassistant/components/upnp/translations/zh-Hant.json @@ -13,9 +13,19 @@ "user": { "data": { "scan_interval": "\u66f4\u65b0\u9593\u9694\uff08\u79d2\u3001\u6700\u5c11 30 \u79d2\uff09", + "unique_id": "\u88dd\u7f6e", "usn": "\u88dd\u7f6e" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u9593\u9694\uff08\u79d2\u3001\u6700\u5c11 30 \u79d2\uff09" + } + } + } } } \ No newline at end of file From 7350942e9e2d68803c5e51c7d6796c4234363b7d Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 20 May 2021 03:10:13 -0300 Subject: [PATCH 586/852] Implement heartbeat in the Broadlink integration (#43878) * Implement heartbeat in the Broadlink integration * Rename INTERVAL to HEARTBEAT_INTERVAL * Test that we log an error message when the heartbeat fails --- .../components/broadlink/__init__.py | 22 +++- .../components/broadlink/heartbeat.py | 55 +++++++++ tests/components/broadlink/test_heartbeat.py | 108 ++++++++++++++++++ 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/broadlink/heartbeat.py create mode 100644 tests/components/broadlink/test_heartbeat.py diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 501afaac930..eb39a057434 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -1,8 +1,11 @@ """The Broadlink integration.""" +from __future__ import annotations + from dataclasses import dataclass, field from .const import DOMAIN from .device import BroadlinkDevice +from .heartbeat import BroadlinkHeartbeat @dataclass @@ -11,6 +14,7 @@ class BroadlinkData: devices: dict = field(default_factory=dict) platforms: dict = field(default_factory=dict) + heartbeat: BroadlinkHeartbeat | None = None async def async_setup(hass, config): @@ -21,11 +25,25 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up a Broadlink device from a config entry.""" + data = hass.data[DOMAIN] + + if data.heartbeat is None: + data.heartbeat = BroadlinkHeartbeat(hass) + hass.async_create_task(data.heartbeat.async_setup()) + device = BroadlinkDevice(hass, entry) return await device.async_setup() async def async_unload_entry(hass, entry): """Unload a config entry.""" - device = hass.data[DOMAIN].devices.pop(entry.entry_id) - return await device.async_unload() + data = hass.data[DOMAIN] + + device = data.devices.pop(entry.entry_id) + result = await device.async_unload() + + if not data.devices: + await data.heartbeat.async_unload() + data.heartbeat = None + + return result diff --git a/homeassistant/components/broadlink/heartbeat.py b/homeassistant/components/broadlink/heartbeat.py new file mode 100644 index 00000000000..282df3ae6a8 --- /dev/null +++ b/homeassistant/components/broadlink/heartbeat.py @@ -0,0 +1,55 @@ +"""Heartbeats for Broadlink devices.""" +import datetime as dt +import logging + +import broadlink as blk + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import event + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class BroadlinkHeartbeat: + """Manages heartbeats in the Broadlink integration. + + Some devices reboot when they cannot reach the cloud. This mechanism + feeds their watchdog timers so they can be used offline. + """ + + HEARTBEAT_INTERVAL = dt.timedelta(minutes=2) + + def __init__(self, hass): + """Initialize the heartbeat.""" + self._hass = hass + self._unsubscribe = None + + async def async_setup(self): + """Set up the heartbeat.""" + if self._unsubscribe is None: + await self.async_heartbeat(dt.datetime.now()) + self._unsubscribe = event.async_track_time_interval( + self._hass, self.async_heartbeat, self.HEARTBEAT_INTERVAL + ) + + async def async_unload(self): + """Unload the heartbeat.""" + if self._unsubscribe is not None: + self._unsubscribe() + self._unsubscribe = None + + async def async_heartbeat(self, now): + """Send packets to feed watchdog timers.""" + hass = self._hass + config_entries = hass.config_entries.async_entries(DOMAIN) + + for entry in config_entries: + host = entry.data[CONF_HOST] + try: + await hass.async_add_executor_job(blk.ping, host) + except OSError as err: + _LOGGER.debug("Failed to send heartbeat to %s: %s", host, err) + else: + _LOGGER.debug("Heartbeat sent to %s", host) diff --git a/tests/components/broadlink/test_heartbeat.py b/tests/components/broadlink/test_heartbeat.py new file mode 100644 index 00000000000..8e52a562425 --- /dev/null +++ b/tests/components/broadlink/test_heartbeat.py @@ -0,0 +1,108 @@ +"""Tests for Broadlink heartbeats.""" +from unittest.mock import call, patch + +from homeassistant.components.broadlink.heartbeat import BroadlinkHeartbeat +from homeassistant.util import dt + +from . import get_device + +from tests.common import async_fire_time_changed + +DEVICE_PING = "homeassistant.components.broadlink.heartbeat.blk.ping" + + +async def test_heartbeat_trigger_startup(hass): + """Test that the heartbeat is initialized with the first config entry.""" + device = get_device("Office") + + with patch(DEVICE_PING) as mock_ping: + await device.setup_entry(hass) + await hass.async_block_till_done() + + assert mock_ping.call_count == 1 + assert mock_ping.call_args == call(device.host) + + +async def test_heartbeat_ignore_oserror(hass, caplog): + """Test that an OSError is ignored.""" + device = get_device("Office") + + with patch(DEVICE_PING, side_effect=OSError()): + await device.setup_entry(hass) + await hass.async_block_till_done() + + assert "Failed to send heartbeat to" in caplog.text + + +async def test_heartbeat_trigger_right_time(hass): + """Test that the heartbeat is triggered at the right time.""" + device = get_device("Office") + + await device.setup_entry(hass) + await hass.async_block_till_done() + + with patch(DEVICE_PING) as mock_ping: + async_fire_time_changed( + hass, dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL + ) + await hass.async_block_till_done() + + assert mock_ping.call_count == 1 + assert mock_ping.call_args == call(device.host) + + +async def test_heartbeat_do_not_trigger_before_time(hass): + """Test that the heartbeat is not triggered before the time.""" + device = get_device("Office") + + await device.setup_entry(hass) + await hass.async_block_till_done() + + with patch(DEVICE_PING) as mock_ping: + async_fire_time_changed( + hass, + dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL // 2, + ) + await hass.async_block_till_done() + + assert mock_ping.call_count == 0 + + +async def test_heartbeat_unload(hass): + """Test that the heartbeat is deactivated when the last config entry is removed.""" + device = get_device("Office") + + _, mock_entry = await device.setup_entry(hass) + await hass.async_block_till_done() + + await hass.config_entries.async_remove(mock_entry.entry_id) + await hass.async_block_till_done() + + with patch(DEVICE_PING) as mock_ping: + async_fire_time_changed( + hass, dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL + ) + + assert mock_ping.call_count == 0 + + +async def test_heartbeat_do_not_unload(hass): + """Test that the heartbeat is not deactivated until the last config entry is removed.""" + device_a = get_device("Office") + device_b = get_device("Bedroom") + + _, mock_entry_a = await device_a.setup_entry(hass) + await device_b.setup_entry(hass) + await hass.async_block_till_done() + + await hass.config_entries.async_remove(mock_entry_a.entry_id) + await hass.async_block_till_done() + + with patch(DEVICE_PING) as mock_ping: + async_fire_time_changed( + hass, dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL + ) + await hass.async_block_till_done() + + assert mock_ping.call_count == 1 + assert mock_ping.call_args == call(device_b.host) From 2976bbbbdd91ec4276ababf0dd8ed367e8e4b3ba Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 20 May 2021 00:08:23 -0700 Subject: [PATCH 587/852] Store Hue bridge in hass.data before setting up platforms (#50703) * Store bridge in hass.data before setting up platforms * Self --- homeassistant/components/hue/__init__.py | 7 +++---- homeassistant/components/hue/bridge.py | 9 +++++++- tests/components/hue/conftest.py | 26 +++++++++++++++++++++++- tests/components/hue/test_init.py | 9 ++++++-- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 6bbe3d9ebdd..71b62e22d33 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -66,7 +66,6 @@ async def async_setup_entry( _register_services(hass) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = bridge config = bridge.api.config # For backwards compat @@ -133,11 +132,11 @@ async def async_setup_entry( async def async_unload_entry(hass, entry): """Unload a config entry.""" - bridge = hass.data[DOMAIN].pop(entry.entry_id) + unload_success = await hass.data[DOMAIN][entry.entry_id].async_reset() if len(hass.data[DOMAIN]) == 0: hass.data.pop(DOMAIN) hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE) - return await bridge.async_reset() + return unload_success @core.callback @@ -172,7 +171,7 @@ def _register_services(hass): group_name, ) - if DOMAIN not in hass.data: + if not hass.services.has_service(DOMAIN, SERVICE_HUE_SCENE): # Register a local handler for scene activation hass.services.async_register( DOMAIN, diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 8f027aee033..1f19138b28f 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -23,6 +23,7 @@ from .const import ( CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_UNREACHABLE, + DOMAIN, LOGGER, ) from .errors import AuthenticationRequired, CannotConnect @@ -107,6 +108,7 @@ class HueBridge: if bridge.sensors is not None: self.sensor_manager = SensorManager(self) + hass.data.setdefault(DOMAIN, {})[self.config_entry.entry_id] = self hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) self.parallel_updates_semaphore = asyncio.Semaphore( @@ -173,10 +175,15 @@ class HueBridge: # If setup was successful, we set api variable, forwarded entry and # register service - return await self.hass.config_entries.async_unload_platforms( + unload_success = await self.hass.config_entries.async_unload_platforms( self.config_entry, PLATFORMS ) + if unload_success: + self.hass.data[DOMAIN].pop(self.config_entry.entry_id) + + return unload_success + async def hue_activate_scene(self, data, skip_reload=False, hide_warnings=False): """Service to call directly into bridge to set scenes.""" if self.api.scenes is None: diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 3aecacac58d..b5c2aec3042 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -32,6 +32,7 @@ def create_mock_bridge(hass): allow_unreachable=False, allow_groups=False, api=create_mock_api(hass), + config_entry=None, reset_jobs=[], spec=hue.HueBridge, ) @@ -41,10 +42,25 @@ def create_mock_bridge(hass): bridge.mock_group_responses = bridge.api.mock_group_responses bridge.mock_sensor_responses = bridge.api.mock_sensor_responses + async def async_setup(): + if bridge.config_entry: + hass.data.setdefault(hue.DOMAIN, {})[bridge.config_entry.entry_id] = bridge + return True + + bridge.async_setup = async_setup + async def async_request_call(task): await task() bridge.async_request_call = async_request_call + + async def async_reset(): + if bridge.config_entry: + hass.data[hue.DOMAIN].pop(bridge.config_entry.entry_id) + return True + + bridge.async_reset = async_reset + return bridge @@ -80,7 +96,15 @@ def create_mock_api(hass): logger = logging.getLogger(__name__) - api.config.apiversion = "9.9.9" + api.config = Mock( + bridgeid="ff:ff:ff:ff:ff:ff", + mac="aa:bb:cc:dd:ee:ff", + modelid="BSB002", + apiversion="9.9.9", + swversion="1935144040", + ) + api.config.name = "Home" + api.lights = Lights(logger, {}, [], mock_request) api.groups = Groups(logger, {}, [], mock_request) api.sensors = Sensors(logger, {}, [], mock_request) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 0c1d75c2ce2..afc920a6667 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -38,9 +38,14 @@ async def test_unload_entry(hass, mock_bridge_setup): assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert len(mock_bridge_setup.mock_calls) == 1 - mock_bridge_setup.async_reset = AsyncMock(return_value=True) + hass.data[hue.DOMAIN] = {entry.entry_id: mock_bridge_setup} + + async def mock_reset(): + hass.data[hue.DOMAIN].pop(entry.entry_id) + return True + + mock_bridge_setup.async_reset = mock_reset assert await hue.async_unload_entry(hass, entry) - assert len(mock_bridge_setup.async_reset.mock_calls) == 1 assert hue.DOMAIN not in hass.data From 623baa7964387227bf5d0d5e4054a1a567839ea7 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 20 May 2021 09:25:31 +0200 Subject: [PATCH 588/852] Fix zamg station check (#49367) --- homeassistant/components/zamg/sensor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 36c9a5fa380..9731e033972 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -108,7 +108,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): station_id = config.get(CONF_STATION_ID) or closest_station( latitude, longitude, hass.config.config_dir ) - if station_id not in zamg_stations(hass.config.config_dir): + if station_id not in _get_ogd_stations(): _LOGGER.error( "Configured ZAMG %s (%s) is not a known station", CONF_STATION_ID, @@ -239,9 +239,14 @@ class ZamgData: return self.data.get(variable) +def _get_ogd_stations(): + """Return all stations in the OGD dataset.""" + return {r["Station"] for r in ZamgData.current_observations()} + + def _get_zamg_stations(): """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config.""" - capital_stations = {r["Station"] for r in ZamgData.current_observations()} + capital_stations = _get_ogd_stations() req = requests.get( "https://www.zamg.ac.at/cms/en/documents/climate/" "doc_metnetwork/zamg-observation-points", From d4d335fb9cfee781558c8ccd6461d663f55651e3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 20 May 2021 09:27:38 +0200 Subject: [PATCH 589/852] Fix MQTT debug info for removed triggers (#50859) --- homeassistant/components/mqtt/debug_info.py | 2 +- tests/components/mqtt/test_device_trigger.py | 44 +++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index e8f0b5784ee..d00d65c2451 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -159,7 +159,7 @@ async def info_for_device(hass, device_id): ) for trigger in mqtt_debug_info["triggers"].values(): - if trigger["device_id"] != device_id: + if trigger["device_id"] != device_id or trigger["discovery_data"] is None: continue discovery_data = { diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 6d3c3b32bc3..61c7f73b5fb 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -1164,7 +1164,7 @@ async def test_trigger_debug_info(hass, mqtt_mock): """ registry = dr.async_get(hass) - config = { + config1 = { "platform": "mqtt", "automation_type": "trigger", "topic": "test-topic", @@ -1178,8 +1178,20 @@ async def test_trigger_debug_info(hass, mqtt_mock): "sw_version": "0.1-beta", }, } - data = json.dumps(config) - async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) + config2 = { + "platform": "mqtt", + "automation_type": "trigger", + "topic": "test-topic2", + "type": "foo", + "subtype": "bar", + "device": { + "connections": [[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"]], + }, + } + data = json.dumps(config1) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data) + data = json.dumps(config2) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data) await hass.async_block_till_done() device = registry.async_get_device( @@ -1187,11 +1199,33 @@ async def test_trigger_debug_info(hass, mqtt_mock): ) assert device is not None + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 0 + assert len(debug_info_data["triggers"]) == 2 + topic_map = { + "homeassistant/device_automation/bla1/config": config1, + "homeassistant/device_automation/bla2/config": config2, + } + assert ( + topic_map[debug_info_data["triggers"][0]["discovery_data"]["topic"]] + != topic_map[debug_info_data["triggers"][1]["discovery_data"]["topic"]] + ) + assert ( + debug_info_data["triggers"][0]["discovery_data"]["payload"] + == topic_map[debug_info_data["triggers"][0]["discovery_data"]["topic"]] + ) + assert ( + debug_info_data["triggers"][1]["discovery_data"]["payload"] + == topic_map[debug_info_data["triggers"][1]["discovery_data"]["topic"]] + ) + + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "") + await hass.async_block_till_done() debug_info_data = await debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 0 assert len(debug_info_data["triggers"]) == 1 assert ( debug_info_data["triggers"][0]["discovery_data"]["topic"] - == "homeassistant/device_automation/bla/config" + == "homeassistant/device_automation/bla2/config" ) - assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config + assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config2 From ccd8e1332ccc0499add8ab178f7d924368a2555c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 20 May 2021 09:29:10 +0200 Subject: [PATCH 590/852] Address late review comments for AccuWeather integration (#50866) * Remove unnecessary converting datetime to str * Address late comments --- .../components/accuweather/__init__.py | 2 +- homeassistant/components/accuweather/const.py | 392 +++++++++--------- .../components/accuweather/sensor.py | 30 +- .../components/accuweather/weather.py | 4 +- 4 files changed, 225 insertions(+), 203 deletions(-) diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 27dd4b9c196..18a4bd2dce4 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -99,7 +99,7 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[Dict[str, Any]]): update_interval = timedelta(minutes=40) if self.forecast: update_interval *= 2 - _LOGGER.debug("Data will be update every %s", str(update_interval)) + _LOGGER.debug("Data will be update every %s", update_interval) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index e4ec49ce2ac..54d9b631ade 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -20,6 +20,8 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ) from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, CONCENTRATION_PARTS_PER_CUBIC_METER, DEVICE_CLASS_TEMPERATURE, LENGTH_FEET, @@ -37,8 +39,14 @@ from homeassistant.const import ( from .model import SensorDescription +API_IMPERIAL: Final = "Imperial" +API_METRIC: Final = "Metric" ATTRIBUTION: Final = "Data provided by AccuWeather" +ATTR_ENABLED: Final = "enabled" ATTR_FORECAST: Final = "forecast" +ATTR_LABEL: Final = "label" +ATTR_UNIT_IMPERIAL: Final = "unit_imperial" +ATTR_UNIT_METRIC: Final = "unit_metric" CONF_FORECAST: Final = "forecast" COORDINATOR: Final = "coordinator" DOMAIN: Final = "accuweather" @@ -66,262 +74,262 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = { FORECAST_SENSOR_TYPES: Final[dict[str, SensorDescription]] = { "CloudCoverDay": { - "device_class": None, - "icon": "mdi:weather-cloudy", - "label": "Cloud Cover Day", - "unit_metric": PERCENTAGE, - "unit_imperial": PERCENTAGE, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-cloudy", + ATTR_LABEL: "Cloud Cover Day", + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, + ATTR_ENABLED: False, }, "CloudCoverNight": { - "device_class": None, - "icon": "mdi:weather-cloudy", - "label": "Cloud Cover Night", - "unit_metric": PERCENTAGE, - "unit_imperial": PERCENTAGE, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-cloudy", + ATTR_LABEL: "Cloud Cover Night", + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, + ATTR_ENABLED: False, }, "Grass": { - "device_class": None, - "icon": "mdi:grass", - "label": "Grass Pollen", - "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER, - "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:grass", + ATTR_LABEL: "Grass Pollen", + ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED: False, }, "HoursOfSun": { - "device_class": None, - "icon": "mdi:weather-partly-cloudy", - "label": "Hours Of Sun", - "unit_metric": TIME_HOURS, - "unit_imperial": TIME_HOURS, - "enabled": True, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-partly-cloudy", + ATTR_LABEL: "Hours Of Sun", + ATTR_UNIT_METRIC: TIME_HOURS, + ATTR_UNIT_IMPERIAL: TIME_HOURS, + ATTR_ENABLED: True, }, "Mold": { - "device_class": None, - "icon": "mdi:blur", - "label": "Mold Pollen", - "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER, - "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: "Mold Pollen", + ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED: False, }, "Ozone": { - "device_class": None, - "icon": "mdi:vector-triangle", - "label": "Ozone", - "unit_metric": None, - "unit_imperial": None, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:vector-triangle", + ATTR_LABEL: "Ozone", + ATTR_UNIT_METRIC: None, + ATTR_UNIT_IMPERIAL: None, + ATTR_ENABLED: False, }, "Ragweed": { - "device_class": None, - "icon": "mdi:sprout", - "label": "Ragweed Pollen", - "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER, - "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:sprout", + ATTR_LABEL: "Ragweed Pollen", + ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED: False, }, "RealFeelTemperatureMax": { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": "RealFeel Temperature Max", - "unit_metric": TEMP_CELSIUS, - "unit_imperial": TEMP_FAHRENHEIT, - "enabled": True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature Max", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + ATTR_ENABLED: True, }, "RealFeelTemperatureMin": { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": "RealFeel Temperature Min", - "unit_metric": TEMP_CELSIUS, - "unit_imperial": TEMP_FAHRENHEIT, - "enabled": True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature Min", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + ATTR_ENABLED: True, }, "RealFeelTemperatureShadeMax": { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": "RealFeel Temperature Shade Max", - "unit_metric": TEMP_CELSIUS, - "unit_imperial": TEMP_FAHRENHEIT, - "enabled": False, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature Shade Max", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + ATTR_ENABLED: False, }, "RealFeelTemperatureShadeMin": { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": "RealFeel Temperature Shade Min", - "unit_metric": TEMP_CELSIUS, - "unit_imperial": TEMP_FAHRENHEIT, - "enabled": False, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature Shade Min", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + ATTR_ENABLED: False, }, "ThunderstormProbabilityDay": { - "device_class": None, - "icon": "mdi:weather-lightning", - "label": "Thunderstorm Probability Day", - "unit_metric": PERCENTAGE, - "unit_imperial": PERCENTAGE, - "enabled": True, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-lightning", + ATTR_LABEL: "Thunderstorm Probability Day", + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, + ATTR_ENABLED: True, }, "ThunderstormProbabilityNight": { - "device_class": None, - "icon": "mdi:weather-lightning", - "label": "Thunderstorm Probability Night", - "unit_metric": PERCENTAGE, - "unit_imperial": PERCENTAGE, - "enabled": True, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-lightning", + ATTR_LABEL: "Thunderstorm Probability Night", + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, + ATTR_ENABLED: True, }, "Tree": { - "device_class": None, - "icon": "mdi:tree-outline", - "label": "Tree Pollen", - "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER, - "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:tree-outline", + ATTR_LABEL: "Tree Pollen", + ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED: False, }, "UVIndex": { - "device_class": None, - "icon": "mdi:weather-sunny", - "label": "UV Index", - "unit_metric": UV_INDEX, - "unit_imperial": UV_INDEX, - "enabled": True, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-sunny", + ATTR_LABEL: "UV Index", + ATTR_UNIT_METRIC: UV_INDEX, + ATTR_UNIT_IMPERIAL: UV_INDEX, + ATTR_ENABLED: True, }, "WindGustDay": { - "device_class": None, - "icon": "mdi:weather-windy", - "label": "Wind Gust Day", - "unit_metric": SPEED_KILOMETERS_PER_HOUR, - "unit_imperial": SPEED_MILES_PER_HOUR, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Gust Day", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + ATTR_ENABLED: False, }, "WindGustNight": { - "device_class": None, - "icon": "mdi:weather-windy", - "label": "Wind Gust Night", - "unit_metric": SPEED_KILOMETERS_PER_HOUR, - "unit_imperial": SPEED_MILES_PER_HOUR, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Gust Night", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + ATTR_ENABLED: False, }, "WindDay": { - "device_class": None, - "icon": "mdi:weather-windy", - "label": "Wind Day", - "unit_metric": SPEED_KILOMETERS_PER_HOUR, - "unit_imperial": SPEED_MILES_PER_HOUR, - "enabled": True, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Day", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + ATTR_ENABLED: True, }, "WindNight": { - "device_class": None, - "icon": "mdi:weather-windy", - "label": "Wind Night", - "unit_metric": SPEED_KILOMETERS_PER_HOUR, - "unit_imperial": SPEED_MILES_PER_HOUR, - "enabled": True, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Night", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + ATTR_ENABLED: True, }, } SENSOR_TYPES: Final[dict[str, SensorDescription]] = { "ApparentTemperature": { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": "Apparent Temperature", - "unit_metric": TEMP_CELSIUS, - "unit_imperial": TEMP_FAHRENHEIT, - "enabled": False, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Apparent Temperature", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + ATTR_ENABLED: False, }, "Ceiling": { - "device_class": None, - "icon": "mdi:weather-fog", - "label": "Cloud Ceiling", - "unit_metric": LENGTH_METERS, - "unit_imperial": LENGTH_FEET, - "enabled": True, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-fog", + ATTR_LABEL: "Cloud Ceiling", + ATTR_UNIT_METRIC: LENGTH_METERS, + ATTR_UNIT_IMPERIAL: LENGTH_FEET, + ATTR_ENABLED: True, }, "CloudCover": { - "device_class": None, - "icon": "mdi:weather-cloudy", - "label": "Cloud Cover", - "unit_metric": PERCENTAGE, - "unit_imperial": PERCENTAGE, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-cloudy", + ATTR_LABEL: "Cloud Cover", + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, + ATTR_ENABLED: False, }, "DewPoint": { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": "Dew Point", - "unit_metric": TEMP_CELSIUS, - "unit_imperial": TEMP_FAHRENHEIT, - "enabled": False, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Dew Point", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + ATTR_ENABLED: False, }, "RealFeelTemperature": { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": "RealFeel Temperature", - "unit_metric": TEMP_CELSIUS, - "unit_imperial": TEMP_FAHRENHEIT, - "enabled": True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + ATTR_ENABLED: True, }, "RealFeelTemperatureShade": { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": "RealFeel Temperature Shade", - "unit_metric": TEMP_CELSIUS, - "unit_imperial": TEMP_FAHRENHEIT, - "enabled": False, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature Shade", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + ATTR_ENABLED: False, }, "Precipitation": { - "device_class": None, - "icon": "mdi:weather-rainy", - "label": "Precipitation", - "unit_metric": LENGTH_MILLIMETERS, - "unit_imperial": LENGTH_INCHES, - "enabled": True, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-rainy", + ATTR_LABEL: "Precipitation", + ATTR_UNIT_METRIC: LENGTH_MILLIMETERS, + ATTR_UNIT_IMPERIAL: LENGTH_INCHES, + ATTR_ENABLED: True, }, "PressureTendency": { - "device_class": "accuweather__pressure_tendency", - "icon": "mdi:gauge", - "label": "Pressure Tendency", - "unit_metric": None, - "unit_imperial": None, - "enabled": True, + ATTR_DEVICE_CLASS: "accuweather__pressure_tendency", + ATTR_ICON: "mdi:gauge", + ATTR_LABEL: "Pressure Tendency", + ATTR_UNIT_METRIC: None, + ATTR_UNIT_IMPERIAL: None, + ATTR_ENABLED: True, }, "UVIndex": { - "device_class": None, - "icon": "mdi:weather-sunny", - "label": "UV Index", - "unit_metric": UV_INDEX, - "unit_imperial": UV_INDEX, - "enabled": True, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-sunny", + ATTR_LABEL: "UV Index", + ATTR_UNIT_METRIC: UV_INDEX, + ATTR_UNIT_IMPERIAL: UV_INDEX, + ATTR_ENABLED: True, }, "WetBulbTemperature": { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": "Wet Bulb Temperature", - "unit_metric": TEMP_CELSIUS, - "unit_imperial": TEMP_FAHRENHEIT, - "enabled": False, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Wet Bulb Temperature", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + ATTR_ENABLED: False, }, "WindChillTemperature": { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": "Wind Chill Temperature", - "unit_metric": TEMP_CELSIUS, - "unit_imperial": TEMP_FAHRENHEIT, - "enabled": False, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Wind Chill Temperature", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + ATTR_ENABLED: False, }, "Wind": { - "device_class": None, - "icon": "mdi:weather-windy", - "label": "Wind", - "unit_metric": SPEED_KILOMETERS_PER_HOUR, - "unit_imperial": SPEED_MILES_PER_HOUR, - "enabled": True, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + ATTR_ENABLED: True, }, "WindGust": { - "device_class": None, - "icon": "mdi:weather-windy", - "label": "Wind Gust", - "unit_metric": SPEED_KILOMETERS_PER_HOUR, - "unit_imperial": SPEED_MILES_PER_HOUR, - "enabled": False, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Gust", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + ATTR_ENABLED: False, }, } diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 9f2d9ed78bd..09e9cda30ad 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -5,7 +5,13 @@ from typing import Any, cast from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TEMPERATURE +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + ATTR_ICON, + CONF_NAME, + DEVICE_CLASS_TEMPERATURE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -14,7 +20,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import AccuWeatherDataUpdateCoordinator from .const import ( + API_IMPERIAL, + API_METRIC, + ATTR_ENABLED, ATTR_FORECAST, + ATTR_LABEL, + ATTR_UNIT_IMPERIAL, + ATTR_UNIT_METRIC, ATTRIBUTION, COORDINATOR, DOMAIN, @@ -79,7 +91,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): else: self._description = FORECAST_SENSOR_TYPES[kind] self._sensor_data = coordinator.data[ATTR_FORECAST][forecast_day][kind] - self._unit_system = "Metric" if coordinator.is_metric else "Imperial" + self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL self._name = name self.kind = kind self._device_class = None @@ -90,8 +102,8 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): def name(self) -> str: """Return the name.""" if self.forecast_day is not None: - return f"{self._name} {self._description['label']} {self.forecast_day}d" - return f"{self._name} {self._description['label']}" + return f"{self._name} {self._description[ATTR_LABEL]} {self.forecast_day}d" + return f"{self._name} {self._description[ATTR_LABEL]}" @property def unique_id(self) -> str: @@ -137,19 +149,19 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): @property def icon(self) -> str | None: """Return the icon.""" - return self._description["icon"] + return self._description[ATTR_ICON] @property def device_class(self) -> str | None: """Return the device_class.""" - return self._description["device_class"] + return self._description[ATTR_DEVICE_CLASS] @property def unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if self.coordinator.is_metric: - return self._description["unit_metric"] - return self._description["unit_imperial"] + return self._description[ATTR_UNIT_METRIC] + return self._description[ATTR_UNIT_IMPERIAL] @property def extra_state_attributes(self) -> dict[str, Any]: @@ -169,4 +181,4 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return self._description["enabled"] + return self._description[ATTR_ENABLED] diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index e4745537c4f..0dc4c7e270c 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -25,6 +25,8 @@ from homeassistant.util.dt import utc_from_timestamp from . import AccuWeatherDataUpdateCoordinator from .const import ( + API_IMPERIAL, + API_METRIC, ATTR_FORECAST, ATTRIBUTION, CONDITION_CLASSES, @@ -61,7 +63,7 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): """Initialize.""" super().__init__(coordinator) self._name = name - self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial" + self._unit_system = API_METRIC if self.coordinator.is_metric else API_IMPERIAL @property def name(self) -> str: From 3bdefc5da7801e94b4a539d2a016769dd7d4dee6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 20 May 2021 11:34:32 +0200 Subject: [PATCH 591/852] Use constants with TypedDict (#50879) --- homeassistant/components/airly/const.py | 38 +++++++++++++----------- homeassistant/components/airly/sensor.py | 17 +++++++---- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 5136f54d6f2..c3dd51e4f69 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -4,6 +4,8 @@ from __future__ import annotations from typing import Final from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -20,15 +22,17 @@ ATTR_API_CAQI: Final = "CAQI" ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION" ATTR_API_CAQI_LEVEL: Final = "LEVEL" ATTR_API_HUMIDITY: Final = "HUMIDITY" -ATTR_API_PM1: Final = "PM1" ATTR_API_PM10: Final = "PM10" ATTR_API_PM10_LIMIT: Final = "PM10_LIMIT" ATTR_API_PM10_PERCENT: Final = "PM10_PERCENT" +ATTR_API_PM1: Final = "PM1" ATTR_API_PM25: Final = "PM25" ATTR_API_PM25_LIMIT: Final = "PM25_LIMIT" ATTR_API_PM25_PERCENT: Final = "PM25_PERCENT" ATTR_API_PRESSURE: Final = "PRESSURE" ATTR_API_TEMPERATURE: Final = "TEMPERATURE" +ATTR_LABEL: Final = "label" +ATTR_UNIT: Final = "unit" ATTRIBUTION: Final = "Data provided by Airly" CONF_USE_NEAREST: Final = "use_nearest" @@ -42,27 +46,27 @@ NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." SENSOR_TYPES: dict[str, SensorDescription] = { ATTR_API_PM1: { - "device_class": None, - "icon": "mdi:blur", - "label": ATTR_API_PM1, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_PM1, + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, }, ATTR_API_HUMIDITY: { - "device_class": DEVICE_CLASS_HUMIDITY, - "icon": None, - "label": ATTR_API_HUMIDITY.capitalize(), - "unit": PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(), + ATTR_UNIT: PERCENTAGE, }, ATTR_API_PRESSURE: { - "device_class": DEVICE_CLASS_PRESSURE, - "icon": None, - "label": ATTR_API_PRESSURE.capitalize(), - "unit": PRESSURE_HPA, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_PRESSURE.capitalize(), + ATTR_UNIT: PRESSURE_HPA, }, ATTR_API_TEMPERATURE: { - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "label": ATTR_API_TEMPERATURE.capitalize(), - "unit": TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(), + ATTR_UNIT: TEMP_CELSIUS, }, } diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index b978afb25a9..4544a806349 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -5,7 +5,12 @@ from typing import Any, cast from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + ATTR_ICON, + CONF_NAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -16,6 +21,8 @@ from . import AirlyDataUpdateCoordinator from .const import ( ATTR_API_PM1, ATTR_API_PRESSURE, + ATTR_LABEL, + ATTR_UNIT, ATTRIBUTION, DEFAULT_NAME, DOMAIN, @@ -63,7 +70,7 @@ class AirlySensor(CoordinatorEntity, SensorEntity): @property def name(self) -> str: """Return the name.""" - return f"{self._name} {self._description['label']}" + return f"{self._name} {self._description[ATTR_LABEL]}" @property def state(self) -> StateType: @@ -81,12 +88,12 @@ class AirlySensor(CoordinatorEntity, SensorEntity): @property def icon(self) -> str | None: """Return the icon.""" - return self._description["icon"] + return self._description[ATTR_ICON] @property def device_class(self) -> str | None: """Return the device_class.""" - return self._description["device_class"] + return self._description[ATTR_DEVICE_CLASS] @property def unique_id(self) -> str: @@ -111,4 +118,4 @@ class AirlySensor(CoordinatorEntity, SensorEntity): @property def unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" - return self._description["unit"] + return self._description[ATTR_UNIT] From 953e6ebe626a34ada24680f0e27b04e5482f40d8 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 20 May 2021 11:36:23 +0200 Subject: [PATCH 592/852] Use constants with TypedDict (#50880) --- homeassistant/components/brother/const.py | 197 +++++++++++---------- homeassistant/components/brother/sensor.py | 13 +- 2 files changed, 108 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 2a2e8724821..52170057fb1 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Final -from homeassistant.const import PERCENTAGE +from homeassistant.const import ATTR_ICON, PERCENTAGE from .model import SensorDescription @@ -25,7 +25,9 @@ ATTR_DRUM_COUNTER: Final = "drum_counter" ATTR_DRUM_REMAINING_LIFE: Final = "drum_remaining_life" ATTR_DRUM_REMAINING_PAGES: Final = "drum_remaining_pages" ATTR_DUPLEX_COUNTER: Final = "duplex_unit_pages_counter" +ATTR_ENABLED: Final = "enabled" ATTR_FUSER_REMAINING_LIFE: Final = "fuser_remaining_life" +ATTR_LABEL: Final = "label" ATTR_LASER_REMAINING_LIFE: Final = "laser_remaining_life" ATTR_MAGENTA_DRUM_COUNTER: Final = "magenta_drum_counter" ATTR_MAGENTA_DRUM_REMAINING_LIFE: Final = "magenta_drum_remaining_life" @@ -38,6 +40,7 @@ ATTR_PF_KIT_1_REMAINING_LIFE: Final = "pf_kit_1_remaining_life" ATTR_PF_KIT_MP_REMAINING_LIFE: Final = "pf_kit_mp_remaining_life" ATTR_REMAINING_PAGES: Final = "remaining_pages" ATTR_STATUS: Final = "status" +ATTR_UNIT: Final = "unit" ATTR_UPTIME: Final = "uptime" ATTR_YELLOW_DRUM_COUNTER: Final = "yellow_drum_counter" ATTR_YELLOW_DRUM_REMAINING_LIFE: Final = "yellow_drum_remaining_life" @@ -77,147 +80,147 @@ ATTRS_MAP: Final[dict[str, tuple[str, str]]] = { SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_STATUS: { - "icon": "mdi:printer", - "label": ATTR_STATUS.title(), - "unit": None, - "enabled": True, + ATTR_ICON: "mdi:printer", + ATTR_LABEL: ATTR_STATUS.title(), + ATTR_UNIT: None, + ATTR_ENABLED: True, }, ATTR_PAGE_COUNTER: { - "icon": "mdi:file-document-outline", - "label": ATTR_PAGE_COUNTER.replace("_", " ").title(), - "unit": UNIT_PAGES, - "enabled": True, + ATTR_ICON: "mdi:file-document-outline", + ATTR_LABEL: ATTR_PAGE_COUNTER.replace("_", " ").title(), + ATTR_UNIT: UNIT_PAGES, + ATTR_ENABLED: True, }, ATTR_BW_COUNTER: { - "icon": "mdi:file-document-outline", - "label": ATTR_BW_COUNTER.replace("_", " ").title(), - "unit": UNIT_PAGES, - "enabled": True, + ATTR_ICON: "mdi:file-document-outline", + ATTR_LABEL: ATTR_BW_COUNTER.replace("_", " ").title(), + ATTR_UNIT: UNIT_PAGES, + ATTR_ENABLED: True, }, ATTR_COLOR_COUNTER: { - "icon": "mdi:file-document-outline", - "label": ATTR_COLOR_COUNTER.replace("_", " ").title(), - "unit": UNIT_PAGES, - "enabled": True, + ATTR_ICON: "mdi:file-document-outline", + ATTR_LABEL: ATTR_COLOR_COUNTER.replace("_", " ").title(), + ATTR_UNIT: UNIT_PAGES, + ATTR_ENABLED: True, }, ATTR_DUPLEX_COUNTER: { - "icon": "mdi:file-document-outline", - "label": ATTR_DUPLEX_COUNTER.replace("_", " ").title(), - "unit": UNIT_PAGES, - "enabled": True, + ATTR_ICON: "mdi:file-document-outline", + ATTR_LABEL: ATTR_DUPLEX_COUNTER.replace("_", " ").title(), + ATTR_UNIT: UNIT_PAGES, + ATTR_ENABLED: True, }, ATTR_DRUM_REMAINING_LIFE: { - "icon": "mdi:chart-donut", - "label": ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:chart-donut", + ATTR_LABEL: ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BLACK_DRUM_REMAINING_LIFE: { - "icon": "mdi:chart-donut", - "label": ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:chart-donut", + ATTR_LABEL: ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_CYAN_DRUM_REMAINING_LIFE: { - "icon": "mdi:chart-donut", - "label": ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:chart-donut", + ATTR_LABEL: ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_MAGENTA_DRUM_REMAINING_LIFE: { - "icon": "mdi:chart-donut", - "label": ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:chart-donut", + ATTR_LABEL: ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_YELLOW_DRUM_REMAINING_LIFE: { - "icon": "mdi:chart-donut", - "label": ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:chart-donut", + ATTR_LABEL: ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BELT_UNIT_REMAINING_LIFE: { - "icon": "mdi:current-ac", - "label": ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:current-ac", + ATTR_LABEL: ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_FUSER_REMAINING_LIFE: { - "icon": "mdi:water-outline", - "label": ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:water-outline", + ATTR_LABEL: ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_LASER_REMAINING_LIFE: { - "icon": "mdi:spotlight-beam", - "label": ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:spotlight-beam", + ATTR_LABEL: ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_PF_KIT_1_REMAINING_LIFE: { - "icon": "mdi:printer-3d", - "label": ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:printer-3d", + ATTR_LABEL: ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_PF_KIT_MP_REMAINING_LIFE: { - "icon": "mdi:printer-3d", - "label": ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:printer-3d", + ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BLACK_TONER_REMAINING: { - "icon": "mdi:printer-3d-nozzle", - "label": ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_CYAN_TONER_REMAINING: { - "icon": "mdi:printer-3d-nozzle", - "label": ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_MAGENTA_TONER_REMAINING: { - "icon": "mdi:printer-3d-nozzle", - "label": ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_YELLOW_TONER_REMAINING: { - "icon": "mdi:printer-3d-nozzle", - "label": ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BLACK_INK_REMAINING: { - "icon": "mdi:printer-3d-nozzle", - "label": ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_CYAN_INK_REMAINING: { - "icon": "mdi:printer-3d-nozzle", - "label": ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_MAGENTA_INK_REMAINING: { - "icon": "mdi:printer-3d-nozzle", - "label": ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_YELLOW_INK_REMAINING: { - "icon": "mdi:printer-3d-nozzle", - "label": ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), - "unit": PERCENTAGE, - "enabled": True, + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), + ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_UPTIME: { - "icon": None, - "label": ATTR_UPTIME.title(), - "unit": None, - "enabled": False, + ATTR_ICON: None, + ATTR_LABEL: ATTR_UPTIME.title(), + ATTR_UNIT: None, + ATTR_ENABLED: False, }, } diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 2f66e1c75d5..50c9b8d79ff 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ATTR_ICON, DEVICE_CLASS_TIMESTAMP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -14,8 +14,11 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import BrotherDataUpdateCoordinator from .const import ( ATTR_COUNTER, + ATTR_ENABLED, + ATTR_LABEL, ATTR_MANUFACTURER, ATTR_REMAINING_PAGES, + ATTR_UNIT, ATTR_UPTIME, ATTRS_MAP, DATA_CONFIG_ENTRY, @@ -58,7 +61,7 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): """Initialize.""" super().__init__(coordinator) self._description = SENSOR_TYPES[kind] - self._name = f"{coordinator.data.model} {self._description['label']}" + self._name = f"{coordinator.data.model} {self._description[ATTR_LABEL]}" self._unique_id = f"{coordinator.data.serial.lower()}_{kind}" self._device_info = device_info self.kind = kind @@ -97,7 +100,7 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): @property def icon(self) -> str | None: """Return the icon.""" - return self._description["icon"] + return self._description[ATTR_ICON] @property def unique_id(self) -> str: @@ -107,7 +110,7 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): @property def unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" - return self._description["unit"] + return self._description[ATTR_UNIT] @property def device_info(self) -> DeviceInfo: @@ -117,4 +120,4 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return self._description["enabled"] + return self._description[ATTR_ENABLED] From be6a1bf09613ab5906be9793609b5f2982525c44 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 20 May 2021 12:23:41 +0200 Subject: [PATCH 593/852] Create KNX climate entity directly from config (#49638) * create climate entities directly from config * deprecate create_temperature_sensors * move create staticmethod to module level * use get() fro optional CONF_SETPOINT_SHIFT_MODE * Fix deprecated version comment Co-authored-by: Martin Hjelmare --- homeassistant/components/knx/climate.py | 109 ++++++++++++++++++++---- homeassistant/components/knx/factory.py | 82 +----------------- homeassistant/components/knx/schema.py | 6 +- 3 files changed, 95 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index aec587c9d0e..b8c07767005 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -3,7 +3,8 @@ from __future__ import annotations from typing import Any -from xknx.devices import Climate as XknxClimate +from xknx import XKNX +from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode from xknx.telegram.address import parse_device_group_address @@ -15,7 +16,7 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,24 +37,26 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up climate(s) for KNX platform.""" - _async_migrate_unique_id(hass, discovery_info) + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + + _async_migrate_unique_id(hass, platform_config) entities = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxClimate): - entities.append(KNXClimate(device)) + for entity_config in platform_config: + entities.append(KNXClimate(xknx, entity_config)) + async_add_entities(entities) @callback def _async_migrate_unique_id( - hass: HomeAssistant, discovery_info: DiscoveryInfoType | None + hass: HomeAssistant, platform_config: list[ConfigType] ) -> None: """Change unique_ids used in 2021.4 to include target_temperature GA.""" entity_registry = er.async_get(hass) - if not discovery_info or not discovery_info["platform_config"]: - return - - platform_config = discovery_info["platform_config"] for entity_config in platform_config: # normalize group address strings - ga_temperature_state was the old uid ga_temperature_state = parse_device_group_address( @@ -88,18 +91,90 @@ def _async_migrate_unique_id( entity_registry.async_update_entity(entity_id, new_unique_id=new_uid) +def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: + """Return a KNX Climate device to be used within XKNX.""" + climate_mode = XknxClimateMode( + xknx, + name=f"{config[CONF_NAME]} Mode", + group_address_operation_mode=config.get( + ClimateSchema.CONF_OPERATION_MODE_ADDRESS + ), + group_address_operation_mode_state=config.get( + ClimateSchema.CONF_OPERATION_MODE_STATE_ADDRESS + ), + group_address_controller_status=config.get( + ClimateSchema.CONF_CONTROLLER_STATUS_ADDRESS + ), + group_address_controller_status_state=config.get( + ClimateSchema.CONF_CONTROLLER_STATUS_STATE_ADDRESS + ), + group_address_controller_mode=config.get( + ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS + ), + group_address_controller_mode_state=config.get( + ClimateSchema.CONF_CONTROLLER_MODE_STATE_ADDRESS + ), + group_address_operation_mode_protection=config.get( + ClimateSchema.CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS + ), + group_address_operation_mode_night=config.get( + ClimateSchema.CONF_OPERATION_MODE_NIGHT_ADDRESS + ), + group_address_operation_mode_comfort=config.get( + ClimateSchema.CONF_OPERATION_MODE_COMFORT_ADDRESS + ), + group_address_operation_mode_standby=config.get( + ClimateSchema.CONF_OPERATION_MODE_STANDBY_ADDRESS + ), + group_address_heat_cool=config.get(ClimateSchema.CONF_HEAT_COOL_ADDRESS), + group_address_heat_cool_state=config.get( + ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS + ), + operation_modes=config.get(ClimateSchema.CONF_OPERATION_MODES), + controller_modes=config.get(ClimateSchema.CONF_CONTROLLER_MODES), + ) + + return XknxClimate( + xknx, + name=config[CONF_NAME], + group_address_temperature=config[ClimateSchema.CONF_TEMPERATURE_ADDRESS], + group_address_target_temperature=config.get( + ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS + ), + group_address_target_temperature_state=config[ + ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS + ], + group_address_setpoint_shift=config.get( + ClimateSchema.CONF_SETPOINT_SHIFT_ADDRESS + ), + group_address_setpoint_shift_state=config.get( + ClimateSchema.CONF_SETPOINT_SHIFT_STATE_ADDRESS + ), + setpoint_shift_mode=config.get(ClimateSchema.CONF_SETPOINT_SHIFT_MODE), + setpoint_shift_max=config[ClimateSchema.CONF_SETPOINT_SHIFT_MAX], + setpoint_shift_min=config[ClimateSchema.CONF_SETPOINT_SHIFT_MIN], + temperature_step=config[ClimateSchema.CONF_TEMPERATURE_STEP], + group_address_on_off=config.get(ClimateSchema.CONF_ON_OFF_ADDRESS), + group_address_on_off_state=config.get(ClimateSchema.CONF_ON_OFF_STATE_ADDRESS), + min_temp=config.get(ClimateSchema.CONF_MIN_TEMP), + max_temp=config.get(ClimateSchema.CONF_MAX_TEMP), + mode=climate_mode, + on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT], + ) + + class KNXClimate(KnxEntity, ClimateEntity): """Representation of a KNX climate device.""" - def __init__(self, device: XknxClimate) -> None: + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX climate device.""" self._device: XknxClimate - super().__init__(device) + super().__init__(_create_climate(xknx, config)) self._unique_id = ( - f"{device.temperature.group_address_state}_" - f"{device.target_temperature.group_address_state}_" - f"{device.target_temperature.group_address}_" - f"{device._setpoint_shift.group_address}" # pylint: disable=protected-access + f"{self._device.temperature.group_address_state}_" + f"{self._device.target_temperature.group_address_state}_" + f"{self._device.target_temperature.group_address}_" + f"{self._device._setpoint_shift.group_address}" # pylint: disable=protected-access ) self._unit_of_measurement = TEMP_CELSIUS diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 3c1ef0fdae4..7b5b19dcdd2 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -3,8 +3,6 @@ from __future__ import annotations from xknx import XKNX from xknx.devices import ( - Climate as XknxClimate, - ClimateMode as XknxClimateMode, Device as XknxDevice, Sensor as XknxSensor, Weather as XknxWeather, @@ -14,7 +12,7 @@ from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.helpers.typing import ConfigType from .const import SupportedPlatforms -from .schema import ClimateSchema, SensorSchema, WeatherSchema +from .schema import SensorSchema, WeatherSchema def create_knx_device( @@ -23,9 +21,6 @@ def create_knx_device( config: ConfigType, ) -> XknxDevice | None: """Return the requested XKNX device.""" - if platform is SupportedPlatforms.CLIMATE: - return _create_climate(knx_module, config) - if platform is SupportedPlatforms.SENSOR: return _create_sensor(knx_module, config) @@ -35,81 +30,6 @@ def create_knx_device( return None -def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: - """Return a KNX Climate device to be used within XKNX.""" - climate_mode = XknxClimateMode( - knx_module, - name=f"{config[CONF_NAME]} Mode", - group_address_operation_mode=config.get( - ClimateSchema.CONF_OPERATION_MODE_ADDRESS - ), - group_address_operation_mode_state=config.get( - ClimateSchema.CONF_OPERATION_MODE_STATE_ADDRESS - ), - group_address_controller_status=config.get( - ClimateSchema.CONF_CONTROLLER_STATUS_ADDRESS - ), - group_address_controller_status_state=config.get( - ClimateSchema.CONF_CONTROLLER_STATUS_STATE_ADDRESS - ), - group_address_controller_mode=config.get( - ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS - ), - group_address_controller_mode_state=config.get( - ClimateSchema.CONF_CONTROLLER_MODE_STATE_ADDRESS - ), - group_address_operation_mode_protection=config.get( - ClimateSchema.CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS - ), - group_address_operation_mode_night=config.get( - ClimateSchema.CONF_OPERATION_MODE_NIGHT_ADDRESS - ), - group_address_operation_mode_comfort=config.get( - ClimateSchema.CONF_OPERATION_MODE_COMFORT_ADDRESS - ), - group_address_operation_mode_standby=config.get( - ClimateSchema.CONF_OPERATION_MODE_STANDBY_ADDRESS - ), - group_address_heat_cool=config.get(ClimateSchema.CONF_HEAT_COOL_ADDRESS), - group_address_heat_cool_state=config.get( - ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS - ), - operation_modes=config.get(ClimateSchema.CONF_OPERATION_MODES), - controller_modes=config.get(ClimateSchema.CONF_CONTROLLER_MODES), - ) - - return XknxClimate( - knx_module, - name=config[CONF_NAME], - group_address_temperature=config[ClimateSchema.CONF_TEMPERATURE_ADDRESS], - group_address_target_temperature=config.get( - ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS - ), - group_address_target_temperature_state=config[ - ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS - ], - group_address_setpoint_shift=config.get( - ClimateSchema.CONF_SETPOINT_SHIFT_ADDRESS - ), - group_address_setpoint_shift_state=config.get( - ClimateSchema.CONF_SETPOINT_SHIFT_STATE_ADDRESS - ), - setpoint_shift_mode=config.get(ClimateSchema.CONF_SETPOINT_SHIFT_MODE), - setpoint_shift_max=config[ClimateSchema.CONF_SETPOINT_SHIFT_MAX], - setpoint_shift_min=config[ClimateSchema.CONF_SETPOINT_SHIFT_MIN], - temperature_step=config[ClimateSchema.CONF_TEMPERATURE_STEP], - group_address_on_off=config.get(ClimateSchema.CONF_ON_OFF_ADDRESS), - group_address_on_off_state=config.get(ClimateSchema.CONF_ON_OFF_STATE_ADDRESS), - min_temp=config.get(ClimateSchema.CONF_MIN_TEMP), - max_temp=config.get(ClimateSchema.CONF_MAX_TEMP), - mode=climate_mode, - on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT], - create_temperature_sensors=config[ - ClimateSchema.CONF_CREATE_TEMPERATURE_SENSORS - ], - ) - - def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor: """Return a KNX sensor to be used within XKNX.""" return XknxSensor( diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 2404ab37ed3..2dde2fd7160 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -159,7 +159,6 @@ class ClimateSchema: CONF_ON_OFF_INVERT = "on_off_invert" CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" - CONF_CREATE_TEMPERATURE_SENSORS = "create_temperature_sensors" DEFAULT_NAME = "KNX Climate" DEFAULT_SETPOINT_SHIFT_MODE = "DPT6010" @@ -171,6 +170,8 @@ class ClimateSchema: SCHEMA = vol.All( # deprecated since September 2020 cv.deprecated("setpoint_shift_step", replacement_key=CONF_TEMPERATURE_STEP), + # deprecated since 2021.6 + cv.deprecated("create_temperature_sensors"), vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -228,9 +229,6 @@ class ClimateSchema: ), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), - vol.Optional( - CONF_CREATE_TEMPERATURE_SENSORS, default=False - ): cv.boolean, } ), ) From aaae4cfc8f64eefb5313679a6f271e8532c5afa3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 20 May 2021 12:38:46 +0200 Subject: [PATCH 594/852] Use constants with TypedDict in Nettigo Air Monitor integration (#50883) * Use constants with TypedDict * Sensor names as consts --- homeassistant/components/nam/air_quality.py | 15 +- homeassistant/components/nam/const.py | 203 +++++++++++--------- homeassistant/components/nam/sensor.py | 15 +- 3 files changed, 133 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/nam/air_quality.py b/homeassistant/components/nam/air_quality.py index c39ad2bea73..163b50148db 100644 --- a/homeassistant/components/nam/air_quality.py +++ b/homeassistant/components/nam/air_quality.py @@ -10,7 +10,14 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NAMDataUpdateCoordinator -from .const import AIR_QUALITY_SENSORS, DEFAULT_NAME, DOMAIN, SUFFIX_P1, SUFFIX_P2 +from .const import ( + AIR_QUALITY_SENSORS, + ATTR_MHZ14A_CARBON_DIOXIDE, + DEFAULT_NAME, + DOMAIN, + SUFFIX_P1, + SUFFIX_P2, +) PARALLEL_UPDATES = 1 @@ -61,7 +68,9 @@ class NAMAirQuality(CoordinatorEntity, AirQualityEntity): @property def carbon_dioxide(self) -> StateType: """Return the particulate matter 10 level.""" - return round_state(getattr(self.coordinator.data, "conc_co2_ppm", None)) + return round_state( + getattr(self.coordinator.data, ATTR_MHZ14A_CARBON_DIOXIDE, None) + ) @property def unique_id(self) -> str: @@ -82,7 +91,7 @@ class NAMAirQuality(CoordinatorEntity, AirQualityEntity): # sensors. For this reason, we mark entities for which data is missing as # unavailable. return available and bool( - getattr(self.coordinator.data, f"{self.sensor_type}_p2", None) + getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}", None) ) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 3800057d5d7..8171914b832 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -5,6 +5,8 @@ from datetime import timedelta from typing import Final from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -19,6 +21,27 @@ from homeassistant.const import ( from .model import SensorDescription +ATTR_BME280_HUMIDITY: Final = "bme280_humidity" +ATTR_BME280_PRESSURE: Final = "bme280_pressure" +ATTR_BME280_TEMPERATURE: Final = "bme280_temperature" +ATTR_BMP280_PRESSURE: Final = "bmp280_pressure" +ATTR_BMP280_TEMPERATURE: Final = "bmp280_temperature" +ATTR_DHT22_HUMIDITY: Final = "humidity" +ATTR_DHT22_TEMPERATURE: Final = "temperature" +ATTR_HECA_HUMIDITY: Final = "heca_humidity" +ATTR_HECA_TEMPERATURE: Final = "heca_temperature" +ATTR_MHZ14A_CARBON_DIOXIDE: Final = "conc_co2_ppm" +ATTR_SHT3X_HUMIDITY: Final = "sht3x_humidity" +ATTR_SHT3X_TEMPERATURE: Final = "sht3x_temperature" +ATTR_SIGNAL_STRENGTH: Final = "signal" +ATTR_SPS30_P0: Final = "sps30_p0" +ATTR_SPS30_P4: Final = "sps30_p4" +ATTR_UPTIME: Final = "uptime" + +ATTR_ENABLED: Final = "enabled" +ATTR_LABEL: Final = "label" +ATTR_UNIT: Final = "unit" + DEFAULT_NAME: Final = "Nettigo Air Monitor" DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6) DOMAIN: Final = "nam" @@ -30,109 +53,109 @@ SUFFIX_P2: Final = "_p2" AIR_QUALITY_SENSORS: Final[dict[str, str]] = {"sds": "SDS011", "sps30": "SPS30"} SENSORS: Final[dict[str, SensorDescription]] = { - "bme280_humidity": { - "label": f"{DEFAULT_NAME} BME280 Humidity", - "unit": PERCENTAGE, - "device_class": DEVICE_CLASS_HUMIDITY, - "icon": None, - "enabled": True, + ATTR_BME280_HUMIDITY: { + ATTR_LABEL: f"{DEFAULT_NAME} BME280 Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "bme280_pressure": { - "label": f"{DEFAULT_NAME} BME280 Pressure", - "unit": PRESSURE_HPA, - "device_class": DEVICE_CLASS_PRESSURE, - "icon": None, - "enabled": True, + ATTR_BME280_PRESSURE: { + ATTR_LABEL: f"{DEFAULT_NAME} BME280 Pressure", + ATTR_UNIT: PRESSURE_HPA, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "bme280_temperature": { - "label": f"{DEFAULT_NAME} BME280 Temperature", - "unit": TEMP_CELSIUS, - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "enabled": True, + ATTR_BME280_TEMPERATURE: { + ATTR_LABEL: f"{DEFAULT_NAME} BME280 Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "bmp280_pressure": { - "label": f"{DEFAULT_NAME} BMP280 Pressure", - "unit": PRESSURE_HPA, - "device_class": DEVICE_CLASS_PRESSURE, - "icon": None, - "enabled": True, + ATTR_BMP280_PRESSURE: { + ATTR_LABEL: f"{DEFAULT_NAME} BMP280 Pressure", + ATTR_UNIT: PRESSURE_HPA, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "bmp280_temperature": { - "label": f"{DEFAULT_NAME} BMP280 Temperature", - "unit": TEMP_CELSIUS, - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "enabled": True, + ATTR_BMP280_TEMPERATURE: { + ATTR_LABEL: f"{DEFAULT_NAME} BMP280 Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "heca_humidity": { - "label": f"{DEFAULT_NAME} HECA Humidity", - "unit": PERCENTAGE, - "device_class": DEVICE_CLASS_HUMIDITY, - "icon": None, - "enabled": True, + ATTR_HECA_HUMIDITY: { + ATTR_LABEL: f"{DEFAULT_NAME} HECA Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "heca_temperature": { - "label": f"{DEFAULT_NAME} HECA Temperature", - "unit": TEMP_CELSIUS, - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "enabled": True, + ATTR_HECA_TEMPERATURE: { + ATTR_LABEL: f"{DEFAULT_NAME} HECA Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "sht3x_humidity": { - "label": f"{DEFAULT_NAME} SHT3X Humidity", - "unit": PERCENTAGE, - "device_class": DEVICE_CLASS_HUMIDITY, - "icon": None, - "enabled": True, + ATTR_SHT3X_HUMIDITY: { + ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "sht3x_temperature": { - "label": f"{DEFAULT_NAME} SHT3X Temperature", - "unit": TEMP_CELSIUS, - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "enabled": True, + ATTR_SHT3X_TEMPERATURE: { + ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "sps30_p0": { - "label": f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - "device_class": None, - "icon": "mdi:blur", - "enabled": True, + ATTR_SPS30_P0: { + ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, }, - "sps30_p4": { - "label": f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - "device_class": None, - "icon": "mdi:blur", - "enabled": True, + ATTR_SPS30_P4: { + ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, }, - "humidity": { - "label": f"{DEFAULT_NAME} DHT22 Humidity", - "unit": PERCENTAGE, - "device_class": DEVICE_CLASS_HUMIDITY, - "icon": None, - "enabled": True, + ATTR_DHT22_HUMIDITY: { + ATTR_LABEL: f"{DEFAULT_NAME} DHT22 Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "signal": { - "label": f"{DEFAULT_NAME} Signal Strength", - "unit": SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - "device_class": DEVICE_CLASS_SIGNAL_STRENGTH, - "icon": None, - "enabled": False, + ATTR_DHT22_TEMPERATURE: { + ATTR_LABEL: f"{DEFAULT_NAME} DHT22 Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_ENABLED: True, }, - "temperature": { - "label": f"{DEFAULT_NAME} DHT22 Temperature", - "unit": TEMP_CELSIUS, - "device_class": DEVICE_CLASS_TEMPERATURE, - "icon": None, - "enabled": True, + ATTR_SIGNAL_STRENGTH: { + ATTR_LABEL: f"{DEFAULT_NAME} Signal Strength", + ATTR_UNIT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH, + ATTR_ICON: None, + ATTR_ENABLED: False, }, - "uptime": { - "label": f"{DEFAULT_NAME} Uptime", - "unit": None, - "device_class": DEVICE_CLASS_TIMESTAMP, - "icon": None, - "enabled": False, + ATTR_UPTIME: { + ATTR_LABEL: f"{DEFAULT_NAME} Uptime", + ATTR_UNIT: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_ICON: None, + ATTR_ENABLED: False, }, } diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 026e40483bd..2774d87f2d3 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -6,6 +6,7 @@ from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -13,7 +14,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow from . import NAMDataUpdateCoordinator -from .const import DOMAIN, SENSORS +from .const import ATTR_ENABLED, ATTR_LABEL, ATTR_UNIT, ATTR_UPTIME, DOMAIN, SENSORS PARALLEL_UPDATES = 1 @@ -27,7 +28,7 @@ async def async_setup_entry( sensors: list[NAMSensor | NAMSensorUptime] = [] for sensor in SENSORS: if sensor in coordinator.data: - if sensor == "uptime": + if sensor == ATTR_UPTIME: sensors.append(NAMSensorUptime(coordinator, sensor)) else: sensors.append(NAMSensor(coordinator, sensor)) @@ -49,7 +50,7 @@ class NAMSensor(CoordinatorEntity, SensorEntity): @property def name(self) -> str: """Return the name.""" - return self._description["label"] + return self._description[ATTR_LABEL] @property def state(self) -> Any: @@ -59,22 +60,22 @@ class NAMSensor(CoordinatorEntity, SensorEntity): @property def unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" - return self._description["unit"] + return self._description[ATTR_UNIT] @property def device_class(self) -> str | None: """Return the class of this sensor.""" - return self._description["device_class"] + return self._description[ATTR_DEVICE_CLASS] @property def icon(self) -> str | None: """Return the icon.""" - return self._description["icon"] + return self._description[ATTR_ICON] @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return self._description["enabled"] + return self._description[ATTR_ENABLED] @property def unique_id(self) -> str: From e16a8063a57c6c1575ea8f52ccb4d368602c7e63 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 20 May 2021 13:05:15 +0200 Subject: [PATCH 595/852] Compile statistics for energy sensors (#50829) * Compile statistics for energy sensors * Update tests * Rename abs_value to state * Tweak * Recreate statistics table * Pylint * Try to fix test * Fix statistics for multiple energy sensors * Fix energy statistics when last_reset is not set --- homeassistant/components/recorder/__init__.py | 1 - .../components/recorder/migration.py | 7 +- homeassistant/components/recorder/models.py | 6 +- .../components/recorder/statistics.py | 38 ++- homeassistant/components/sensor/recorder.py | 56 +++- tests/components/history/conftest.py | 20 -- tests/components/recorder/conftest.py | 49 +-- tests/components/recorder/test_init.py | 2 + tests/components/recorder/test_statistics.py | 3 + tests/components/recorder/test_util.py | 23 +- tests/components/sensor/test_recorder.py | 307 ++++++++++++++++-- tests/conftest.py | 37 ++- 12 files changed, 437 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index e2a5f6b9a6d..91f29225cd2 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -63,7 +63,6 @@ from .util import ( _LOGGER = logging.getLogger(__name__) SERVICE_PURGE = "purge" -SERVICE_STATISTICS = "statistics" SERVICE_ENABLE = "enable" SERVICE_DISABLE = "disable" diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 17b6e277614..51d0f0a86fd 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -11,7 +11,7 @@ from sqlalchemy.exc import ( ) from sqlalchemy.schema import AddConstraint, DropConstraint -from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges +from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges, Statistics from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -415,6 +415,11 @@ def _apply_update(engine, session, new_version, old_version): ) elif new_version == 14: _modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"]) + elif new_version == 15: + if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): + # Recreate the statistics table + Statistics.__table__.drop(engine) + Statistics.__table__.create(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index af22239713b..924f59790b0 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -28,7 +28,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 14 +SCHEMA_VERSION = 15 _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,6 @@ TABLE_EVENTS = "events" TABLE_STATES = "states" TABLE_RECORDER_RUNS = "recorder_runs" TABLE_SCHEMA_CHANGES = "schema_changes" - TABLE_STATISTICS = "statistics" ALL_TABLES = [ @@ -223,6 +222,9 @@ class Statistics(Base): # type: ignore mean = Column(Float()) min = Column(Float()) max = Column(Float()) + last_reset = Column(DATETIME_TYPE) + state = Column(Float()) + sum = Column(Float()) __table_args__ = ( # Used for fetching statistics for a certain entity at a specific time diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 65bed4423c5..ee733039d43 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -25,6 +25,9 @@ QUERY_STATISTICS = [ Statistics.mean, Statistics.min, Statistics.max, + Statistics.last_reset, + Statistics.state, + Statistics.sum, ] STATISTICS_BAKERY = "recorder_statistics_bakery" @@ -97,16 +100,38 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_id=None) statistic_ids = [statistic_id] if statistic_id is not None else None - return _sorted_statistics_to_dict( - hass, session, stats, start_time, statistic_ids + return _sorted_statistics_to_dict(stats, statistic_ids) + + +def get_last_statistics(hass, number_of_stats, statistic_id=None): + """Return the last number_of_stats statistics.""" + with session_scope(hass=hass) as session: + baked_query = hass.data[STATISTICS_BAKERY]( + lambda session: session.query(*QUERY_STATISTICS) ) + if statistic_id is not None: + baked_query += lambda q: q.filter_by(statistic_id=bindparam("statistic_id")) + + baked_query += lambda q: q.order_by( + Statistics.statistic_id, Statistics.start.desc() + ) + + baked_query += lambda q: q.limit(bindparam("number_of_stats")) + + stats = execute( + baked_query(session).params( + number_of_stats=number_of_stats, statistic_id=statistic_id + ) + ) + + statistic_ids = [statistic_id] if statistic_id is not None else None + + return _sorted_statistics_to_dict(stats, statistic_ids) + def _sorted_statistics_to_dict( - hass, - session, stats, - start_time, statistic_ids, ): """Convert SQL results into JSON friendly data structure.""" @@ -130,6 +155,9 @@ def _sorted_statistics_to_dict( "mean": db_state.mean, "min": db_state.min, "max": db_state.max, + "last_reset": _process_timestamp_to_utc_isoformat(db_state.last_reset), + "state": db_state.state, + "sum": db_state.sum, } for db_state in group ) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index f79b830a7d7..34e373dae50 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -2,16 +2,18 @@ from __future__ import annotations import datetime -import statistics +import itertools +from statistics import fmean -from homeassistant.components.recorder import history +from homeassistant.components.recorder import history, statistics from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ATTR_DEVICE_CLASS from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util from . import DOMAIN -DEVICE_CLASS_STATISTICS = {"temperature": {"mean", "min", "max"}} +DEVICE_CLASS_STATISTICS = {"temperature": {"mean", "min", "max"}, "energy": {"sum"}} def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]: @@ -50,7 +52,7 @@ def compile_statistics( # Get history between start and end history_list = history.get_significant_states( # type: ignore - hass, start, end, [i[0] for i in entities] + hass, start - datetime.timedelta.resolution, end, [i[0] for i in entities] ) for entity_id, device_class in entities: @@ -60,7 +62,9 @@ def compile_statistics( continue entity_history = history_list[entity_id] - fstates = [float(el.state) for el in entity_history if _is_number(el.state)] + fstates = [ + (float(el.state), el) for el in entity_history if _is_number(el.state) + ] if not fstates: continue @@ -69,13 +73,49 @@ def compile_statistics( # Make calculations if "max" in wanted_statistics: - result[entity_id]["max"] = max(fstates) + result[entity_id]["max"] = max(*itertools.islice(zip(*fstates), 1)) if "min" in wanted_statistics: - result[entity_id]["min"] = min(fstates) + result[entity_id]["min"] = min(*itertools.islice(zip(*fstates), 1)) # Note: The average calculation will be incorrect for unevenly spaced readings, # this needs to be improved by weighting with time between measurements if "mean" in wanted_statistics: - result[entity_id]["mean"] = statistics.fmean(fstates) + result[entity_id]["mean"] = fmean(*itertools.islice(zip(*fstates), 1)) + + if "sum" in wanted_statistics: + last_reset = old_last_reset = None + new_state = old_state = None + _sum = 0 + last_stats = statistics.get_last_statistics(hass, 1, entity_id) # type: ignore + if entity_id in last_stats: + # We have compiled history for this sensor before, use that as a starting point + last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"] + new_state = old_state = last_stats[entity_id][0]["state"] + _sum = last_stats[entity_id][0]["sum"] + + for fstate, state in fstates: + if "last_reset" not in state.attributes: + continue + if (last_reset := state.attributes["last_reset"]) != old_last_reset: + # The sensor has been reset, update the sum + if old_state is not None: + _sum += new_state - old_state + # ..and update the starting point + new_state = fstate + old_last_reset = last_reset + old_state = new_state + else: + new_state = fstate + + if last_reset is None or new_state is None or old_state is None: + # No valid updates + result.pop(entity_id) + continue + + # Update the sum with the last state + _sum += new_state - old_state + result[entity_id]["last_reset"] = dt_util.parse_datetime(last_reset) + result[entity_id]["sum"] = _sum + result[entity_id]["state"] = new_state return result diff --git a/tests/components/history/conftest.py b/tests/components/history/conftest.py index 35829ece721..5e81b444393 100644 --- a/tests/components/history/conftest.py +++ b/tests/components/history/conftest.py @@ -2,28 +2,8 @@ import pytest from homeassistant.components import history -from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, init_recorder_component - - -@pytest.fixture -def hass_recorder(): - """Home Assistant fixture with in-memory recorder.""" - hass = get_test_home_assistant() - - def setup_recorder(config=None): - """Set up with params.""" - init_recorder_component(hass, config) - hass.start() - hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() - return hass - - yield setup_recorder - hass.stop() - @pytest.fixture def hass_history(hass_recorder): diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index 6b8c61d4d7d..2a29513a88e 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -3,9 +3,11 @@ from __future__ import annotations from collections.abc import AsyncGenerator from typing import Awaitable, Callable, cast +from unittest.mock import patch import pytest +from homeassistant.components import recorder from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.core import HomeAssistant @@ -13,47 +15,32 @@ from homeassistant.helpers.typing import ConfigType from .common import async_recorder_block_till_done -from tests.common import ( - async_init_recorder_component, - get_test_home_assistant, - init_recorder_component, -) +from tests.common import async_init_recorder_component SetupRecorderInstanceT = Callable[..., Awaitable[Recorder]] @pytest.fixture -def hass_recorder(): - """Home Assistant fixture with in-memory recorder.""" - hass = get_test_home_assistant() - - def setup_recorder(config=None): - """Set up with params.""" - init_recorder_component(hass, config) - hass.start() - hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() - return hass - - yield setup_recorder - hass.stop() - - -@pytest.fixture -async def async_setup_recorder_instance() -> AsyncGenerator[ - SetupRecorderInstanceT, None -]: +async def async_setup_recorder_instance( + enable_statistics, +) -> AsyncGenerator[SetupRecorderInstanceT, None]: """Yield callable to setup recorder instance.""" async def async_setup_recorder( hass: HomeAssistant, config: ConfigType | None = None ) -> Recorder: """Setup and return recorder instance.""" # noqa: D401 - await async_init_recorder_component(hass, config) - await hass.async_block_till_done() - instance = cast(Recorder, hass.data[DATA_INSTANCE]) - await async_recorder_block_till_done(hass, instance) - assert isinstance(instance, Recorder) - return instance + stats = recorder.Recorder.async_hourly_statistics if enable_statistics else None + with patch( + "homeassistant.components.recorder.Recorder.async_hourly_statistics", + side_effect=stats, + autospec=True, + ): + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + instance = cast(Recorder, hass.data[DATA_INSTANCE]) + await async_recorder_block_till_done(hass, instance) + assert isinstance(instance, Recorder) + return instance yield async_setup_recorder diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index bb334599c26..5d4620ef29c 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import sqlite3 from unittest.mock import patch +import pytest from sqlalchemy.exc import DatabaseError, OperationalError, SQLAlchemyError from homeassistant.components import recorder @@ -682,6 +683,7 @@ def test_auto_purge_disabled(hass_recorder): dt_util.set_default_time_zone(original_tz) +@pytest.mark.parametrize("enable_statistics", [True]) def test_auto_statistics(hass_recorder): """Test periodic statistics scheduling.""" hass = hass_recorder() diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index ee05cb993b9..74be1075626 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -33,6 +33,9 @@ def test_compile_hourly_statistics(hass_recorder): "mean": 15.0, "min": 10.0, "max": 20.0, + "last_reset": None, + "state": None, + "sum": None, } ] } diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 5ba206a751f..5b4b234fbbb 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -15,28 +15,7 @@ from homeassistant.util import dt as dt_util from .common import corrupt_db_file -from tests.common import ( - async_init_recorder_component, - get_test_home_assistant, - init_recorder_component, -) - - -@pytest.fixture -def hass_recorder(): - """Home Assistant fixture with in-memory recorder.""" - hass = get_test_home_assistant() - - def setup_recorder(config=None): - """Set up with params.""" - init_recorder_component(hass, config) - hass.start() - hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() - return hass - - yield setup_recorder - hass.stop() +from tests.common import async_init_recorder_component def test_session_scope_not_setup(hass_recorder): diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index a391161ee1e..5d86ac520a5 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -3,8 +3,6 @@ from datetime import timedelta from unittest.mock import patch, sentinel -import pytest - from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat @@ -13,27 +11,9 @@ from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant, init_recorder_component from tests.components.recorder.common import wait_recording_done -@pytest.fixture -def hass_recorder(): - """Home Assistant fixture with in-memory recorder.""" - hass = get_test_home_assistant() - - def setup_recorder(config=None): - """Set up with params.""" - init_recorder_component(hass, config) - hass.start() - hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() - return hass - - yield setup_recorder - hass.stop() - - def test_compile_hourly_statistics(hass_recorder): """Test compiling hourly statistics.""" hass = hass_recorder() @@ -54,11 +34,198 @@ def test_compile_hourly_statistics(hass_recorder): "mean": 15.0, "min": 10.0, "max": 20.0, + "last_reset": None, + "state": None, + "sum": None, } ] } +def test_compile_hourly_energy_statistics(hass_recorder): + """Test compiling hourly statistics.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + sns1_attr = {"device_class": "energy", "state_class": "measurement"} + sns2_attr = {"device_class": "energy"} + sns3_attr = {} + + zero, four, eight, states = record_energy_states( + hass, sns1_attr, sns2_attr, sns3_attr + ) + hist = history.get_significant_states( + hass, zero - timedelta.resolution, eight + timedelta.resolution + ) + assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1)) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(zero), + "state": 20.0, + "sum": 10.0, + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": 40.0, + "sum": 10.0, + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": 70.0, + "sum": 40.0, + }, + ] + } + + +def test_compile_hourly_energy_statistics2(hass_recorder): + """Test compiling hourly statistics.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + sns1_attr = {"device_class": "energy", "state_class": "measurement"} + sns2_attr = {"device_class": "energy", "state_class": "measurement"} + sns3_attr = {"device_class": "energy", "state_class": "measurement"} + + zero, four, eight, states = record_energy_states( + hass, sns1_attr, sns2_attr, sns3_attr + ) + hist = history.get_significant_states( + hass, zero - timedelta.resolution, eight + timedelta.resolution + ) + assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1)) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(zero), + "state": 20.0, + "sum": 10.0, + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": 40.0, + "sum": 10.0, + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": 70.0, + "sum": 40.0, + }, + ], + "sensor.test2": [ + { + "statistic_id": "sensor.test2", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(zero), + "state": 130.0, + "sum": 20.0, + }, + { + "statistic_id": "sensor.test2", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": 45.0, + "sum": -95.0, + }, + { + "statistic_id": "sensor.test2", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": 75.0, + "sum": -65.0, + }, + ], + "sensor.test3": [ + { + "statistic_id": "sensor.test3", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(zero), + "state": 5.0, + "sum": 5.0, + }, + { + "statistic_id": "sensor.test3", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": 50.0, + "sum": 30.0, + }, + { + "statistic_id": "sensor.test3", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": 90.0, + "sum": 70.0, + }, + ], + } + + def test_compile_hourly_statistics_unchanged(hass_recorder): """Test compiling hourly statistics, with no changes during the hour.""" hass = hass_recorder() @@ -79,6 +246,9 @@ def test_compile_hourly_statistics_unchanged(hass_recorder): "mean": 20.0, "min": 20.0, "max": 20.0, + "last_reset": None, + "state": None, + "sum": None, } ] } @@ -104,6 +274,9 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder): "mean": 17.5, "min": 10.0, "max": 25.0, + "last_reset": None, + "state": None, + "sum": None, } ] } @@ -127,7 +300,7 @@ def test_compile_hourly_statistics_unavailable(hass_recorder): def record_states(hass): """Record some test states. - We inject a bunch of state updates temperature sensors. + We inject a bunch of state updates for temperature sensors. """ mp = "media_player.test" sns1 = "sensor.test1" @@ -174,6 +347,98 @@ def record_states(hass): return zero, four, states +def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr): + """Record some test states. + + We inject a bunch of state updates for energy sensors. + """ + sns1 = "sensor.test1" + sns2 = "sensor.test2" + sns3 = "sensor.test3" + sns4 = "sensor.test4" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(minutes=15) + two = one + timedelta(minutes=30) + three = two + timedelta(minutes=15) + four = three + timedelta(minutes=15) + five = four + timedelta(minutes=30) + six = five + timedelta(minutes=15) + seven = six + timedelta(minutes=15) + eight = seven + timedelta(minutes=30) + + sns1_attr = {**_sns1_attr, "last_reset": zero.isoformat()} + sns2_attr = {**_sns2_attr, "last_reset": zero.isoformat()} + sns3_attr = {**_sns3_attr, "last_reset": zero.isoformat()} + sns4_attr = {**_sns3_attr} + + states = {sns1: [], sns2: [], sns3: [], sns4: []} + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=zero): + states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) # Sum 0 + states[sns2].append(set_state(sns2, "110", attributes=sns2_attr)) # Sum 0 + states[sns3].append(set_state(sns3, "0", attributes=sns3_attr)) # Sum 0 + states[sns4].append(set_state(sns4, "0", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) # Sum 5 + states[sns2].append(set_state(sns2, "120", attributes=sns2_attr)) # Sum 10 + states[sns3].append(set_state(sns3, "0", attributes=sns3_attr)) # Sum 0 + states[sns4].append(set_state(sns4, "0", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) # Sum 10 + states[sns2].append(set_state(sns2, "130", attributes=sns2_attr)) # Sum 20 + states[sns3].append(set_state(sns3, "5", attributes=sns3_attr)) # Sum 5 + states[sns4].append(set_state(sns4, "5", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) # Sum 0 + states[sns2].append(set_state(sns2, "0", attributes=sns2_attr)) # Sum -110 + states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) # Sum 10 + states[sns4].append(set_state(sns4, "10", attributes=sns4_attr)) # - + + sns1_attr = {**_sns1_attr, "last_reset": four.isoformat()} + sns2_attr = {**_sns2_attr, "last_reset": four.isoformat()} + sns3_attr = {**_sns3_attr, "last_reset": four.isoformat()} + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=four): + states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) # Sum 0 + states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) # Sum -110 + states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) # Sum 10 + states[sns4].append(set_state(sns4, "30", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=five): + states[sns1].append(set_state(sns1, "40", attributes=sns1_attr)) # Sum 10 + states[sns2].append(set_state(sns2, "45", attributes=sns2_attr)) # Sum -95 + states[sns3].append(set_state(sns3, "50", attributes=sns3_attr)) # Sum 30 + states[sns4].append(set_state(sns4, "50", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=six): + states[sns1].append(set_state(sns1, "50", attributes=sns1_attr)) # Sum 20 + states[sns2].append(set_state(sns2, "55", attributes=sns2_attr)) # Sum -85 + states[sns3].append(set_state(sns3, "60", attributes=sns3_attr)) # Sum 40 + states[sns4].append(set_state(sns4, "60", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=seven): + states[sns1].append(set_state(sns1, "60", attributes=sns1_attr)) # Sum 30 + states[sns2].append(set_state(sns2, "65", attributes=sns2_attr)) # Sum -75 + states[sns3].append(set_state(sns3, "80", attributes=sns3_attr)) # Sum 60 + states[sns4].append(set_state(sns4, "80", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=eight): + states[sns1].append(set_state(sns1, "70", attributes=sns1_attr)) # Sum 40 + states[sns2].append(set_state(sns2, "75", attributes=sns2_attr)) # Sum -65 + states[sns3].append(set_state(sns3, "90", attributes=sns3_attr)) # Sum 70 + + return zero, four, eight, states + + def record_states_partially_unavailable(hass): """Record some test states. diff --git a/tests/conftest.py b/tests/conftest.py index 2a453a8dad1..1f5ffc80d0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ from homeassistant import core as ha, loader, runner, util from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant, legacy_api_password -from homeassistant.components import mqtt +from homeassistant.components import mqtt, recorder from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, @@ -39,6 +39,8 @@ from tests.common import ( # noqa: E402, isort:skip MockUser, async_fire_mqtt_message, async_test_home_assistant, + get_test_home_assistant, + init_recorder_component, mock_storage as mock_storage, ) from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:skip @@ -595,3 +597,36 @@ def legacy_patchable_time(): def enable_custom_integrations(hass): """Enable custom integrations defined in the test dir.""" hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) + + +@pytest.fixture +def enable_statistics(): + """Fixture to control enabling of recorder's statistics compilation. + + To enable statistics, tests can be marked with: + @pytest.mark.parametrize("enable_statistics", [True]) + """ + return False + + +@pytest.fixture +def hass_recorder(enable_statistics): + """Home Assistant fixture with in-memory recorder.""" + hass = get_test_home_assistant() + stats = recorder.Recorder.async_hourly_statistics if enable_statistics else None + with patch( + "homeassistant.components.recorder.Recorder.async_hourly_statistics", + side_effect=stats, + autospec=True, + ): + + def setup_recorder(config=None): + """Set up with params.""" + init_recorder_component(hass, config) + hass.start() + hass.block_till_done() + hass.data[recorder.DATA_INSTANCE].block_till_done() + return hass + + yield setup_recorder + hass.stop() From f3db819548b801940e1fe32a552d4566267a5833 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 20 May 2021 15:03:27 +0300 Subject: [PATCH 596/852] Add play_media channel support to LG Netcast (#49527) --- .../components/lg_netcast/media_player.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index f3e7e44fe5a..31316ca975b 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -46,6 +47,7 @@ SUPPORT_LGTV = ( | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -85,6 +87,7 @@ class LgTVDevice(MediaPlayerEntity): # Assume that the TV is in Play mode self._playing = True self._volume = 0 + self._channel_id = None self._channel_name = "" self._program_name = "" self._state = None @@ -116,8 +119,11 @@ class LgTVDevice(MediaPlayerEntity): channel_info = client.query_data("cur_channel") if channel_info: channel_info = channel_info[0] + channel_id = channel_info.find("major") self._channel_name = channel_info.find("chname").text self._program_name = channel_info.find("progName").text + if channel_id is not None: + self._channel_id = int(channel_id.text) if self._channel_name is None: self._channel_name = channel_info.find("inputSourceName").text if self._program_name is None: @@ -172,6 +178,11 @@ class LgTVDevice(MediaPlayerEntity): """List of available input sources.""" return self._source_names + @property + def media_content_id(self): + """Content id of current playing media.""" + return self._channel_id + @property def media_content_type(self): """Content type of current playing media.""" @@ -252,3 +263,16 @@ class LgTVDevice(MediaPlayerEntity): def media_previous_track(self): """Send the previous track command.""" self.send_command(37) + + def play_media(self, media_type, media_id, **kwargs): + """Tune to channel.""" + if media_type != MEDIA_TYPE_CHANNEL: + raise ValueError(f"Invalid media type: {media_type}") + + for name, channel in self._sources.items(): + channel_id = channel.find("major") + if channel_id is not None and int(channel_id.text) == int(media_id): + self.select_source(name) + return + + raise ValueError(f"Invalid media id: {media_id}") From e06a2a53c47bf2ac1d79837a27fd3add8ac77a8d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 20 May 2021 14:06:44 +0200 Subject: [PATCH 597/852] Add constructor return type in integrations L-N (#50888) * Add constructor return type in integrations L-N * Small fix --- homeassistant/components/light/__init__.py | 2 +- homeassistant/components/lovelace/resources.py | 2 +- homeassistant/components/lyric/api.py | 2 +- homeassistant/components/media_player/__init__.py | 2 +- homeassistant/components/media_source/local_source.py | 4 ++-- homeassistant/components/media_source/models.py | 4 ++-- homeassistant/components/melcloud/__init__.py | 2 +- homeassistant/components/melcloud/climate.py | 2 +- homeassistant/components/meteo_france/config_flow.py | 2 +- homeassistant/components/meteo_france/sensor.py | 4 ++-- homeassistant/components/meteo_france/weather.py | 2 +- homeassistant/components/minio/__init__.py | 2 +- homeassistant/components/minio/minio_helper.py | 2 +- homeassistant/components/mobile_app/entity.py | 2 +- homeassistant/components/modbus/climate.py | 2 +- homeassistant/components/modbus/cover.py | 2 +- homeassistant/components/modbus/switch.py | 2 +- homeassistant/components/mysensors/device.py | 2 +- homeassistant/components/neato/__init__.py | 2 +- homeassistant/components/neato/api.py | 2 +- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/nest/api.py | 2 +- homeassistant/components/nest/camera_sdm.py | 2 +- homeassistant/components/nest/climate_sdm.py | 2 +- homeassistant/components/nest/device_info.py | 2 +- homeassistant/components/nest/sensor_sdm.py | 2 +- homeassistant/components/netatmo/api.py | 2 +- homeassistant/components/netatmo/config_flow.py | 2 +- homeassistant/components/netatmo/data_handler.py | 2 +- homeassistant/components/netatmo/light.py | 2 +- homeassistant/components/netatmo/media_source.py | 2 +- homeassistant/components/nightscout/config_flow.py | 2 +- homeassistant/components/nilu/air_quality.py | 2 +- homeassistant/components/notion/__init__.py | 2 +- homeassistant/components/notion/sensor.py | 2 +- homeassistant/components/nsw_fuel_station/sensor.py | 2 +- homeassistant/components/nut/config_flow.py | 2 +- homeassistant/components/nws/__init__.py | 2 +- homeassistant/components/nzbget/coordinator.py | 2 +- homeassistant/components/nzbget/sensor.py | 2 +- homeassistant/components/nzbget/switch.py | 2 +- 41 files changed, 44 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index caf3ac209cb..8cb35a8bffe 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -488,7 +488,7 @@ class Profile: class Profiles: """Representation of available color profiles.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize profiles.""" self.hass = hass self.data: dict[str, Profile] = {} diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 6a97d5c4192..2a098361962 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -52,7 +52,7 @@ class ResourceStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(RESOURCE_CREATE_FIELDS) UPDATE_SCHEMA = vol.Schema(RESOURCE_UPDATE_FIELDS) - def __init__(self, hass: HomeAssistant, ll_config: LovelaceConfig): + def __init__(self, hass: HomeAssistant, ll_config: LovelaceConfig) -> None: """Initialize the storage collection.""" super().__init__( storage.Store(hass, RESOURCES_STORAGE_VERSION, RESOURCE_STORAGE_KEY), diff --git a/homeassistant/components/lyric/api.py b/homeassistant/components/lyric/api.py index a77c6365baf..4d955165174 100644 --- a/homeassistant/components/lyric/api.py +++ b/homeassistant/components/lyric/api.py @@ -18,7 +18,7 @@ class ConfigEntryLyricClient(LyricClient): self, websession: ClientSession, oauth_session: config_entry_oauth2_flow.OAuth2Session, - ): + ) -> None: """Initialize Honeywell Lyric auth.""" super().__init__(websession) self._oauth_session = oauth_session diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 23261ea029e..6fca2a4c3d5 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1161,7 +1161,7 @@ class BrowseMedia: children: list[BrowseMedia] | None = None, children_media_class: str | None = None, thumbnail: str | None = None, - ): + ) -> None: """Initialize browse media item.""" self.media_class = media_class self.media_content_id = media_content_id diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index fb5e9094dfb..ab2e18183de 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -30,7 +30,7 @@ class LocalSource(MediaSource): name: str = "Local Media" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize local source.""" super().__init__(DOMAIN) self.hass = hass @@ -183,7 +183,7 @@ class LocalMediaView(HomeAssistantView): url = "/media/{source_dir_id}/{location:.*}" name = "media" - def __init__(self, hass: HomeAssistant, source: LocalSource): + def __init__(self, hass: HomeAssistant, source: LocalSource) -> None: """Initialize the media view.""" self.hass = hass self.source = source diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index aa17fff320e..247361296a1 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -29,7 +29,7 @@ class BrowseMediaSource(BrowseMedia): children: list[BrowseMediaSource] | None - def __init__(self, *, domain: str | None, identifier: str | None, **kwargs): + def __init__(self, *, domain: str | None, identifier: str | None, **kwargs) -> None: """Initialize media source browse media.""" media_content_id = f"{URI_SCHEME}{domain or ''}" if identifier: @@ -106,7 +106,7 @@ class MediaSource(ABC): name: str = None - def __init__(self, domain: str): + def __init__(self, domain: str) -> None: """Initialize a media source.""" self.domain = domain if not self.name: diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 2380d0ea8d7..7b42c1f42e8 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -84,7 +84,7 @@ async def async_unload_entry(hass, config_entry): class MelCloudDevice: """MELCloud Device instance.""" - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: """Construct a device wrapper.""" self.device = device self.name = device.name diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 49cb0fe462e..42c5ed7ef85 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -100,7 +100,7 @@ async def async_setup_entry( class MelCloudClimate(ClimateEntity): """Base climate device.""" - def __init__(self, device: MelCloudDevice): + def __init__(self, device: MelCloudDevice) -> None: """Initialize the climate.""" self.api = device self._base_device = self.api.device diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py index baaf8e8b99c..26e2ac1bda2 100644 --- a/homeassistant/components/meteo_france/config_flow.py +++ b/homeassistant/components/meteo_france/config_flow.py @@ -113,7 +113,7 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class MeteoFranceOptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 802305667fc..b710686554f 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -78,7 +78,7 @@ async def async_setup_entry( class MeteoFranceSensor(CoordinatorEntity, SensorEntity): """Representation of a Meteo-France sensor.""" - def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator): + def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator) -> None: """Initialize the Meteo-France sensor.""" super().__init__(coordinator) self._type = sensor_type @@ -194,7 +194,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor): class MeteoFranceAlertSensor(MeteoFranceSensor): """Representation of a Meteo-France alert sensor.""" - def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator): + def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator) -> None: """Initialize the Meteo-France sensor.""" super().__init__(sensor_type, coordinator) dept_code = self.coordinator.data.domain_id diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index f45893ed7ca..d084198bd5b 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -68,7 +68,7 @@ async def async_setup_entry( class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" - def __init__(self, coordinator: DataUpdateCoordinator, mode: str): + def __init__(self, coordinator: DataUpdateCoordinator, mode: str) -> None: """Initialise the platform with a data instance and station name.""" super().__init__(coordinator) self._city_name = self.coordinator.data.position["name"] diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index 6e7174b60ee..f33dd6c389c 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -232,7 +232,7 @@ class MinioListener: prefix: str, suffix: str, events: list[str], - ): + ) -> None: """Create Listener.""" self._queue = queue self._endpoint = endpoint diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index c77e41727a4..b7fb3157c71 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -89,7 +89,7 @@ class MinioEventThread(threading.Thread): prefix: str, suffix: str, events: list[str], - ): + ) -> None: """Copy over all Minio client options.""" super().__init__() self._queue = queue diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 46f4589fa2c..d0de89a94a1 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -26,7 +26,7 @@ def unique_id(webhook_id, sensor_unique_id): class MobileAppEntity(RestoreEntity): """Representation of an mobile app entity.""" - def __init__(self, config: dict, device: DeviceEntry, entry: ConfigEntry): + def __init__(self, config: dict, device: DeviceEntry, entry: ConfigEntry) -> None: """Initialize the entity.""" self._config = config self._device = device diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index e871a21a8ed..a8a3f2a8557 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -106,7 +106,7 @@ class ModbusThermostat(ClimateEntity): self, hub: ModbusHub, config: dict[str, Any], - ): + ) -> None: """Initialize the modbus thermostat.""" self._hub: ModbusHub = hub self._name = config[CONF_NAME] diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 7c0e9d33215..e04d4bd8ce3 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -74,7 +74,7 @@ class ModbusCover(CoverEntity, RestoreEntity): self, hub: ModbusHub, config: dict[str, Any], - ): + ) -> None: """Initialize the modbus cover.""" self._hub: ModbusHub = hub self._coil = config.get(CALL_TYPE_COIL) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 27e67bf2a3e..1b0cd37eb87 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -52,7 +52,7 @@ async def async_setup_platform( class ModbusSwitch(SwitchEntity, RestoreEntity): """Base class representing a Modbus switch.""" - def __init__(self, hub: ModbusHub, config: dict): + def __init__(self, hub: ModbusHub, config: dict) -> None: """Initialize the switch.""" self._hub: ModbusHub = hub self._name = config[CONF_NAME] diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index b2e03dd037f..c1d8c431bc0 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -43,7 +43,7 @@ class MySensorsDevice: node_id: int, child_id: int, value_type: int, - ): + ) -> None: """Set up the MySensors device.""" self.gateway_id: GatewayId = gateway_id self.gateway: BaseAsyncGateway = gateway diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index b009e876a7b..f61db94332b 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -108,7 +108,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: class NeatoHub: """A My Neato hub wrapper class.""" - def __init__(self, hass: HomeAssistant, neato: Account): + def __init__(self, hass: HomeAssistant, neato: Account) -> None: """Initialize the Neato hub.""" self._hass = hass self.my_neato: Account = neato diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py index 31988fc175e..a22b1b48e74 100644 --- a/homeassistant/components/neato/api.py +++ b/homeassistant/components/neato/api.py @@ -15,7 +15,7 @@ class ConfigEntryAuth(pybotvac.OAuthSession): hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, - ): + ) -> None: """Initialize Neato Botvac Auth.""" self.hass = hass self.session = config_entry_oauth2_flow.OAuth2Session( diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index d58ad4863ed..5cd84effbc8 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -106,7 +106,7 @@ async def async_setup(hass: HomeAssistant, config: dict): class SignalUpdateCallback: """An EventCallback invoked when new events arrive from subscriber.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize EventCallback.""" self._hass = hass diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 3b571354c0f..29f39f5aec3 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -22,7 +22,7 @@ class AsyncConfigEntryAuth(AbstractAuth): oauth_session: config_entry_oauth2_flow.OAuth2Session, client_id: str, client_secret: str, - ): + ) -> None: """Initialize Google Nest Device Access auth.""" super().__init__(websession, API_URL) self._oauth_session = oauth_session diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 66568907aa0..f8f2db506e2 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -56,7 +56,7 @@ async def async_setup_sdm_entry( class NestCamera(Camera): """Devices that support cameras.""" - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: """Initialize the camera.""" super().__init__() self._device = device diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index a90fa06ce1f..ab987ff332f 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -98,7 +98,7 @@ async def async_setup_sdm_entry( class ThermostatEntity(ClimateEntity): """A nest thermostat climate entity.""" - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" self._device = device self._device_info = DeviceInfo(device) diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 36419d0dd6b..579733de8ad 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -18,7 +18,7 @@ class DeviceInfo: device_brand = "Google Nest" - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: """Initialize the DeviceInfo.""" self._device = device diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index b70d6cd5c57..8182ef3ed95 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -56,7 +56,7 @@ async def async_setup_sdm_entry( class SensorBase(SensorEntity): """Representation of a dynamically updated Sensor.""" - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: """Initialize the sensor.""" self._device = device self._device_info = DeviceInfo(device) diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index c13a0b899c7..7a5f018396e 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -15,7 +15,7 @@ class ConfigEntryNetatmoAuth(pyatmo.auth.NetatmoOAuth2): hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, - ): + ) -> None: """Initialize Netatmo Auth.""" self.hass = hass self.session = config_entry_oauth2_flow.OAuth2Session( diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index 00899f24566..ea44339b99f 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -75,7 +75,7 @@ class NetatmoFlowHandler( class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): """Handle Netatmo options.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Netatmo options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 41e7d158c0c..b8f81257eab 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -52,7 +52,7 @@ SCAN_INTERVAL = 60 class NetatmoDataHandler: """Manages the Netatmo data handling.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize self.""" self.hass = hass self._auth = hass.data[DOMAIN][entry.entry_id][AUTH] diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 08744c462e8..47712e75a0d 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -80,7 +80,7 @@ class NetatmoLight(NetatmoBase, LightEntity): camera_id: str, camera_type: str, home_id: str, - ): + ) -> None: """Initialize a Netatmo Presence camera light.""" LightEntity.__init__(self) super().__init__(data_handler) diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index db00df5129f..061b2c57971 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -41,7 +41,7 @@ class NetatmoSource(MediaSource): name: str = MANUFACTURER - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize Netatmo source.""" super().__init__(DOMAIN) self.hass = hass diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py index c8807f3a2ab..1f3f62835bc 100644 --- a/homeassistant/components/nightscout/config_flow.py +++ b/homeassistant/components/nightscout/config_flow.py @@ -66,7 +66,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class InputValidationError(exceptions.HomeAssistantError): """Error to indicate we cannot proceed due to invalid input.""" - def __init__(self, base: str): + def __init__(self, base: str) -> None: """Initialize with error base.""" super().__init__() self.base = base diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index d6fcad3ac7e..fb5b4c75798 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -158,7 +158,7 @@ class NiluData: class NiluSensor(AirQualityEntity): """Single nilu station air sensor.""" - def __init__(self, api_data: NiluData, name: str, show_on_map: bool): + def __init__(self, api_data: NiluData, name: str, show_on_map: bool) -> None: """Initialize the sensor.""" self._api = api_data self._name = f"{name} {api_data.data.name}" diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index a6dfe7e73a9..141086bb2be 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -141,7 +141,7 @@ class NotionEntity(CoordinatorEntity): system_id: str, name: str, device_class: str, - ): + ) -> None: """Initialize the entity.""" super().__init__(coordinator) self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 8652888d955..2494ed2d2e8 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -55,7 +55,7 @@ class NotionSensor(NotionEntity, SensorEntity): name: str, device_class: str, unit: str, - ): + ) -> None: """Initialize the entity.""" super().__init__( coordinator, task_id, sensor_id, bridge_id, system_id, name, device_class diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 6c8061294e9..9522d6c430f 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -148,7 +148,7 @@ class StationPriceData: class StationPriceSensor(SensorEntity): """Implementation of a sensor that reports the fuel price for a station.""" - def __init__(self, station_data: StationPriceData, fuel_type: str): + def __init__(self, station_data: StationPriceData, fuel_type: str) -> None: """Initialize the sensor.""" self._station_data = station_data self._fuel_type = fuel_type diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index fc389da5539..0b5ad8bbc1f 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -227,7 +227,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for nut.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 386a426c1d1..47465739250 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -57,7 +57,7 @@ class NwsDataUpdateCoordinator(DataUpdateCoordinator): failed_update_interval: datetime.timedelta, update_method: Callable[[], Awaitable] | None = None, request_refresh_debouncer: debounce.Debouncer | None = None, - ): + ) -> None: """Initialize NWS coordinator.""" super().__init__( hass, diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index 57e0b9fc395..5851bb21b41 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching NZBGet data.""" - def __init__(self, hass: HomeAssistant, *, config: dict, options: dict): + def __init__(self, hass: HomeAssistant, *, config: dict, options: dict) -> None: """Initialize global NZBGet data updater.""" self.nzbget = NZBGetAPI( config[CONF_HOST], diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 49506f72976..97bced9e9c2 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -77,7 +77,7 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): sensor_type: str, sensor_name: str, unit_of_measurement: str | None = None, - ): + ) -> None: """Initialize a new NZBGet sensor.""" self._sensor_type = sensor_type self._unique_id = f"{entry_id}_{sensor_type}" diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py index 605454246eb..4e4cca34aa8 100644 --- a/homeassistant/components/nzbget/switch.py +++ b/homeassistant/components/nzbget/switch.py @@ -41,7 +41,7 @@ class NZBGetDownloadSwitch(NZBGetEntity, SwitchEntity): coordinator: NZBGetDataUpdateCoordinator, entry_id: str, entry_name: str, - ): + ) -> None: """Initialize a new NZBGet switch.""" self._unique_id = f"{entry_id}_download" From ceec87134029de425c1679db368b7dcc1764c8f1 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 20 May 2021 14:59:19 +0200 Subject: [PATCH 598/852] Make Netatmo use async pyatmo (#49717) * Split initialization from data retrival * Await class initialization * Async camera * More async * Remove stale code * Clean up * Update tests * Fix test * Improve error handling * Bump pyatmo version to 5.0.0 * Add tests * Add cloudhook test * Increase coverage * Add test with no camera devices * Add test for ApiError * Add test for timeout * Clean up * Catch pyatmo ApiError * Fix PublicData * Fix media source bug * Increase coverage for light * Test webhook with delayed start * Increase coverage * Clean up leftover data classes * Make nonprivate * Review comments * Clean up stale code * Increase cov * Clean up code * Code clean up * Revert delay * Update homeassistant/components/netatmo/climate.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/netatmo/sensor.py Co-authored-by: Martin Hjelmare * Address comment * Raise cov Co-authored-by: Martin Hjelmare --- homeassistant/components/netatmo/__init__.py | 23 +- homeassistant/components/netatmo/api.py | 34 +-- homeassistant/components/netatmo/camera.py | 126 +++----- homeassistant/components/netatmo/climate.py | 118 ++++---- .../components/netatmo/data_handler.py | 89 +++--- homeassistant/components/netatmo/light.py | 58 ++-- .../components/netatmo/manifest.json | 2 +- .../components/netatmo/media_source.py | 2 +- .../components/netatmo/netatmo_entity_base.py | 9 +- homeassistant/components/netatmo/sensor.py | 35 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/netatmo/common.py | 25 +- tests/components/netatmo/conftest.py | 91 +----- tests/components/netatmo/test_camera.py | 194 ++++++++++-- tests/components/netatmo/test_climate.py | 116 +++++-- tests/components/netatmo/test_init.py | 283 ++++++++++++++++-- tests/components/netatmo/test_light.py | 58 +++- tests/components/netatmo/test_media_source.py | 10 + tests/components/netatmo/test_sensor.py | 42 +-- tests/fixtures/netatmo/homestatus.json | 3 - 21 files changed, 846 insertions(+), 476 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 1f452f1ccd4..fa4e63a21f8 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -19,7 +19,11 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers import ( + aiohttp_client, + config_entry_oauth2_flow, + config_validation as cv, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -102,8 +106,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) hass.data[DOMAIN][entry.entry_id] = { - AUTH: api.ConfigEntryNetatmoAuth(hass, entry, implementation) + AUTH: api.AsyncConfigEntryNetatmoAuth( + aiohttp_client.async_get_clientsession(hass), session + ) } data_handler = NetatmoDataHandler(hass, entry) @@ -122,6 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): {"type": "None", "data": {"push_type": "webhook_deactivation"}}, ) webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook() async def register_webhook(event): if CONF_WEBHOOK_ID not in entry.data: @@ -175,11 +183,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): handle_event, ) - activation_timeout = async_call_later(hass, 10, unregister_webhook) + activation_timeout = async_call_later(hass, 30, unregister_webhook) - await hass.async_add_executor_job( - hass.data[DOMAIN][entry.entry_id][AUTH].addwebhook, webhook_url - ) + await hass.data[DOMAIN][entry.entry_id][AUTH].async_addwebhook(webhook_url) _LOGGER.info("Register Netatmo webhook: %s", webhook_url) except pyatmo.ApiError as err: _LOGGER.error("Error during webhook registration - %s", err) @@ -202,9 +208,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" if CONF_WEBHOOK_ID in entry.data: - await hass.async_add_executor_job( - hass.data[DOMAIN][entry.entry_id][AUTH].dropwebhook - ) + webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook() _LOGGER.info("Unregister Netatmo webhook") await hass.data[DOMAIN][entry.entry_id][DATA_HANDLER].async_cleanup() diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index 7a5f018396e..19dfdac359b 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -1,34 +1,24 @@ """API for Netatmo bound to HASS OAuth.""" -from asyncio import run_coroutine_threadsafe - +from aiohttp import ClientSession import pyatmo -from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow -class ConfigEntryNetatmoAuth(pyatmo.auth.NetatmoOAuth2): +class AsyncConfigEntryNetatmoAuth(pyatmo.auth.AbstractAsyncAuth): """Provide Netatmo authentication tied to an OAuth2 based config entry.""" def __init__( self, - hass: core.HomeAssistant, - config_entry: config_entries.ConfigEntry, - implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, ) -> None: - """Initialize Netatmo Auth.""" - self.hass = hass - self.session = config_entry_oauth2_flow.OAuth2Session( - hass, config_entry, implementation - ) - super().__init__(token=self.session.token) + """Initialize the auth.""" + super().__init__(websession) + self._oauth_session = oauth_session - def refresh_tokens( - self, - ) -> dict: - """Refresh and return new Netatmo tokens using Home Assistant OAuth2 session.""" - run_coroutine_threadsafe( - self.session.async_ensure_token_valid(), self.hass.loop - ).result() - - return self.session.token + async def async_get_access_token(self): + """Return a valid access token for Netatmo API.""" + if not self._oauth_session.valid_token: + await self._oauth_session.async_ensure_token_valid() + return self._oauth_session.token["access_token"] diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 11e674e0431..60914860d3d 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -1,8 +1,8 @@ """Support for the Netatmo cameras.""" import logging +import aiohttp import pyatmo -import requests import voluptuous as vol from homeassistant.components.camera import SUPPORT_STREAM, Camera @@ -46,58 +46,40 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.info( "Cameras are currently not supported with this authentication method" ) - return data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] await data_handler.register_data_class( CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None ) + data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME) - if CAMERA_DATA_CLASS_NAME not in data_handler.data: + if not data_class or not data_class.raw_data: raise PlatformNotReady - async def get_entities(): - """Retrieve Netatmo entities.""" + all_cameras = [] + for home in data_class.cameras.values(): + for camera in home.values(): + all_cameras.append(camera) - if not data_handler.data.get(CAMERA_DATA_CLASS_NAME): - return [] + entities = [ + NetatmoCamera( + data_handler, + camera["id"], + camera["type"], + camera["home_id"], + DEFAULT_QUALITY, + ) + for camera in all_cameras + ] - data_class = data_handler.data[CAMERA_DATA_CLASS_NAME] + for person_id, person_data in data_handler.data[ + CAMERA_DATA_CLASS_NAME + ].persons.items(): + hass.data[DOMAIN][DATA_PERSONS][person_id] = person_data.get(ATTR_PSEUDO) - entities = [] - try: - all_cameras = [] - for home in data_class.cameras.values(): - for camera in home.values(): - all_cameras.append(camera) - - for camera in all_cameras: - _LOGGER.debug("Adding camera %s %s", camera["id"], camera["name"]) - entities.append( - NetatmoCamera( - data_handler, - camera["id"], - camera["type"], - camera["home_id"], - DEFAULT_QUALITY, - ) - ) - - for person_id, person_data in data_handler.data[ - CAMERA_DATA_CLASS_NAME - ].persons.items(): - hass.data[DOMAIN][DATA_PERSONS][person_id] = person_data.get( - ATTR_PSEUDO - ) - except pyatmo.NoDevice: - _LOGGER.debug("No cameras found") - - return entities - - async_add_entities(await get_entities(), True) - - await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None) + _LOGGER.debug("Adding cameras %s", entities) + async_add_entities(entities, True) platform = entity_platform.async_get_current_platform() @@ -188,33 +170,17 @@ class NetatmoCamera(NetatmoBase, Camera): self.async_write_ha_state() return - def camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" try: - if self._localurl: - response = requests.get( - f"{self._localurl}/live/snapshot_720.jpg", timeout=10 - ) - elif self._vpnurl: - response = requests.get( - f"{self._vpnurl}/live/snapshot_720.jpg", - timeout=10, - verify=True, - ) - else: - _LOGGER.error("Welcome/Presence VPN URL is None") - (self._vpnurl, self._localurl) = self._data.camera_urls( - camera_id=self._id - ) - return None - - except requests.exceptions.RequestException as error: - _LOGGER.info("Welcome/Presence URL changed: %s", error) - self._data.update_camera_urls(camera_id=self._id) - (self._vpnurl, self._localurl) = self._data.camera_urls(camera_id=self._id) - return None - - return response.content + return await self._data.async_get_live_snapshot(camera_id=self._id) + except ( + aiohttp.ClientPayloadError, + pyatmo.exceptions.ApiError, + aiohttp.ContentTypeError, + ) as err: + _LOGGER.debug("Could not fetch live camera image (%s)", err) + return None @property def extra_state_attributes(self): @@ -255,15 +221,17 @@ class NetatmoCamera(NetatmoBase, Camera): """Return true if on.""" return self.is_streaming - def turn_off(self): + async def async_turn_off(self): """Turn off camera.""" - self._data.set_state( + await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, monitoring="off" ) - def turn_on(self): + async def async_turn_on(self): """Turn on camera.""" - self._data.set_state(home_id=self._home_id, camera_id=self._id, monitoring="on") + await self._data.async_set_state( + home_id=self._home_id, camera_id=self._id, monitoring="on" + ) async def stream_source(self): """Return the stream source.""" @@ -312,7 +280,7 @@ class NetatmoCamera(NetatmoBase, Camera): ] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" return events - def _service_set_persons_home(self, **kwargs): + async def _service_set_persons_home(self, **kwargs): """Service to change current home schedule.""" persons = kwargs.get(ATTR_PERSONS) person_ids = [] @@ -321,10 +289,12 @@ class NetatmoCamera(NetatmoBase, Camera): if data.get("pseudo") == person: person_ids.append(pid) - self._data.set_persons_home(person_ids=person_ids, home_id=self._home_id) + await self._data.async_set_persons_home( + person_ids=person_ids, home_id=self._home_id + ) _LOGGER.debug("Set %s as at home", persons) - def _service_set_person_away(self, **kwargs): + async def _service_set_person_away(self, **kwargs): """Service to mark a person as away or set the home as empty.""" person = kwargs.get(ATTR_PERSON) person_id = None @@ -333,25 +303,25 @@ class NetatmoCamera(NetatmoBase, Camera): if data.get("pseudo") == person: person_id = pid - if person_id is not None: - self._data.set_persons_away( + if person_id: + await self._data.async_set_persons_away( person_id=person_id, home_id=self._home_id, ) _LOGGER.debug("Set %s as away", person) else: - self._data.set_persons_away( + await self._data.async_set_persons_away( person_id=person_id, home_id=self._home_id, ) _LOGGER.debug("Set home as empty") - def _service_set_camera_light(self, **kwargs): + async def _service_set_camera_light(self, **kwargs): """Service to set light mode.""" mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE) _LOGGER.debug("Turn %s camera light for '%s'", mode, self._name) - self._data.set_state( + await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, floodlight=mode, diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 2dee518db59..ce1eba11b70 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -116,47 +116,39 @@ async def async_setup_entry(hass, entry, async_add_entities): ) home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME) + if not home_data or home_data.raw_data == {}: + raise PlatformNotReady + if HOMEDATA_DATA_CLASS_NAME not in data_handler.data: raise PlatformNotReady - async def get_entities(): - """Retrieve Netatmo entities.""" - entities = [] + entities = [] + for home_id in get_all_home_ids(home_data): + for room_id in home_data.rooms[home_id]: + signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}" + await data_handler.register_data_class( + HOMESTATUS_DATA_CLASS_NAME, signal_name, None, home_id=home_id + ) + home_status = data_handler.data.get(signal_name) + if home_status and room_id in home_status.rooms: + entities.append(NetatmoThermostat(data_handler, home_id, room_id)) - for home_id in get_all_home_ids(home_data): - _LOGGER.debug("Setting up home %s", home_id) - for room_id in home_data.rooms[home_id].keys(): - room_name = home_data.rooms[home_id][room_id]["name"] - _LOGGER.debug("Setting up room %s (%s)", room_name, room_id) - signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( - HOMESTATUS_DATA_CLASS_NAME, signal_name, None, home_id=home_id - ) - home_status = data_handler.data.get(signal_name) - if home_status and room_id in home_status.rooms: - entities.append(NetatmoThermostat(data_handler, home_id, room_id)) - - hass.data[DOMAIN][DATA_SCHEDULES][home_id] = { - schedule_id: schedule_data.get("name") - for schedule_id, schedule_data in ( - data_handler.data[HOMEDATA_DATA_CLASS_NAME] - .schedules[home_id] - .items() - ) - } - - hass.data[DOMAIN][DATA_HOMES] = { - home_id: home_data.get("name") - for home_id, home_data in ( - data_handler.data[HOMEDATA_DATA_CLASS_NAME].homes.items() + hass.data[DOMAIN][DATA_SCHEDULES][home_id] = { + schedule_id: schedule_data.get("name") + for schedule_id, schedule_data in ( + data_handler.data[HOMEDATA_DATA_CLASS_NAME].schedules[home_id].items() ) } - return entities + hass.data[DOMAIN][DATA_HOMES] = { + home_id: home_data.get("name") + for home_id, home_data in ( + data_handler.data[HOMEDATA_DATA_CLASS_NAME].homes.items() + ) + } - async_add_entities(await get_entities(), True) - - await data_handler.unregister_data_class(HOMEDATA_DATA_CLASS_NAME, None) + _LOGGER.debug("Adding climate devices %s", entities) + async_add_entities(entities, True) platform = entity_platform.async_get_current_platform() @@ -164,7 +156,7 @@ async def async_setup_entry(hass, entry, async_add_entities): platform.async_register_entity_service( SERVICE_SET_SCHEDULE, {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, - "_service_set_schedule", + "_async_service_set_schedule", ) @@ -205,7 +197,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._model = NA_THERM break - self._state = None self._device_name = self._data.rooms[home_id][room_id]["name"] self._name = f"{MANUFACTURER} {self._device_name}" self._current_temperature = None @@ -357,24 +348,24 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE - def set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_OFF: - self.turn_off() + await self.async_turn_off() elif hvac_mode == HVAC_MODE_AUTO: if self.hvac_mode == HVAC_MODE_OFF: - self.turn_on() - self.set_preset_mode(PRESET_SCHEDULE) + await self.async_turn_on() + await self.async_set_preset_mode(PRESET_SCHEDULE) elif hvac_mode == HVAC_MODE_HEAT: - self.set_preset_mode(PRESET_BOOST) + await self.async_set_preset_mode(PRESET_BOOST) - def set_preset_mode(self, preset_mode: str) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if self.hvac_mode == HVAC_MODE_OFF: - self.turn_on() + await self.async_turn_on() if self.target_temperature == 0: - self._home_status.set_room_thermpoint( + await self._home_status.async_set_room_thermpoint( self._id, STATE_NETATMO_HOME, ) @@ -384,14 +375,14 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): and self._model == NA_VALVE and self.hvac_mode == HVAC_MODE_HEAT ): - self._home_status.set_room_thermpoint( + await self._home_status.async_set_room_thermpoint( self._id, STATE_NETATMO_HOME, ) elif ( preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self._model == NA_VALVE ): - self._home_status.set_room_thermpoint( + await self._home_status.async_set_room_thermpoint( self._id, STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, @@ -400,13 +391,15 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self.hvac_mode == HVAC_MODE_HEAT ): - self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_HOME) + await self._home_status.async_set_room_thermpoint( + self._id, STATE_NETATMO_HOME + ) elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX]: - self._home_status.set_room_thermpoint( + await self._home_status.async_set_room_thermpoint( self._id, PRESET_MAP_NETATMO[preset_mode] ) elif preset_mode in [PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY]: - self._home_status.set_thermmode(PRESET_MAP_NETATMO[preset_mode]) + await self._home_status.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode]) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) @@ -422,12 +415,14 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Return a list of available preset modes.""" return SUPPORT_PRESET - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature for 2 hours.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is None: return - self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_MANUAL, temp) + await self._home_status.async_set_room_thermpoint( + self._id, STATE_NETATMO_MANUAL, temp + ) self.async_write_ha_state() @@ -449,21 +444,23 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return attr - def turn_off(self): + async def async_turn_off(self): """Turn the entity off.""" if self._model == NA_VALVE: - self._home_status.set_room_thermpoint( + await self._home_status.async_set_room_thermpoint( self._id, STATE_NETATMO_MANUAL, DEFAULT_MIN_TEMP, ) elif self.hvac_mode != HVAC_MODE_OFF: - self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_OFF) + await self._home_status.async_set_room_thermpoint( + self._id, STATE_NETATMO_OFF + ) self.async_write_ha_state() - def turn_on(self): + async def async_turn_on(self): """Turn the entity on.""" - self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_HOME) + await self._home_status.async_set_room_thermpoint(self._id, STATE_NETATMO_HOME) self.async_write_ha_state() @property @@ -475,6 +472,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): def async_update_callback(self): """Update the entity's state.""" self._home_status = self.data_handler.data[self._home_status_class] + if self._home_status is None: + if self.available: + self._connected = False + return + self._room_status = self._home_status.rooms.get(self._id) self._room_data = self._data.rooms.get(self._home_id, {}).get(self._id) @@ -570,7 +572,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return {} - def _service_set_schedule(self, **kwargs): + async def _async_service_set_schedule(self, **kwargs): schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_id = None for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): @@ -581,7 +583,9 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) return - self._data.switch_home_schedule(home_id=self._home_id, schedule_id=schedule_id) + await self._data.async_switch_home_schedule( + home_id=self._home_id, schedule_id=schedule_id + ) _LOGGER.debug( "Setting %s schedule to %s (%s)", self._home_id, diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index b8f81257eab..d3c2db95afa 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -1,9 +1,9 @@ """The Netatmo data handler.""" from __future__ import annotations +import asyncio from collections import deque from datetime import timedelta -from functools import partial from itertools import islice import logging from time import time @@ -19,22 +19,22 @@ from .const import AUTH, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) -CAMERA_DATA_CLASS_NAME = "CameraData" -WEATHERSTATION_DATA_CLASS_NAME = "WeatherStationData" -HOMECOACH_DATA_CLASS_NAME = "HomeCoachData" -HOMEDATA_DATA_CLASS_NAME = "HomeData" -HOMESTATUS_DATA_CLASS_NAME = "HomeStatus" -PUBLICDATA_DATA_CLASS_NAME = "PublicData" +CAMERA_DATA_CLASS_NAME = "AsyncCameraData" +WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData" +HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData" +HOMEDATA_DATA_CLASS_NAME = "AsyncHomeData" +HOMESTATUS_DATA_CLASS_NAME = "AsyncHomeStatus" +PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData" NEXT_SCAN = "next_scan" DATA_CLASSES = { - WEATHERSTATION_DATA_CLASS_NAME: pyatmo.WeatherStationData, - HOMECOACH_DATA_CLASS_NAME: pyatmo.HomeCoachData, - CAMERA_DATA_CLASS_NAME: pyatmo.CameraData, - HOMEDATA_DATA_CLASS_NAME: pyatmo.HomeData, - HOMESTATUS_DATA_CLASS_NAME: pyatmo.HomeStatus, - PUBLICDATA_DATA_CLASS_NAME: pyatmo.PublicData, + WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData, + HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData, + CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData, + HOMEDATA_DATA_CLASS_NAME: pyatmo.AsyncHomeData, + HOMESTATUS_DATA_CLASS_NAME: pyatmo.AsyncHomeStatus, + PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData, } BATCH_SIZE = 3 @@ -57,7 +57,7 @@ class NetatmoDataHandler: self.hass = hass self._auth = hass.data[DOMAIN][entry.entry_id][AUTH] self.listeners: list[CALLBACK_TYPE] = [] - self._data_classes: dict = {} + self.data_classes: dict = {} self.data = {} self._queue = deque() self._webhook: bool = False @@ -87,21 +87,19 @@ class NetatmoDataHandler: for data_class in islice(self._queue, 0, BATCH_SIZE): if data_class[NEXT_SCAN] > time(): continue - self._data_classes[data_class["name"]][NEXT_SCAN] = ( + self.data_classes[data_class["name"]][NEXT_SCAN] = ( time() + data_class["interval"] ) - await self.async_fetch_data( - data_class["class"], data_class["name"], **data_class["kwargs"] - ) + await self.async_fetch_data(data_class["name"]) self._queue.rotate(BATCH_SIZE) @callback def async_force_update(self, data_class_entry): """Prioritize data retrieval for given data class entry.""" - self._data_classes[data_class_entry][NEXT_SCAN] = time() - self._queue.rotate(-(self._queue.index(self._data_classes[data_class_entry]))) + self.data_classes[data_class_entry][NEXT_SCAN] = time() + self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry]))) async def async_cleanup(self): """Clean up the Netatmo data handler.""" @@ -122,19 +120,10 @@ class NetatmoDataHandler: _LOGGER.debug("%s camera reconnected", MANUFACTURER) self.async_force_update(CAMERA_DATA_CLASS_NAME) - async def async_fetch_data(self, data_class, data_class_entry, **kwargs): + async def async_fetch_data(self, data_class_entry): """Fetch data and notify.""" try: - self.data[data_class_entry] = await self.hass.async_add_executor_job( - partial(data_class, **kwargs), - self._auth, - ) - - for update_callback in self._data_classes[data_class_entry][ - "subscriptions" - ]: - if update_callback: - update_callback() + await self.data[data_class_entry].async_update() except pyatmo.NoDevice as err: _LOGGER.debug(err) @@ -143,42 +132,46 @@ class NetatmoDataHandler: except pyatmo.ApiError as err: _LOGGER.debug(err) + except asyncio.TimeoutError as err: + _LOGGER.debug(err) + return + + for update_callback in self.data_classes[data_class_entry]["subscriptions"]: + if update_callback: + update_callback() + async def register_data_class( self, data_class_name, data_class_entry, update_callback, **kwargs ): """Register data class.""" - if data_class_entry in self._data_classes: - self._data_classes[data_class_entry]["subscriptions"].append( - update_callback - ) + if data_class_entry in self.data_classes: + self.data_classes[data_class_entry]["subscriptions"].append(update_callback) return - self._data_classes[data_class_entry] = { - "class": DATA_CLASSES[data_class_name], + self.data_classes[data_class_entry] = { "name": data_class_entry, "interval": DEFAULT_INTERVALS[data_class_name], NEXT_SCAN: time() + DEFAULT_INTERVALS[data_class_name], - "kwargs": kwargs, "subscriptions": [update_callback], } - await self.async_fetch_data( - DATA_CLASSES[data_class_name], data_class_entry, **kwargs + self.data[data_class_entry] = DATA_CLASSES[data_class_name]( + self._auth, **kwargs ) - self._queue.append(self._data_classes[data_class_entry]) + await self.async_fetch_data(data_class_entry) + + self._queue.append(self.data_classes[data_class_entry]) _LOGGER.debug("Data class %s added", data_class_entry) async def unregister_data_class(self, data_class_entry, update_callback): """Unregister data class.""" - if update_callback not in self._data_classes[data_class_entry]["subscriptions"]: - return + self.data_classes[data_class_entry]["subscriptions"].remove(update_callback) - self._data_classes[data_class_entry]["subscriptions"].remove(update_callback) - - if not self._data_classes[data_class_entry].get("subscriptions"): - self._queue.remove(self._data_classes[data_class_entry]) - self._data_classes.pop(data_class_entry) + if not self.data_classes[data_class_entry].get("subscriptions"): + self._queue.remove(self.data_classes[data_class_entry]) + self.data_classes.pop(data_class_entry) + self.data.pop(data_class_entry) _LOGGER.debug("Data class %s removed", data_class_entry) @property diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 47712e75a0d..f51b0fd9eaf 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -1,8 +1,6 @@ """Support for the Netatmo camera lights.""" import logging -import pyatmo - from homeassistant.components.light import LightEntity from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady @@ -34,41 +32,29 @@ async def async_setup_entry(hass, entry, async_add_entities): await data_handler.register_data_class( CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None ) + data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME) - if CAMERA_DATA_CLASS_NAME not in data_handler.data: + if not data_class or data_class.raw_data == {}: raise PlatformNotReady - async def get_entities(): - """Retrieve Netatmo entities.""" + all_cameras = [] + for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values(): + for camera in home.values(): + all_cameras.append(camera) - entities = [] - all_cameras = [] + entities = [ + NetatmoLight( + data_handler, + camera["id"], + camera["type"], + camera["home_id"], + ) + for camera in all_cameras + if camera["type"] == "NOC" + ] - try: - for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values(): - for camera in home.values(): - all_cameras.append(camera) - - except pyatmo.NoDevice: - _LOGGER.debug("No cameras found") - - for camera in all_cameras: - if camera["type"] == "NOC": - _LOGGER.debug("Adding camera light %s %s", camera["id"], camera["name"]) - entities.append( - NetatmoLight( - data_handler, - camera["id"], - camera["type"], - camera["home_id"], - ) - ) - - return entities - - async_add_entities(await get_entities(), True) - - await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None) + _LOGGER.debug("Adding camera lights %s", entities) + async_add_entities(entities, True) class NetatmoLight(NetatmoBase, LightEntity): @@ -136,19 +122,19 @@ class NetatmoLight(NetatmoBase, LightEntity): """Return true if light is on.""" return self._is_on - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn camera floodlight on.""" _LOGGER.debug("Turn camera '%s' on", self._name) - self._data.set_state( + await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, floodlight="on", ) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn camera floodlight into auto mode.""" _LOGGER.debug("Turn camera '%s' to auto mode", self._name) - self._data.set_state( + await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, floodlight="auto", diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 090bc3dd9d6..60a54df8a6e 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==4.2.3" + "pyatmo==5.0.1" ], "after_dependencies": [ "cloud", diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 061b2c57971..ea023d1ef57 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -159,7 +159,7 @@ def async_parse_identifier( item: MediaSourceItem, ) -> tuple[str, str, int | None]: """Parse identifier.""" - if not item.identifier: + if "/" not in item.identifier: return "events", "", None source, path = item.identifier.lstrip("/").split("/", 1) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index e41b873bdc4..1fcd4a121d8 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -7,7 +7,7 @@ from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import Entity from .const import DATA_DEVICE_IDS, DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME -from .data_handler import NetatmoDataHandler +from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler _LOGGER = logging.getLogger(__name__) @@ -29,7 +29,6 @@ class NetatmoBase(Entity): async def async_added_to_hass(self) -> None: """Entity created.""" - _LOGGER.debug("New client %s", self.entity_id) for data_class in self._data_classes: signal_name = data_class[SIGNAL_NAME] @@ -41,7 +40,7 @@ class NetatmoBase(Entity): home_id=data_class["home_id"], ) - elif data_class["name"] == "PublicData": + elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME: await self.data_handler.register_data_class( data_class["name"], signal_name, @@ -57,7 +56,9 @@ class NetatmoBase(Entity): data_class["name"], signal_name, self.async_update_callback ) - await self.data_handler.unregister_data_class(signal_name, None) + for sub in self.data_handler.data_classes[signal_name].get("subscriptions"): + if sub is None: + await self.data_handler.unregister_data_class(signal_name, None) registry = await self.hass.helpers.device_registry.async_get_registry() device = registry.async_get_device({(DOMAIN, self._id)}, set()) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 380ae1eff69..e56847386a3 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -132,18 +132,8 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - await data_handler.register_data_class( - WEATHERSTATION_DATA_CLASS_NAME, WEATHERSTATION_DATA_CLASS_NAME, None - ) - await data_handler.register_data_class( - HOMECOACH_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME, None - ) - async def find_entities(data_class_name): """Find all entities.""" - if data_class_name not in data_handler.data: - raise PlatformNotReady - all_module_infos = {} data = data_handler.data @@ -167,11 +157,6 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.debug("Skipping module %s", module.get("module_name")) continue - _LOGGER.debug( - "Adding module %s %s", - module.get("module_name"), - module.get("_id"), - ) conditions = [ c.lower() for c in data_class.get_monitored_conditions(module_id=module["_id"]) @@ -188,14 +173,19 @@ async def async_setup_entry(hass, entry, async_add_entities): NetatmoSensor(data_handler, data_class_name, module, condition) ) - await data_handler.unregister_data_class(data_class_name, None) - + _LOGGER.debug("Adding weather sensors %s", entities) return entities for data_class_name in [ WEATHERSTATION_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME, ]: + await data_handler.register_data_class(data_class_name, data_class_name, None) + data_class = data_handler.data.get(data_class_name) + + if not data_class or not data_class.raw_data: + raise PlatformNotReady + async_add_entities(await find_entities(data_class_name), True) device_registry = await hass.helpers.device_registry.async_get_registry() @@ -410,6 +400,8 @@ class NetatmoSensor(NetatmoBase, SensorEntity): self._state = None return + self.async_write_ha_state() + def fix_angle(angle: int) -> int: """Fix angle when value is negative.""" @@ -615,13 +607,6 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): @callback def async_update_callback(self): """Update the entity's state.""" - if self._data is None: - if self._state is None: - return - _LOGGER.warning("No data from update") - self._state = None - return - data = None if self.type == "temperature": @@ -655,3 +640,5 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._state = round(sum(values) / len(values), 1) elif self._mode == "max": self._state = max(values) + + self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index dfcb906b0c5..c122c63fc4f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1295,7 +1295,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==4.2.3 +pyatmo==5.0.1 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6025a78b6ae..5587aa5c58a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -720,7 +720,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==4.2.3 +pyatmo==5.0.1 # homeassistant.components.apple_tv pyatv==0.7.7 diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 54e7610c4e5..32202cb85e5 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -1,5 +1,7 @@ """Common methods used across tests for Netatmo.""" +from contextlib import contextmanager import json +from unittest.mock import patch from homeassistant.components.webhook import async_handle_webhook from homeassistant.util.aiohttp import MockRequest @@ -35,13 +37,19 @@ FAKE_WEBHOOK_ACTIVATION = { "push_type": "webhook_activation", } +DEFAULT_PLATFORMS = ["camera", "climate", "light", "sensor"] -def fake_post_request(**args): + +async def fake_post_request(*args, **kwargs): """Return fake data.""" - if "url" not in args: + if "url" not in kwargs: return "{}" - endpoint = args["url"].split("/")[-1] + endpoint = kwargs["url"].split("/")[-1] + + if endpoint in "snapshot_720.jpg": + return b"test stream image bytes" + if endpoint in [ "setpersonsaway", "setpersonshome", @@ -55,7 +63,7 @@ def fake_post_request(**args): return json.loads(load_fixture(f"netatmo/{endpoint}.json")) -def fake_post_request_no_data(**args): +async def fake_post_request_no_data(*args, **kwargs): """Fake error during requesting backend data.""" return "{}" @@ -68,3 +76,12 @@ async def simulate_webhook(hass, webhook_id, response): ) await async_handle_webhook(hass, webhook_id, request) await hass.async_block_till_done() + + +@contextmanager +def selected_platforms(platforms): + """Restrict loaded platforms to list given.""" + with patch("homeassistant.components.netatmo.PLATFORMS", platforms), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch("homeassistant.components.webhook.async_generate_url"): + yield diff --git a/tests/components/netatmo/conftest.py b/tests/components/netatmo/conftest.py index 9a16391d2a4..d443802a41d 100644 --- a/tests/components/netatmo/conftest.py +++ b/tests/components/netatmo/conftest.py @@ -1,17 +1,16 @@ """Provide common Netatmo fixtures.""" -from contextlib import contextmanager from time import time -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest -from .common import ALL_SCOPES, TEST_TIME, fake_post_request, fake_post_request_no_data +from .common import ALL_SCOPES, fake_post_request from tests.common import MockConfigEntry @pytest.fixture(name="config_entry") -async def mock_config_entry_fixture(hass): +def mock_config_entry_fixture(hass): """Mock a config entry.""" mock_entry = MockConfigEntry( domain="netatmo", @@ -54,81 +53,13 @@ async def mock_config_entry_fixture(hass): return mock_entry -@contextmanager -def selected_platforms(platforms=["camera", "climate", "light", "sensor"]): +@pytest.fixture +def netatmo_auth(): """Restrict loaded platforms to list given.""" - with patch("homeassistant.components.netatmo.PLATFORMS", platforms), patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.webhook.async_generate_url" - ): - mock_auth.return_value.post_request.side_effect = fake_post_request - yield - - -@pytest.fixture(name="entry") -async def mock_entry_fixture(hass, config_entry): - """Mock setup of all platforms.""" - with selected_platforms(): - await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - return config_entry - - -@pytest.fixture(name="sensor_entry") -async def mock_sensor_entry_fixture(hass, config_entry): - """Mock setup of sensor platform.""" - with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - yield config_entry - - -@pytest.fixture(name="camera_entry") -async def mock_camera_entry_fixture(hass, config_entry): - """Mock setup of camera platform.""" - with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - return config_entry - - -@pytest.fixture(name="light_entry") -async def mock_light_entry_fixture(hass, config_entry): - """Mock setup of light platform.""" - with selected_platforms(["light"]): - await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - return config_entry - - -@pytest.fixture(name="climate_entry") -async def mock_climate_entry_fixture(hass, config_entry): - """Mock setup of climate platform.""" - with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - return config_entry - - -@pytest.fixture(name="entry_error") -async def mock_entry_error_fixture(hass, config_entry): - """Mock erroneous setup of platforms.""" with patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.webhook.async_generate_url" - ): - mock_auth.return_value.post_request.side_effect = fake_post_request_no_data - await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - yield config_entry + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth: + mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + yield diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 372af748267..4825946beab 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -1,6 +1,9 @@ """The tests for Netatmo camera.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch + +import pyatmo +import pytest from homeassistant.components import camera from homeassistant.components.camera import STATE_STREAMING @@ -13,14 +16,19 @@ from homeassistant.components.netatmo.const import ( from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.util import dt -from .common import fake_post_request, simulate_webhook +from .common import fake_post_request, selected_platforms, simulate_webhook from tests.common import async_capture_events, async_fire_time_changed -async def test_setup_component_with_webhook(hass, camera_entry): +async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): """Test setup with webhook.""" - webhook_id = camera_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] await hass.async_block_till_done() camera_entity_indoor = "camera.netatmo_hall" @@ -58,7 +66,7 @@ async def test_setup_component_with_webhook(hass, camera_entry): } await simulate_webhook(hass, webhook_id, response) - assert hass.states.get(camera_entity_indoor).state == "streaming" + assert hass.states.get(camera_entity_outdoor).state == "streaming" assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "on" response = { @@ -84,12 +92,39 @@ async def test_setup_component_with_webhook(hass, camera_entry): assert hass.states.get(camera_entity_indoor).state == "streaming" assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "auto" + with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: + await hass.services.async_call( + "camera", "turn_off", service_data={"entity_id": "camera.netatmo_hall"} + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + home_id="91763b24c43d3e344f424e8b", + camera_id="12:34:56:00:f1:62", + monitoring="off", + ) + + with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: + await hass.services.async_call( + "camera", "turn_on", service_data={"entity_id": "camera.netatmo_hall"} + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + home_id="91763b24c43d3e344f424e8b", + camera_id="12:34:56:00:f1:62", + monitoring="on", + ) + IMAGE_BYTES_FROM_STREAM = b"test stream image bytes" -async def test_camera_image_local(hass, camera_entry, requests_mock): +async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_auth): """Test retrieval or local camera image.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.async_block_till_done() uri = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" @@ -111,8 +146,13 @@ async def test_camera_image_local(hass, camera_entry, requests_mock): assert image.content == IMAGE_BYTES_FROM_STREAM -async def test_camera_image_vpn(hass, camera_entry, requests_mock): +async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth): """Test retrieval of remote camera image.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.async_block_till_done() uri = ( @@ -137,8 +177,13 @@ async def test_camera_image_vpn(hass, camera_entry, requests_mock): assert image.content == IMAGE_BYTES_FROM_STREAM -async def test_service_set_person_away(hass, camera_entry): +async def test_service_set_person_away(hass, config_entry, netatmo_auth): """Test service to set person as away.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.async_block_till_done() data = { @@ -146,7 +191,9 @@ async def test_service_set_person_away(hass, camera_entry): "person": "Richard Doe", } - with patch("pyatmo.camera.CameraData.set_persons_away") as mock_set_persons_away: + with patch( + "pyatmo.camera.AsyncCameraData.async_set_persons_away" + ) as mock_set_persons_away: await hass.services.async_call( "netatmo", SERVICE_SET_PERSON_AWAY, service_data=data ) @@ -160,7 +207,9 @@ async def test_service_set_person_away(hass, camera_entry): "entity_id": "camera.netatmo_hall", } - with patch("pyatmo.camera.CameraData.set_persons_away") as mock_set_persons_away: + with patch( + "pyatmo.camera.AsyncCameraData.async_set_persons_away" + ) as mock_set_persons_away: await hass.services.async_call( "netatmo", SERVICE_SET_PERSON_AWAY, service_data=data ) @@ -171,8 +220,13 @@ async def test_service_set_person_away(hass, camera_entry): ) -async def test_service_set_persons_home(hass, camera_entry): +async def test_service_set_persons_home(hass, config_entry, netatmo_auth): """Test service to set persons as home.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.async_block_till_done() data = { @@ -180,7 +234,9 @@ async def test_service_set_persons_home(hass, camera_entry): "persons": "John Doe", } - with patch("pyatmo.camera.CameraData.set_persons_home") as mock_set_persons_home: + with patch( + "pyatmo.camera.AsyncCameraData.async_set_persons_home" + ) as mock_set_persons_home: await hass.services.async_call( "netatmo", SERVICE_SET_PERSONS_HOME, service_data=data ) @@ -191,8 +247,13 @@ async def test_service_set_persons_home(hass, camera_entry): ) -async def test_service_set_camera_light(hass, camera_entry): +async def test_service_set_camera_light(hass, config_entry, netatmo_auth): """Test service to set the outdoor camera light mode.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.async_block_till_done() data = { @@ -200,7 +261,7 @@ async def test_service_set_camera_light(hass, camera_entry): "camera_light_mode": "on", } - with patch("pyatmo.camera.CameraData.set_state") as mock_set_state: + with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( "netatmo", SERVICE_SET_CAMERA_LIGHT, service_data=data ) @@ -214,16 +275,26 @@ async def test_service_set_camera_light(hass, camera_entry): async def test_camera_reconnect_webhook(hass, config_entry): """Test webhook event on camera reconnect.""" + fake_post_hits = 0 + + async def fake_post(*args, **kwargs): + """Fake error during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return await fake_post_request(*args, **kwargs) + with patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth.post_request" - ) as mock_post, patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( "homeassistant.components.netatmo.PLATFORMS", ["camera"] ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( "homeassistant.components.webhook.async_generate_url" ) as mock_webhook: - mock_post.side_effect = fake_post_request + mock_auth.return_value.async_post_request.side_effect = fake_post + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() mock_webhook.return_value = "https://example.com" await hass.config_entries.async_setup(config_entry.entry_id) @@ -238,8 +309,9 @@ async def test_camera_reconnect_webhook(hass, config_entry): await simulate_webhook(hass, webhook_id, response) await hass.async_block_till_done() - mock_post.assert_called() - mock_post.reset_mock() + assert fake_post_hits == 5 + + calls = fake_post_hits # Fake camera reconnect response = { @@ -253,11 +325,16 @@ async def test_camera_reconnect_webhook(hass, config_entry): dt.utcnow() + timedelta(seconds=60), ) await hass.async_block_till_done() - mock_post.assert_called() + assert fake_post_hits > calls -async def test_webhook_person_event(hass, camera_entry): +async def test_webhook_person_event(hass, config_entry, netatmo_auth): """Test that person events are handled.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + test_netatmo_event = async_capture_events(hass, NETATMO_EVENT) assert not test_netatmo_event @@ -282,7 +359,80 @@ async def test_webhook_person_event(hass, camera_entry): "push_type": "NACamera-person", } - webhook_id = camera_entry.data[CONF_WEBHOOK_ID] + webhook_id = config_entry.data[CONF_WEBHOOK_ID] await simulate_webhook(hass, webhook_id, fake_webhook_event) assert test_netatmo_event + + +async def test_setup_component_no_devices(hass, config_entry): + """Test setup with no devices.""" + fake_post_hits = 0 + + async def fake_post_no_data(*args, **kwargs): + """Fake error during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return "{}" + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( + "homeassistant.components.netatmo.PLATFORMS", ["camera"] + ), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = fake_post_no_data + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert fake_post_hits == 1 + + +async def test_camera_image_raises_exception(hass, config_entry, requests_mock): + """Test setup with no devices.""" + fake_post_hits = 0 + + async def fake_post(*args, **kwargs): + """Return fake data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + + if "url" not in kwargs: + return "{}" + + endpoint = kwargs["url"].split("/")[-1] + + if "snapshot_720.jpg" in endpoint: + raise pyatmo.exceptions.ApiError() + + return await fake_post_request(*args, **kwargs) + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( + "homeassistant.components.netatmo.PLATFORMS", ["camera"] + ), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = fake_post + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + camera_entity_indoor = "camera.netatmo_hall" + + with pytest.raises(Exception) as excinfo: + await camera.async_get_image(hass, camera_entity_indoor) + + assert excinfo.value.args == ("Unable to get image",) + assert fake_post_hits == 6 diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index 16359c85498..ef7f8884e2e 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -26,12 +26,17 @@ from homeassistant.components.netatmo.const import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID -from .common import simulate_webhook +from .common import selected_platforms, simulate_webhook -async def test_webhook_event_handling_thermostats(hass, climate_entry): +async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_auth): """Test service and webhook event handling with thermostats.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_livingroom = "climate.netatmo_livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" @@ -199,9 +204,16 @@ async def test_webhook_event_handling_thermostats(hass, climate_entry): ) -async def test_service_preset_mode_frost_guard_thermostat(hass, climate_entry): +async def test_service_preset_mode_frost_guard_thermostat( + hass, config_entry, netatmo_auth +): """Test service with frost guard preset for thermostats.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_livingroom = "climate.netatmo_livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" @@ -267,9 +279,14 @@ async def test_service_preset_mode_frost_guard_thermostat(hass, climate_entry): ) -async def test_service_preset_modes_thermostat(hass, climate_entry): +async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth): """Test service with preset modes for thermostats.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_livingroom = "climate.netatmo_livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" @@ -341,10 +358,15 @@ async def test_service_preset_modes_thermostat(hass, climate_entry): assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 30 -async def test_webhook_event_handling_no_data(hass, climate_entry): +async def test_webhook_event_handling_no_data(hass, config_entry, netatmo_auth): """Test service and webhook event handling with erroneous data.""" + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + # Test webhook without home entry - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + webhook_id = config_entry.data[CONF_WEBHOOK_ID] response = { "push_type": "home_event_changed", @@ -385,14 +407,19 @@ async def test_webhook_event_handling_no_data(hass, climate_entry): await simulate_webhook(hass, webhook_id, response) -async def test_service_schedule_thermostats(hass, climate_entry, caplog): +async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_auth): """Test service for selecting Netatmo schedule with thermostats.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_livingroom = "climate.netatmo_livingroom" # Test setting a valid schedule with patch( - "pyatmo.thermostat.HomeData.switch_home_schedule" + "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" ) as mock_switch_home_schedule: await hass.services.async_call( "netatmo", @@ -421,7 +448,7 @@ async def test_service_schedule_thermostats(hass, climate_entry, caplog): # Test setting an invalid schedule with patch( - "pyatmo.thermostat.HomeData.switch_home_schedule" + "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" ) as mock_switch_home_schedule: await hass.services.async_call( "netatmo", @@ -435,9 +462,16 @@ async def test_service_schedule_thermostats(hass, climate_entry, caplog): assert "summer is not a valid schedule" in caplog.text -async def test_service_preset_mode_already_boost_valves(hass, climate_entry): +async def test_service_preset_mode_already_boost_valves( + hass, config_entry, netatmo_auth +): """Test service with boost preset for valves when already in boost mode.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" assert hass.states.get(climate_entity_entrada).state == "auto" @@ -508,9 +542,14 @@ async def test_service_preset_mode_already_boost_valves(hass, climate_entry): assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30 -async def test_service_preset_mode_boost_valves(hass, climate_entry): +async def test_service_preset_mode_boost_valves(hass, config_entry, netatmo_auth): """Test service with boost preset for valves.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" # Test service setting the preset mode to "boost" @@ -553,8 +592,13 @@ async def test_service_preset_mode_boost_valves(hass, climate_entry): assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30 -async def test_service_preset_mode_invalid(hass, climate_entry, caplog): +async def test_service_preset_mode_invalid(hass, config_entry, caplog, netatmo_auth): """Test service with invalid preset.""" + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -566,9 +610,14 @@ async def test_service_preset_mode_invalid(hass, climate_entry, caplog): assert "Preset mode 'invalid' not available" in caplog.text -async def test_valves_service_turn_off(hass, climate_entry): +async def test_valves_service_turn_off(hass, config_entry, netatmo_auth): """Test service turn off for valves.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" # Test turning valve off @@ -606,9 +655,14 @@ async def test_valves_service_turn_off(hass, climate_entry): assert hass.states.get(climate_entity_entrada).state == "off" -async def test_valves_service_turn_on(hass, climate_entry): +async def test_valves_service_turn_on(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" # Test turning valve on @@ -661,9 +715,14 @@ async def test_get_all_home_ids(): assert climate.get_all_home_ids(home_data) == expected -async def test_webhook_home_id_mismatch(hass, climate_entry): +async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" assert hass.states.get(climate_entity_entrada).state == "auto" @@ -694,9 +753,14 @@ async def test_webhook_home_id_mismatch(hass, climate_entry): assert hass.states.get(climate_entity_entrada).state == "auto" -async def test_webhook_set_point(hass, climate_entry): +async def test_webhook_set_point(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" # Fake backend response for valve being turned on diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 2ec7d83689e..b81c6f6ad16 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -1,15 +1,26 @@ """The tests for Netatmo component.""" +import asyncio +from datetime import timedelta from time import time -from unittest.mock import patch +from unittest.mock import AsyncMock, patch + +import pyatmo from homeassistant import config_entries from homeassistant.components.netatmo import DOMAIN from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.core import CoreState from homeassistant.setup import async_setup_component +from homeassistant.util import dt -from .common import FAKE_WEBHOOK_ACTIVATION, fake_post_request, simulate_webhook +from .common import ( + FAKE_WEBHOOK_ACTIVATION, + fake_post_request, + selected_platforms, + simulate_webhook, +) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.cloud import mock_cloud # Fake webhook thermostat mode change to "Max" @@ -57,13 +68,15 @@ async def test_setup_component(hass): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", ) as mock_auth, patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ) as mock_impl, patch( "homeassistant.components.webhook.async_generate_url" ) as mock_webhook: - mock_auth.return_value.post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() assert await async_setup_component(hass, "netatmo", {}) await hass.async_block_till_done() @@ -86,38 +99,54 @@ async def test_setup_component(hass): async def test_setup_component_with_config(hass, config_entry): """Test setup of the netatmo component with dev account.""" + fake_post_hits = 0 + + async def fake_post(*args, **kwargs): + """Fake error during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return await fake_post_request(*args, **kwargs) + with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ) as mock_impl, patch( "homeassistant.components.webhook.async_generate_url" ) as mock_webhook, patch( - "pyatmo.auth.NetatmoOAuth2.post_request" - ) as fake_post_requests, patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, patch( "homeassistant.components.netatmo.PLATFORMS", ["sensor"] ): + mock_auth.return_value.async_post_request.side_effect = fake_post + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} ) await hass.async_block_till_done() - fake_post_requests.assert_called() + assert fake_post_hits == 3 mock_impl.assert_called_once() mock_webhook.assert_called_once() - assert config_entry.state == config_entries.ENTRY_STATE_LOADED - assert hass.config_entries.async_entries(DOMAIN) - assert len(hass.states.async_all()) > 0 + assert hass.config_entries.async_entries(DOMAIN) + assert len(hass.states.async_all()) > 0 -async def test_setup_component_with_webhook(hass, entry): +async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): """Test setup and teardown of the netatmo component with webhook registration.""" - webhook_id = entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["camera", "climate", "light", "sensor"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) assert len(hass.states.async_all()) > 0 - webhook_id = entry.data[CONF_WEBHOOK_ID] + webhook_id = config_entry.data[CONF_WEBHOOK_ID] await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) # Assert webhook is established successfully @@ -134,36 +163,30 @@ async def test_setup_component_with_webhook(hass, entry): assert len(hass.config_entries.async_entries(DOMAIN)) == 0 -async def test_setup_without_https(hass, config_entry): +async def test_setup_without_https(hass, config_entry, caplog): """Test if set up with cloud link and without https.""" hass.config.components.add("cloud") with patch( "homeassistant.helpers.network.get_url", - return_value="https://example.nabu.casa", + return_value="http://example.nabu.casa", ), patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" ) as mock_auth, patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( "homeassistant.components.webhook.async_generate_url" - ) as mock_webhook: - mock_auth.return_value.post_request.side_effect = fake_post_request - mock_webhook.return_value = "https://example.com" + ) as mock_async_generate_url: + mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_async_generate_url.return_value = "http://example.com" assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} ) - await hass.async_block_till_done() + await hass.async_block_till_done() + mock_auth.assert_called_once() + mock_async_generate_url.assert_called_once() - webhook_id = config_entry.data[CONF_WEBHOOK_ID] - await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) - - # Assert webhook is established successfully - climate_entity_livingroom = "climate.netatmo_livingroom" - assert hass.states.get(climate_entity_livingroom).state == "auto" - await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK) - await hass.async_block_till_done() - assert hass.states.get(climate_entity_livingroom).state == "heat" + assert "https and port 443 is required to register the webhook" in caplog.text async def test_setup_with_cloud(hass, config_entry): @@ -181,7 +204,7 @@ async def test_setup_with_cloud(hass, config_entry): ) as fake_create_cloudhook, patch( "homeassistant.components.cloud.async_delete_cloudhook" ) as fake_delete_cloudhook, patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" ) as mock_auth, patch( "homeassistant.components.netatmo.PLATFORMS", [] ), patch( @@ -189,7 +212,7 @@ async def test_setup_with_cloud(hass, config_entry): ), patch( "homeassistant.components.webhook.async_generate_url" ): - mock_auth.return_value.post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_request.side_effect = fake_post_request assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} ) @@ -210,3 +233,199 @@ async def test_setup_with_cloud(hass, config_entry): await hass.async_block_till_done() assert not hass.config_entries.async_entries(DOMAIN) + + +async def test_setup_with_cloudhook(hass): + """Test if set up with active cloud subscription and cloud hook.""" + config_entry = MockConfigEntry( + domain="netatmo", + data={ + "auth_implementation": "cloud", + "cloudhook_url": "https://hooks.nabu.casa/ABCD", + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "expires_at": time() + 1000, + "scope": "read_station", + }, + }, + ) + config_entry.add_to_hass(hass) + + await mock_cloud(hass) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.cloud.async_is_logged_in", return_value=True + ), patch( + "homeassistant.components.cloud.async_active_subscription", return_value=True + ), patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ) as fake_create_cloudhook, patch( + "homeassistant.components.cloud.async_delete_cloudhook" + ) as fake_delete_cloudhook, patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( + "homeassistant.components.netatmo.PLATFORMS", [] + ), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + assert await async_setup_component(hass, "netatmo", {}) + assert hass.components.cloud.async_active_subscription() is True + + assert ( + hass.config_entries.async_entries("netatmo")[0].data["cloudhook_url"] + == "https://hooks.nabu.casa/ABCD" + ) + + await hass.async_block_till_done() + assert hass.config_entries.async_entries(DOMAIN) + fake_create_cloudhook.assert_not_called() + + for config_entry in hass.config_entries.async_entries("netatmo"): + await hass.config_entries.async_remove(config_entry.entry_id) + fake_delete_cloudhook.assert_called_once() + + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(DOMAIN) + + +async def test_setup_component_api_error(hass): + """Test error on setup of the netatmo component.""" + config_entry = MockConfigEntry( + domain="netatmo", + data={ + "auth_implementation": "cloud", + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "expires_at": time() + 1000, + "scope": "read_station", + }, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = ( + pyatmo.exceptions.ApiError() + ) + + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + assert await async_setup_component(hass, "netatmo", {}) + + await hass.async_block_till_done() + + mock_auth.assert_called_once() + mock_impl.assert_called_once() + + +async def test_setup_component_api_timeout(hass): + """Test timeout on setup of the netatmo component.""" + config_entry = MockConfigEntry( + domain="netatmo", + data={ + "auth_implementation": "cloud", + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "expires_at": time() + 1000, + "scope": "read_station", + }, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = ( + asyncio.exceptions.TimeoutError() + ) + + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + assert await async_setup_component(hass, "netatmo", {}) + + await hass.async_block_till_done() + + mock_auth.assert_called_once() + mock_impl.assert_called_once() + + +async def test_setup_component_with_delay(hass, config_entry): + """Test setup of the netatmo component with delayed startup.""" + hass.state = CoreState.not_running + + with patch( + "pyatmo.AbstractAsyncAuth.async_addwebhook", side_effect=AsyncMock() + ) as mock_addwebhook, patch( + "pyatmo.AbstractAsyncAuth.async_dropwebhook", side_effect=AsyncMock() + ) as mock_dropwebhook, patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, patch( + "homeassistant.components.webhook.async_generate_url" + ) as mock_webhook, patch( + "pyatmo.AbstractAsyncAuth.async_post_request", side_effect=fake_post_request + ) as mock_post_request, patch( + "homeassistant.components.netatmo.PLATFORMS", ["light"] + ): + + assert await async_setup_component( + hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} + ) + + await hass.async_block_till_done() + + assert mock_post_request.call_count == 5 + + mock_impl.assert_called_once() + mock_webhook.assert_not_called() + + await hass.async_start() + await hass.async_block_till_done() + mock_webhook.assert_called_once() + + # Fake webhook activation + await simulate_webhook( + hass, config_entry.data[CONF_WEBHOOK_ID], FAKE_WEBHOOK_ACTIVATION + ) + await hass.async_block_till_done() + + mock_addwebhook.assert_called_once() + mock_dropwebhook.assert_not_awaited() + + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=60), + ) + await hass.async_block_till_done() + + assert hass.config_entries.async_entries(DOMAIN) + assert len(hass.states.async_all()) > 0 + + await hass.async_stop() + mock_dropwebhook.assert_called_once() diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index 4d84bc4e5a5..6abbb646055 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -1,19 +1,25 @@ """The tests for Netatmo light.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) +from homeassistant.components.netatmo import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, CONF_WEBHOOK_ID -from .common import FAKE_WEBHOOK_ACTIVATION, simulate_webhook +from .common import FAKE_WEBHOOK_ACTIVATION, selected_platforms, simulate_webhook -async def test_light_setup_and_services(hass, light_entry): +async def test_light_setup_and_services(hass, config_entry, netatmo_auth): """Test setup and services.""" - webhook_id = light_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["light"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] # Fake webhook activation await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) @@ -45,7 +51,7 @@ async def test_light_setup_and_services(hass, light_entry): assert hass.states.get(light_entity).state == "on" # Test turning light off - with patch("pyatmo.camera.CameraData.set_state") as mock_set_state: + with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -60,7 +66,7 @@ async def test_light_setup_and_services(hass, light_entry): ) # Test turning light on - with patch("pyatmo.camera.CameraData.set_state") as mock_set_state: + with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -73,3 +79,43 @@ async def test_light_setup_and_services(hass, light_entry): camera_id="12:34:56:00:a5:a4", floodlight="on", ) + + +async def test_setup_component_no_devices(hass, config_entry): + """Test setup with no devices.""" + fake_post_hits = 0 + + async def fake_post_request_no_data(*args, **kwargs): + """Fake error during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return "{}" + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( + "homeassistant.components.netatmo.PLATFORMS", ["light"] + ), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = ( + fake_post_request_no_data + ) + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Fake webhook activation + await simulate_webhook( + hass, config_entry.data[CONF_WEBHOOK_ID], FAKE_WEBHOOK_ACTIVATION + ) + await hass.async_block_till_done() + + assert fake_post_hits == 1 + + assert hass.config_entries.async_entries(DOMAIN) + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index fd36c57dfd1..2ba70ca9489 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -51,6 +51,16 @@ async def test_async_browse_media(hass): ) assert str(excinfo.value) == "Unknown source directory." + # Test invalid base + with pytest.raises(ValueError) as excinfo: + await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}/") + assert str(excinfo.value) == "Invalid media source URI" + + # Test successful listing + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/events" + ) + # Test successful listing media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/" diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index fcb2ce454df..bebd8e0191c 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -1,23 +1,22 @@ """The tests for the Netatmo sensor platform.""" -from datetime import timedelta from unittest.mock import patch import pytest from homeassistant.components.netatmo import sensor from homeassistant.components.netatmo.sensor import MODULE_TYPE_WIND -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt -from .common import TEST_TIME -from .conftest import selected_platforms - -from tests.common import async_fire_time_changed +from .common import TEST_TIME, selected_platforms -async def test_weather_sensor(hass, sensor_entry): +async def test_weather_sensor(hass, config_entry, netatmo_auth): """Test weather sensor setup.""" + with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + prefix = "sensor.netatmo_mystation_" assert hass.states.get(f"{prefix}temperature").state == "24.6" @@ -26,8 +25,15 @@ async def test_weather_sensor(hass, sensor_entry): assert hass.states.get(f"{prefix}pressure").state == "1017.3" -async def test_public_weather_sensor(hass, sensor_entry): +async def test_public_weather_sensor(hass, config_entry, netatmo_auth): """Test public weather sensor setup.""" + with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + assert len(hass.states.async_all()) > 0 + prefix = "sensor.netatmo_home_max_" assert hass.states.get(f"{prefix}temperature").state == "27.4" @@ -40,7 +46,6 @@ async def test_public_weather_sensor(hass, sensor_entry): assert hass.states.get(f"{prefix}humidity").state == "63.2" assert hass.states.get(f"{prefix}pressure").state == "1010.3" - assert len(hass.states.async_all()) > 0 entities_before_change = len(hass.states.async_all()) valid_option = { @@ -53,7 +58,7 @@ async def test_public_weather_sensor(hass, sensor_entry): "mode": "max", } - result = await hass.config_entries.options.async_init(sensor_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"new_area": "Home avg"} ) @@ -63,18 +68,11 @@ async def test_public_weather_sensor(hass, sensor_entry): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - await hass.async_block_till_done() - async_fire_time_changed( - hass, - dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), - ) - await hass.async_block_till_done() - assert hass.states.get(f"{prefix}temperature").state == "27.4" - assert hass.states.get(f"{prefix}humidity").state == "76" - assert hass.states.get(f"{prefix}pressure").state == "1014.4" + await hass.async_block_till_done() assert len(hass.states.async_all()) == entities_before_change + assert hass.states.get(f"{prefix}temperature").state == "27.4" @pytest.mark.parametrize( @@ -213,7 +211,9 @@ async def test_fix_angle(angle, expected): ), ], ) -async def test_weather_sensor_enabling(hass, config_entry, uid, name, expected): +async def test_weather_sensor_enabling( + hass, config_entry, uid, name, expected, netatmo_auth +): """Test enabling of by default disabled sensors.""" with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): states_before = len(hass.states.async_all()) diff --git a/tests/fixtures/netatmo/homestatus.json b/tests/fixtures/netatmo/homestatus.json index 5d508ea03b0..490bf999045 100644 --- a/tests/fixtures/netatmo/homestatus.json +++ b/tests/fixtures/netatmo/homestatus.json @@ -27,7 +27,6 @@ "type": "NATherm1", "firmware_revision": 65, "rf_strength": 58, - "battery_level": 3793, "boiler_valve_comfort_boost": false, "boiler_status": false, "anticipating": false, @@ -40,7 +39,6 @@ "type": "NRV", "firmware_revision": 79, "rf_strength": 51, - "battery_level": 3025, "bridge": "12:34:56:00:fa:d0", "battery_state": "full" }, @@ -50,7 +48,6 @@ "type": "NRV", "firmware_revision": 79, "rf_strength": 59, - "battery_level": 2329, "bridge": "12:34:56:00:fa:d0", "battery_state": "full" } From 61ef58aa162329aa4fc1466e1fee8e7f5ba4746a Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 20 May 2021 15:14:34 +0200 Subject: [PATCH 599/852] bump garage_amsterdam lib to v2.0.5 (#50891) --- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index f0456c5afef..3e4d90a38aa 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -3,7 +3,7 @@ "name": "Garages Amsterdam", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", - "requirements": ["garages-amsterdam==2.0.4"], + "requirements": ["garages-amsterdam==2.0.5"], "codeowners": ["@klaasnicolaas"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index c122c63fc4f..c56b47aa2f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ fritzconnection==1.4.2 gTTS==2.2.2 # homeassistant.components.garages_amsterdam -garages-amsterdam==2.0.4 +garages-amsterdam==2.0.5 # homeassistant.components.garmin_connect garminconnect==0.1.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5587aa5c58a..5a0073112de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ fritzconnection==1.4.2 gTTS==2.2.2 # homeassistant.components.garages_amsterdam -garages-amsterdam==2.0.4 +garages-amsterdam==2.0.5 # homeassistant.components.garmin_connect garminconnect==0.1.19 From a65d3868cb55a59c18e02c1850cdb69cf74208d4 Mon Sep 17 00:00:00 2001 From: Fredrik Tuomas Date: Thu, 20 May 2021 15:39:34 +0200 Subject: [PATCH 600/852] Add support for EDS0066 (#50035) * Add support for EDS0066 * Added a test * Corrected entity_ids * Added missing part of sensor entity id * Add type hint * Update tests/components/onewire/const.py Co-authored-by: jan iversen * Update tests/components/onewire/const.py Co-authored-by: jan iversen * Revert "Update tests/components/onewire/const.py" This reverts commit 4a01b89868bb692bb2911ca5b9f9939611a5ff2f. * Revert "Update tests/components/onewire/const.py" This reverts commit 151eb9c0d3303b6bd3b3dc49a1eccd7c1a1b31b8. Co-authored-by: jan iversen --- homeassistant/components/onewire/sensor.py | 12 +++++++++ tests/components/onewire/const.py | 30 ++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index ba202ff24f2..3b63f551f98 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -196,6 +196,18 @@ HOBBYBOARD_EF: dict[str, list[DeviceComponentDescription]] = { # 7E sensors are special sensors by Embedded Data Systems EDS_SENSORS: dict[str, list[DeviceComponentDescription]] = { + "EDS0066": [ + { + "path": "EDS0066/temperature", + "name": "Temperature", + "type": SENSOR_TYPE_TEMPERATURE, + }, + { + "path": "EDS0066/pressure", + "name": "Pressure", + "type": SENSOR_TYPE_PRESSURE, + }, + ], "EDS0068": [ { "path": "EDS0068/temperature", diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index a58528ab55f..57d54e0dcc3 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -775,6 +775,36 @@ MOCK_OWPROXY_DEVICES = { }, ], }, + "7E.222222222222": { + "inject_reads": [ + b"EDS", # read type + b"EDS0066", # read device_type - note EDS specific + ], + "device_info": { + "identifiers": {(DOMAIN, "7E.222222222222")}, + "manufacturer": "Maxim Integrated", + "model": "EDS", + "name": "7E.222222222222", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.7e_222222222222_temperature", + "unique_id": "/7E.222222222222/EDS0066/temperature", + "injected_value": b" 13.9375", + "result": "13.9", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.7e_222222222222_pressure", + "unique_id": "/7E.222222222222/EDS0066/pressure", + "injected_value": b" 1012.21", + "result": "1012.2", + "unit": PRESSURE_MBAR, + "class": DEVICE_CLASS_PRESSURE, + }, + ], + }, } MOCK_SYSBUS_DEVICES = { From f212049fc242122f25e5313aff203c76f2c6c0dd Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 20 May 2021 15:58:17 +0200 Subject: [PATCH 601/852] Add constructor return type in integrations O-S (#50896) --- homeassistant/components/omnilogic/common.py | 4 ++-- homeassistant/components/omnilogic/sensor.py | 4 ++-- homeassistant/components/omnilogic/switch.py | 4 ++-- homeassistant/components/ondilo_ico/api.py | 2 +- homeassistant/components/ondilo_ico/oauth_impl.py | 2 +- homeassistant/components/ondilo_ico/sensor.py | 2 +- homeassistant/components/onewire/onewire_entities.py | 4 ++-- homeassistant/components/onewire/onewirehub.py | 2 +- homeassistant/components/onvif/device.py | 2 +- homeassistant/components/onvif/event.py | 4 +++- homeassistant/components/ovo_energy/sensor.py | 8 ++++---- homeassistant/components/person/__init__.py | 2 +- homeassistant/components/philips_js/__init__.py | 2 +- homeassistant/components/philips_js/media_player.py | 2 +- homeassistant/components/philips_js/remote.py | 2 +- homeassistant/components/picnic/coordinator.py | 2 +- homeassistant/components/plaato/config_flow.py | 2 +- homeassistant/components/rachio/config_flow.py | 2 +- homeassistant/components/recollect_waste/config_flow.py | 2 +- homeassistant/components/ring/__init__.py | 4 ++-- .../components/rituals_perfume_genie/__init__.py | 2 +- homeassistant/components/roku/__init__.py | 2 +- homeassistant/components/ruckus_unleashed/coordinator.py | 2 +- homeassistant/components/screenlogic/config_flow.py | 2 +- homeassistant/components/search/__init__.py | 2 +- homeassistant/components/sentry/config_flow.py | 2 +- homeassistant/components/sharkiq/vacuum.py | 4 +++- homeassistant/components/smappee/api.py | 2 +- homeassistant/components/smart_meter_texas/__init__.py | 4 +++- homeassistant/components/smart_meter_texas/sensor.py | 2 +- homeassistant/components/somfy/api.py | 2 +- homeassistant/components/somfy_mylink/config_flow.py | 2 +- homeassistant/components/spotify/media_player.py | 2 +- homeassistant/components/starline/account.py | 2 +- homeassistant/components/starline/binary_sensor.py | 2 +- homeassistant/components/starline/device_tracker.py | 2 +- homeassistant/components/starline/entity.py | 2 +- homeassistant/components/starline/lock.py | 2 +- homeassistant/components/starline/sensor.py | 2 +- homeassistant/components/starline/switch.py | 2 +- homeassistant/components/stream/core.py | 2 +- homeassistant/components/subaru/config_flow.py | 2 +- homeassistant/components/surepetcare/binary_sensor.py | 2 +- homeassistant/components/surepetcare/sensor.py | 2 +- homeassistant/components/synology_dsm/config_flow.py | 2 +- homeassistant/components/system_bridge/binary_sensor.py | 2 +- 46 files changed, 60 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index a4e8d4f491e..a103b8d112c 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -35,7 +35,7 @@ class OmniLogicUpdateCoordinator(DataUpdateCoordinator): name: str, config_entry: ConfigEntry, polling_interval: int, - ): + ) -> None: """Initialize the global Omnilogic data updater.""" self.api = api self.config_entry = config_entry @@ -89,7 +89,7 @@ class OmniLogicEntity(CoordinatorEntity): name: str, item_id: tuple, icon: str, - ): + ) -> None: """Initialize the OmniLogic Entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index 60ca685a2f8..beec071b192 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -61,7 +61,7 @@ class OmnilogicSensor(OmniLogicEntity, SensorEntity): unit: str, item_id: tuple, state_key: str, - ): + ) -> None: """Initialize Entities.""" super().__init__( coordinator=coordinator, @@ -217,7 +217,7 @@ class OmniLogicORPSensor(OmnilogicSensor): device_class: str, icon: str, unit: str, - ): + ) -> None: """Initialize the sensor.""" super().__init__( coordinator=coordinator, diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py index b6fec2e9f25..771b02a24c1 100644 --- a/homeassistant/components/omnilogic/switch.py +++ b/homeassistant/components/omnilogic/switch.py @@ -67,7 +67,7 @@ class OmniLogicSwitch(OmniLogicEntity, SwitchEntity): icon: str, item_id: tuple, state_key: str, - ): + ) -> None: """Initialize Entities.""" super().__init__( coordinator=coordinator, @@ -142,7 +142,7 @@ class OmniLogicPumpControl(OmniLogicSwitch): icon: str, item_id: tuple, state_key: str, - ): + ) -> None: """Initialize entities.""" super().__init__( coordinator=coordinator, diff --git a/homeassistant/components/ondilo_ico/api.py b/homeassistant/components/ondilo_ico/api.py index e753f8d6dcb..f698dcc693e 100644 --- a/homeassistant/components/ondilo_ico/api.py +++ b/homeassistant/components/ondilo_ico/api.py @@ -18,7 +18,7 @@ class OndiloClient(Ondilo): hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, - ): + ) -> None: """Initialize Ondilo ICO Auth.""" self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/components/ondilo_ico/oauth_impl.py b/homeassistant/components/ondilo_ico/oauth_impl.py index d6072cd6f6f..e1c6e6fdb90 100644 --- a/homeassistant/components/ondilo_ico/oauth_impl.py +++ b/homeassistant/components/ondilo_ico/oauth_impl.py @@ -15,7 +15,7 @@ from .const import ( class OndiloOauth2Implementation(LocalOAuth2Implementation): """Local implementation of OAuth2 specific to Ondilo to hard code client id and secret and return a proper name.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Just init default class with default values.""" super().__init__( hass, diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 3af2bb7c326..2428862cb31 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -89,7 +89,7 @@ class OndiloICO(CoordinatorEntity, SensorEntity): def __init__( self, coordinator: DataUpdateCoordinator, poolidx: int, sensor_idx: int - ): + ) -> None: """Initialize sensor entity with data from coordinator.""" super().__init__(coordinator) diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index 0182f86f6de..b60d06739e8 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -33,7 +33,7 @@ class OneWireBaseEntity(Entity): device_info: DeviceInfo, default_disabled: bool, unique_id: str, - ): + ) -> None: """Initialize the entity.""" self._name = f"{name} {entity_name or entity_type.capitalize()}" self._device_file = device_file @@ -88,7 +88,7 @@ class OneWireProxyEntity(OneWireBaseEntity): entity_path: str, entity_specs: DeviceComponentDescription, owproxy: protocol._Proxy, - ): + ) -> None: """Initialize the sensor.""" super().__init__( name=device_name, diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 68ee0af85aa..26d08594055 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -23,7 +23,7 @@ DEVICE_COUPLERS = { class OneWireHub: """Hub to communicate with SysBus or OWServer.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize.""" self.hass = hass self.type: str | None = None diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 826ff4b1a29..e8428696bfc 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -41,7 +41,7 @@ from .models import PTZ, Capabilities, DeviceInfo, Profile, Resolution, Video class ONVIFDevice: """Manages an ONVIF device.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None) -> None: """Initialize the device.""" self.hass: HomeAssistant = hass self.config_entry: ConfigEntry = config_entry diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 76b18d729a8..a45cc02c84b 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -29,7 +29,9 @@ SUBSCRIPTION_ERRORS = ( class EventManager: """ONVIF Event Manager.""" - def __init__(self, hass: HomeAssistant, device: ONVIFCamera, unique_id: str): + def __init__( + self, hass: HomeAssistant, device: ONVIFCamera, unique_id: str + ) -> None: """Initialize event manager.""" self.hass: HomeAssistant = hass self.device: ONVIFCamera = device diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index adc62906e65..7615a7011d3 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -80,7 +80,7 @@ class OVOEnergySensor(OVOEnergyDeviceEntity, SensorEntity): class OVOEnergyLastElectricityReading(OVOEnergySensor): """Defines a OVO Energy last reading sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, client: OVOEnergy): + def __init__(self, coordinator: DataUpdateCoordinator, client: OVOEnergy) -> None: """Initialize OVO Energy sensor.""" super().__init__( @@ -115,7 +115,7 @@ class OVOEnergyLastElectricityReading(OVOEnergySensor): class OVOEnergyLastGasReading(OVOEnergySensor): """Defines a OVO Energy last reading sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, client: OVOEnergy): + def __init__(self, coordinator: DataUpdateCoordinator, client: OVOEnergy) -> None: """Initialize OVO Energy sensor.""" super().__init__( @@ -152,7 +152,7 @@ class OVOEnergyLastElectricityCost(OVOEnergySensor): def __init__( self, coordinator: DataUpdateCoordinator, client: OVOEnergy, currency: str - ): + ) -> None: """Initialize OVO Energy sensor.""" super().__init__( coordinator, @@ -188,7 +188,7 @@ class OVOEnergyLastGasCost(OVOEnergySensor): def __init__( self, coordinator: DataUpdateCoordinator, client: OVOEnergy, currency: str - ): + ) -> None: """Initialize OVO Energy sensor.""" super().__init__( coordinator, diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 86f50367fd4..7641a75e9c6 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -168,7 +168,7 @@ class PersonStorageCollection(collection.StorageCollection): logger: logging.Logger, id_manager: collection.IDManager, yaml_collection: collection.YamlCollection, - ): + ) -> None: """Initialize a person storage collection.""" super().__init__(store, logger, id_manager) self.yaml_collection = yaml_collection diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index cc78402dda9..f21337f512e 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -60,7 +60,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class PluggableAction: """A pluggable action handler.""" - def __init__(self, update: Callable[[], None]): + def __init__(self, update: Callable[[], None]) -> None: """Initialize.""" self._update = update self._actions: dict[Any, AutomationActionType] = {} diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 60862d8eded..61aa97a66b1 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -128,7 +128,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): coordinator: PhilipsTVDataUpdateCoordinator, system: dict[str, Any], unique_id: str, - ): + ) -> None: """Initialize the Philips TV.""" self._tv = coordinator.api self._coordinator = coordinator diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index f4d34904f1b..98ecab96fd4 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -37,7 +37,7 @@ class PhilipsTVRemote(RemoteEntity): coordinator: PhilipsTVDataUpdateCoordinator, system: SystemType, unique_id: str, - ): + ) -> None: """Initialize the Philips TV.""" self._tv = coordinator.api self._coordinator = coordinator diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index a4660344aaf..bcd4e79a098 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -24,7 +24,7 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): hass: HomeAssistant, picnic_api_client: PicnicAPI, config_entry: ConfigEntry, - ): + ) -> None: """Initialize the coordinator with the given Picnic API client.""" self.picnic_api_client = picnic_api_client self.config_entry = config_entry diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index b1d81db1de5..79a4657c312 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -171,7 +171,7 @@ class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class PlaatoOptionsFlowHandler(config_entries.OptionsFlow): """Handle Plaato options.""" - def __init__(self, config_entry: ConfigEntry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize domain options flow.""" super().__init__() diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index b664ebcd45d..ee03d042ec2 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -96,7 +96,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Rachio.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py index 1a53eb78d5e..9919c2653a8 100644 --- a/homeassistant/components/recollect_waste/config_flow.py +++ b/homeassistant/components/recollect_waste/config_flow.py @@ -73,7 +73,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class RecollectWasteOptionsFlowHandler(config_entries.OptionsFlow): """Handle a Recollect Waste options flow.""" - def __init__(self, entry: config_entries.ConfigEntry): + def __init__(self, entry: config_entries.ConfigEntry) -> None: """Initialize.""" self._entry = entry diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index a0d07d0a878..a8196b30302 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -146,7 +146,7 @@ class GlobalDataUpdater: ring: Ring, update_method: str, update_interval: timedelta, - ): + ) -> None: """Initialize global data updater.""" self.hass = hass self.data_type = data_type @@ -219,7 +219,7 @@ class DeviceDataUpdater: ring: Ring, update_method: str, update_interval: timedelta, - ): + ) -> None: """Initialize device data updater.""" self.data_type = data_type self.hass = hass diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 1906478cbd2..65c1a2dd97c 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -64,7 +64,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class RitualsDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Rituals Perufme Genie device data from single endpoint.""" - def __init__(self, hass: HomeAssistant, device: Diffuser): + def __init__(self, hass: HomeAssistant, device: Diffuser) -> None: """Initialize global Rituals Perufme Genie data updater.""" self._device = device super().__init__( diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 6b7f3237d1e..e81f5260ac1 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -85,7 +85,7 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): hass: HomeAssistant, *, host: str, - ): + ) -> None: """Initialize global Roku data updater.""" self.roku = Roku(host=host, session=async_get_clientsession(hass)) diff --git a/homeassistant/components/ruckus_unleashed/coordinator.py b/homeassistant/components/ruckus_unleashed/coordinator.py index 1e145a19c74..8b80eaae0da 100644 --- a/homeassistant/components/ruckus_unleashed/coordinator.py +++ b/homeassistant/components/ruckus_unleashed/coordinator.py @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__package__) class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator): """Coordinator to manage data from Ruckus Unleashed client.""" - def __init__(self, hass: HomeAssistant, *, ruckus: Ruckus): + def __init__(self, hass: HomeAssistant, *, ruckus: Ruckus) -> None: """Initialize global Ruckus Unleashed data updater.""" self.ruckus = ruckus diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index c9b13a586ef..1fc01a2e854 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -188,7 +188,7 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ScreenLogicOptionsFlowHandler(config_entries.OptionsFlow): """Handles the options for the ScreenLogic integration.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Init the screen logic options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index 3198f40720b..db97410469f 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -69,7 +69,7 @@ class Searcher: hass: HomeAssistant, device_reg: device_registry.DeviceRegistry, entity_reg: entity_registry.EntityRegistry, - ): + ) -> None: """Search results.""" self.hass = hass self._device_reg = device_reg diff --git a/homeassistant/components/sentry/config_flow.py b/homeassistant/components/sentry/config_flow.py index bcb540a1687..4fe2b0cc503 100644 --- a/homeassistant/components/sentry/config_flow.py +++ b/homeassistant/components/sentry/config_flow.py @@ -73,7 +73,7 @@ class SentryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class SentryOptionsFlow(config_entries.OptionsFlow): """Handle Sentry options.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Sentry options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 09dc4ec3efb..4ecdf217abc 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -83,7 +83,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): """Shark IQ vacuum entity.""" - def __init__(self, sharkiq: SharkIqVacuum, coordinator: SharkIqUpdateCoordinator): + def __init__( + self, sharkiq: SharkIqVacuum, coordinator: SharkIqUpdateCoordinator + ) -> None: """Create a new SharkVacuumEntity.""" super().__init__(coordinator) self.sharkiq = sharkiq diff --git a/homeassistant/components/smappee/api.py b/homeassistant/components/smappee/api.py index 8de1d4cfec5..f12f53ff27f 100644 --- a/homeassistant/components/smappee/api.py +++ b/homeassistant/components/smappee/api.py @@ -18,7 +18,7 @@ class ConfigEntrySmappeeApi(api.SmappeeApi): hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, - ): + ) -> None: """Initialize Smappee Auth.""" self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index 1fc3eb218a7..82a8ccb354b 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -86,7 +86,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): class SmartMeterTexasData: """Manages coordinatation of API data updates.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry, account: Account): + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, account: Account + ) -> None: """Initialize the data coordintator.""" self._entry = entry self.account = account diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index 54e5969f8ea..13e93fe362b 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): """Representation of an Smart Meter Texas sensor.""" - def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator): + def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.meter = meter diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py index 43db2c29060..bfa834cddb2 100644 --- a/homeassistant/components/somfy/api.py +++ b/homeassistant/components/somfy/api.py @@ -17,7 +17,7 @@ class ConfigEntrySomfyApi(somfy_api.SomfyApi): hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, - ): + ) -> None: """Initialize the Config Entry Somfy API.""" self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index 88185629b27..aac04a34a9a 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -119,7 +119,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for somfy_mylink.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self.options = deepcopy(dict(config_entry.options)) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index a30868fe913..e9ae2367273 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -233,7 +233,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): me: dict, user_id: str, name: str, - ): + ) -> None: """Initialize.""" self._id = user_id self._me = me diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index fc7be6582a7..8af9940370e 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -26,7 +26,7 @@ from .const import ( class StarlineAccount: """StarLine Account class.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize StarLine account.""" self._hass: HomeAssistant = hass self._config_entry: ConfigEntry = config_entry diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py index df9bd348b8d..3468d141cf6 100644 --- a/homeassistant/components/starline/binary_sensor.py +++ b/homeassistant/components/starline/binary_sensor.py @@ -43,7 +43,7 @@ class StarlineSensor(StarlineEntity, BinarySensorEntity): key: str, name: str, device_class: str, - ): + ) -> None: """Initialize sensor.""" super().__init__(account, device, key, name) self._device_class = device_class diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index 59b9f5b4f95..04e18bce8fb 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): """StarLine device tracker.""" - def __init__(self, account: StarlineAccount, device: StarlineDevice): + def __init__(self, account: StarlineAccount, device: StarlineDevice) -> None: """Set up StarLine entity.""" super().__init__(account, device, "location", "Location") diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py index 9b81481b9d1..b48816e1a7c 100644 --- a/homeassistant/components/starline/entity.py +++ b/homeassistant/components/starline/entity.py @@ -13,7 +13,7 @@ class StarlineEntity(Entity): def __init__( self, account: StarlineAccount, device: StarlineDevice, key: str, name: str - ): + ) -> None: """Initialize StarLine entity.""" self._account = account self._device = device diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index f19fa4896ba..8ed6de0497d 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class StarlineLock(StarlineEntity, LockEntity): """Representation of a StarLine lock.""" - def __init__(self, account: StarlineAccount, device: StarlineDevice): + def __init__(self, account: StarlineAccount, device: StarlineDevice) -> None: """Initialize the lock.""" super().__init__(account, device, "lock", "Security") diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 29deacee428..4cb470be894 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -49,7 +49,7 @@ class StarlineSensor(StarlineEntity, SensorEntity): device_class: str, unit: str, icon: str, - ): + ) -> None: """Initialize StarLine sensor.""" super().__init__(account, device, key, name) self._device_class = device_class diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py index b3214390a44..c8afc41cb2d 100644 --- a/homeassistant/components/starline/switch.py +++ b/homeassistant/components/starline/switch.py @@ -41,7 +41,7 @@ class StarlineSwitch(StarlineEntity, SwitchEntity): name: str, icon_on: str, icon_off: str, - ): + ) -> None: """Initialize the switch.""" super().__init__(account, device, key, name) self._icon_on = icon_on diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index ae96d80af0b..a289464f92b 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -53,7 +53,7 @@ class IdleTimer: def __init__( self, hass: HomeAssistant, timeout: int, idle_callback: Callable[[], None] - ): + ) -> None: """Initialize IdleTimer.""" self._hass = hass self._timeout = timeout diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py index 0c41a478c04..f21abbdb56f 100644 --- a/homeassistant/components/subaru/config_flow.py +++ b/homeassistant/components/subaru/config_flow.py @@ -133,7 +133,7 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Subaru.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 5f6a82839e1..fd75264ee27 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -62,7 +62,7 @@ class SurePetcareBinarySensor(BinarySensorEntity): spc: SurePetcareAPI, device_class: str, sure_type: EntityType, - ): + ) -> None: """Initialize a Sure Petcare binary sensor.""" self._id = _id diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index 33396e25267..cfdb25fd412 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -49,7 +49,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class SurePetcareSensor(SensorEntity): """A binary sensor implementation for Sure Petcare Entities.""" - def __init__(self, _id: int, spc: SurePetcareAPI): + def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare sensor.""" self._id = _id diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 85d1cb4bf7a..c2de05b833e 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -275,7 +275,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): class SynologyDSMOptionsFlowHandler(OptionsFlow): """Handle a option flow.""" - def __init__(self, config_entry: ConfigEntry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index e18bcf516eb..488aca90bde 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -53,7 +53,7 @@ class BridgeBinarySensor(BridgeDeviceEntity, BinarySensorEntity): class BridgeBatteryIsChargingBinarySensor(BridgeBinarySensor): """Defines a Battery is charging binary sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge): + def __init__(self, coordinator: DataUpdateCoordinator, bridge: Bridge) -> None: """Initialize System Bridge binary sensor.""" super().__init__( coordinator, From c650deef98a50c6423e7b65a39d3ea8d52014046 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 20 May 2021 16:56:11 +0200 Subject: [PATCH 602/852] Add base class for all modbus platforms (#50878) * Add base for all platforms. * Please pylint. --- .../components/modbus/base_platform.py | 67 +++++++++++++++++++ .../components/modbus/binary_sensor.py | 49 ++------------ homeassistant/components/modbus/climate.py | 38 +++-------- homeassistant/components/modbus/cover.py | 47 +++---------- homeassistant/components/modbus/sensor.py | 46 ++----------- homeassistant/components/modbus/switch.py | 33 ++------- 6 files changed, 98 insertions(+), 182 deletions(-) create mode 100644 homeassistant/components/modbus/base_platform.py diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py new file mode 100644 index 00000000000..ddfe11717de --- /dev/null +++ b/homeassistant/components/modbus/base_platform.py @@ -0,0 +1,67 @@ +"""Base implementation for all modbus platforms.""" +from __future__ import annotations + +from abc import abstractmethod +from datetime import timedelta +import logging +from typing import Any + +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DEVICE_CLASS, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_SLAVE, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval + +from .const import CONF_INPUT_TYPE +from .modbus import ModbusHub + +PARALLEL_UPDATES = 1 +_LOGGER = logging.getLogger(__name__) + + +class BasePlatform(Entity): + """Base for readonly platforms.""" + + def __init__(self, hub: ModbusHub, entry: dict[str, Any]) -> None: + """Initialize the Modbus binary sensor.""" + self._hub = hub + self._name = entry[CONF_NAME] + self._slave = entry.get(CONF_SLAVE) + self._address = int(entry[CONF_ADDRESS]) + self._device_class = entry.get(CONF_DEVICE_CLASS) + self._input_type = entry[CONF_INPUT_TYPE] + self._value = None + self._available = True + self._scan_interval = timedelta(seconds=entry[CONF_SCAN_INTERVAL]) + + @abstractmethod + async def async_update(self, now=None): + """Virtual function to be overwritten.""" + + async def async_base_added_to_hass(self): + """Handle entity which will be added.""" + async_track_time_interval(self.hass, self.async_update, self._scan_interval) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + @property + def device_class(self) -> str | None: + """Return the device class of the sensor.""" + return self._device_class + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 3605b623c3c..045447f7246 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,7 +1,6 @@ """Support for Modbus Coil and Discrete Input sensors.""" from __future__ import annotations -from datetime import timedelta import logging import voluptuous as vol @@ -21,9 +20,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .base_platform import BasePlatform from .const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, @@ -92,65 +91,25 @@ async def async_setup_platform( hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] if CONF_SCAN_INTERVAL not in entry: entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL - sensors.append(ModbusBinarySensor(hub, hass, entry)) + sensors.append(ModbusBinarySensor(hub, entry)) async_add_entities(sensors) -class ModbusBinarySensor(BinarySensorEntity): +class ModbusBinarySensor(BasePlatform, BinarySensorEntity): """Modbus binary sensor.""" - def __init__(self, hub, hass, entry): - """Initialize the Modbus binary sensor.""" - self._hub = hub - self._hass = hass - self._name = entry[CONF_NAME] - self._slave = entry.get(CONF_SLAVE) - self._address = int(entry[CONF_ADDRESS]) - self._device_class = entry.get(CONF_DEVICE_CLASS) - self._input_type = entry[CONF_INPUT_TYPE] - self._value = None - self._available = True - self._scan_interval = timedelta(seconds=entry[CONF_SCAN_INTERVAL]) - async def async_added_to_hass(self): """Handle entity which will be added.""" - async_track_time_interval(self._hass, self.async_update, self._scan_interval) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name + await self.async_base_added_to_hass() @property def is_on(self): """Return the state of the sensor.""" return self._value - @property - def device_class(self) -> str | None: - """Return the device class of the sensor.""" - return self._device_class - - @property - def should_poll(self): - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - - # Handle polling directly in this entity - return False - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - async def async_update(self, now=None): """Update the state of the sensor.""" - # remark "now" is a dummy parameter to avoid problems with - # async_track_time_interval result = await self._hub.async_pymodbus_call( self._slave, self._address, 1, self._input_type ) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index a8a3f2a8557..d23146bd2ba 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -1,7 +1,6 @@ """Support for Generic Modbus Thermostats.""" from __future__ import annotations -from datetime import timedelta import logging import struct from typing import Any @@ -12,19 +11,18 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( + CONF_ADDRESS, CONF_NAME, CONF_OFFSET, - CONF_SCAN_INTERVAL, - CONF_SLAVE, CONF_STRUCTURE, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .base_platform import BasePlatform from .const import ( ATTR_TEMPERATURE, CALL_TYPE_REGISTER_HOLDING, @@ -34,6 +32,7 @@ from .const import ( CONF_CURRENT_TEMP_REGISTER_TYPE, CONF_DATA_COUNT, CONF_DATA_TYPE, + CONF_INPUT_TYPE, CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_PRECISION, @@ -99,7 +98,7 @@ async def async_setup_platform( async_add_entities(entities) -class ModbusThermostat(ClimateEntity): +class ModbusThermostat(BasePlatform, ClimateEntity): """Representation of a Modbus Thermostat.""" def __init__( @@ -108,9 +107,9 @@ class ModbusThermostat(ClimateEntity): config: dict[str, Any], ) -> None: """Initialize the modbus thermostat.""" - self._hub: ModbusHub = hub - self._name = config[CONF_NAME] - self._slave = config.get(CONF_SLAVE) + config[CONF_ADDRESS] = "0" + config[CONF_INPUT_TYPE] = "" + super().__init__(hub, config) self._target_temperature_register = config[CONF_TARGET_TEMP] self._current_temperature_register = config[CONF_CURRENT_TEMP] self._current_temperature_register_type = config[ @@ -123,26 +122,15 @@ class ModbusThermostat(ClimateEntity): self._count = config[CONF_DATA_COUNT] self._precision = config[CONF_PRECISION] self._scale = config[CONF_SCALE] - self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) self._offset = config[CONF_OFFSET] self._unit = config[CONF_TEMPERATURE_UNIT] self._max_temp = config[CONF_MAX_TEMP] self._min_temp = config[CONF_MIN_TEMP] self._temp_step = config[CONF_STEP] - self._available = True async def async_added_to_hass(self): """Handle entity which will be added.""" - async_track_time_interval(self.hass, self.async_update, self._scan_interval) - - @property - def should_poll(self): - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - # Handle polling directly in this entity - return False + await self.async_base_added_to_hass() @property def supported_features(self): @@ -164,11 +152,6 @@ class ModbusThermostat(ClimateEntity): # Home Assistant expects this method. # We'll keep it here to avoid getting exceptions. - @property - def name(self): - """Return the name of the climate device.""" - return self._name - @property def current_temperature(self): """Return the current temperature.""" @@ -217,11 +200,6 @@ class ModbusThermostat(ClimateEntity): self._available = result is not None await self.async_update() - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - async def async_update(self, now=None): """Update Target & Current Temperature.""" # remark "now" is a dummy parameter to avoid problems with diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index e04d4bd8ce3..f6e1b21b0cf 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -1,17 +1,14 @@ """Support for Modbus covers.""" from __future__ import annotations -from datetime import timedelta import logging from typing import Any from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity from homeassistant.const import ( + CONF_ADDRESS, CONF_COVERS, - CONF_DEVICE_CLASS, CONF_NAME, - CONF_SCAN_INTERVAL, - CONF_SLAVE, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, @@ -20,15 +17,16 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .base_platform import BasePlatform from .const import ( CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_WRITE_COIL, CALL_TYPE_WRITE_REGISTER, + CONF_INPUT_TYPE, CONF_REGISTER, CONF_STATE_CLOSED, CONF_STATE_CLOSING, @@ -67,7 +65,7 @@ async def async_setup_platform( async_add_entities(covers) -class ModbusCover(CoverEntity, RestoreEntity): +class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): """Representation of a Modbus cover.""" def __init__( @@ -76,21 +74,17 @@ class ModbusCover(CoverEntity, RestoreEntity): config: dict[str, Any], ) -> None: """Initialize the modbus cover.""" - self._hub: ModbusHub = hub + config[CONF_ADDRESS] = "0" + config[CONF_INPUT_TYPE] = "" + super().__init__(hub, config) self._coil = config.get(CALL_TYPE_COIL) - self._device_class = config.get(CONF_DEVICE_CLASS) - self._name = config[CONF_NAME] self._register = config.get(CONF_REGISTER) - self._slave = config.get(CONF_SLAVE) self._state_closed = config[CONF_STATE_CLOSED] self._state_closing = config[CONF_STATE_CLOSING] self._state_open = config[CONF_STATE_OPEN] self._state_opening = config[CONF_STATE_OPENING] self._status_register = config.get(CONF_STATUS_REGISTER) self._status_register_type = config[CONF_STATUS_REGISTER_TYPE] - self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) - self._value = None - self._available = True # If we read cover status from coil, and not from optional status register, # we interpret boolean value False as closed cover, and value True as open cover. @@ -114,6 +108,7 @@ class ModbusCover(CoverEntity, RestoreEntity): async def async_added_to_hass(self): """Handle entity which will be added.""" + await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: convert = { @@ -126,28 +121,11 @@ class ModbusCover(CoverEntity, RestoreEntity): } self._value = convert[state.state] - async_track_time_interval(self.hass, self.async_update, self._scan_interval) - - @property - def device_class(self) -> str | None: - """Return the device class of the sensor.""" - return self._device_class - - @property - def name(self): - """Return the name of the switch.""" - return self._name - @property def supported_features(self): """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - @property def is_opening(self): """Return if the cover is opening or not.""" @@ -163,15 +141,6 @@ class ModbusCover(CoverEntity, RestoreEntity): """Return if the cover is closed or not.""" return self._value == self._state_closed - @property - def should_poll(self): - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - # Handle polling directly in this entity - return False - async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" result = await self._hub.async_pymodbus_call( diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 614925b79a6..85bb591711c 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -1,7 +1,6 @@ """Support for Modbus Register sensors.""" from __future__ import annotations -from datetime import timedelta import logging import struct @@ -26,11 +25,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import number +from .base_platform import BasePlatform from .const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, @@ -194,7 +193,7 @@ async def async_setup_platform( async_add_entities(sensors) -class ModbusRegisterSensor(RestoreEntity, SensorEntity): +class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity): """Modbus register sensor.""" def __init__( @@ -204,12 +203,9 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): structure, ): """Initialize the modbus register sensor.""" - self._hub = hub - self._name = entry[CONF_NAME] - slave = entry.get(CONF_SLAVE) - self._slave = int(slave) if slave else None - self._register = int(entry[CONF_ADDRESS]) - self._register_type = entry[CONF_INPUT_TYPE] + super().__init__(hub, entry) + self._register = self._address + self._register_type = self._input_type self._unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._count = int(entry[CONF_COUNT]) self._swap = entry[CONF_SWAP] @@ -218,54 +214,24 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): self._precision = entry[CONF_PRECISION] self._structure = structure self._data_type = entry[CONF_DATA_TYPE] - self._device_class = entry.get(CONF_DEVICE_CLASS) - self._value = None - self._available = True - self._scan_interval = timedelta(seconds=entry.get(CONF_SCAN_INTERVAL)) async def async_added_to_hass(self): """Handle entity which will be added.""" + await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: self._value = state.state - async_track_time_interval(self.hass, self.async_update, self._scan_interval) - @property def state(self): """Return the state of the sensor.""" return self._value - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def should_poll(self): - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - - # Handle polling directly in this entity - return False - @property def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement - @property - def device_class(self) -> str | None: - """Return the device class of the sensor.""" - return self._device_class - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - def _swap_registers(self, registers): """Do swap as needed.""" if self._swap in [CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE]: diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 1b0cd37eb87..ef068d7bd18 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -1,7 +1,6 @@ """Support for Modbus switches.""" from __future__ import annotations -from datetime import timedelta import logging from homeassistant.components.switch import SwitchEntity @@ -10,16 +9,14 @@ from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_NAME, - CONF_SCAN_INTERVAL, - CONF_SLAVE, CONF_SWITCHES, STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from .base_platform import BasePlatform from .const import ( CALL_TYPE_COIL, CALL_TYPE_WRITE_COIL, @@ -49,18 +46,14 @@ async def async_setup_platform( async_add_entities(switches) -class ModbusSwitch(SwitchEntity, RestoreEntity): +class ModbusSwitch(BasePlatform, SwitchEntity, RestoreEntity): """Base class representing a Modbus switch.""" def __init__(self, hub: ModbusHub, config: dict) -> None: """Initialize the switch.""" - self._hub: ModbusHub = hub - self._name = config[CONF_NAME] - self._slave = config.get(CONF_SLAVE) + config[CONF_INPUT_TYPE] = "" + super().__init__(hub, config) self._is_on = None - self._available = True - self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) - self._address = config[CONF_ADDRESS] if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: self._write_type = CALL_TYPE_WRITE_COIL else: @@ -84,32 +77,16 @@ class ModbusSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self): """Handle entity which will be added.""" + await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: self._is_on = state.state == STATE_ON - async_track_time_interval(self.hass, self.async_update, self._scan_interval) - @property def is_on(self): """Return true if switch is on.""" return self._is_on - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - async def async_turn_on(self, **kwargs): """Set switch on.""" From 9eecd90afcb7d2797a839ee1279535b667bc494f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 20 May 2021 17:00:19 +0200 Subject: [PATCH 603/852] Add constructor return type in integrations T-Z (#50899) --- homeassistant/components/tado/config_flow.py | 2 +- homeassistant/components/tag/__init__.py | 2 +- homeassistant/components/template/sensor.py | 2 +- .../components/template/template_entity.py | 2 +- .../components/template/trigger_entity.py | 2 +- homeassistant/components/tesla/config_flow.py | 2 +- homeassistant/components/timer/__init__.py | 2 +- homeassistant/components/toon/coordinator.py | 2 +- homeassistant/components/toon/oauth2.py | 2 +- homeassistant/components/tplink/common.py | 2 +- homeassistant/components/tplink/switch.py | 2 +- homeassistant/components/trace/__init__.py | 2 +- homeassistant/components/tuya/config_flow.py | 4 ++-- homeassistant/components/twinkly/light.py | 2 +- .../components/upc_connect/device_tracker.py | 2 +- .../components/upcloud/config_flow.py | 2 +- homeassistant/components/updater/__init__.py | 4 +++- homeassistant/components/vera/__init__.py | 4 +++- .../components/vera/binary_sensor.py | 2 +- homeassistant/components/vera/climate.py | 2 +- homeassistant/components/vera/config_flow.py | 2 +- homeassistant/components/vera/cover.py | 2 +- homeassistant/components/vera/light.py | 2 +- homeassistant/components/vera/lock.py | 4 +++- homeassistant/components/vera/scene.py | 4 +++- homeassistant/components/vera/sensor.py | 2 +- homeassistant/components/vera/switch.py | 2 +- homeassistant/components/verisure/camera.py | 2 +- homeassistant/components/version/sensor.py | 4 ++-- homeassistant/components/wemo/__init__.py | 2 +- homeassistant/components/withings/common.py | 4 ++-- homeassistant/components/wled/__init__.py | 2 +- homeassistant/components/wled/light.py | 4 ++-- homeassistant/components/wled/switch.py | 2 +- .../components/wunderground/sensor.py | 20 +++++++++++-------- homeassistant/components/xbox/api.py | 2 +- homeassistant/components/xbox/base_sensor.py | 4 +++- homeassistant/components/xbox/media_source.py | 2 +- .../components/yamaha/media_player.py | 2 +- homeassistant/components/yeelight/__init__.py | 4 ++-- .../components/zha/core/channels/__init__.py | 2 +- homeassistant/components/zha/core/const.py | 2 +- homeassistant/components/zha/core/device.py | 2 +- homeassistant/components/zha/core/group.py | 4 ++-- homeassistant/components/zha/cover.py | 2 +- homeassistant/components/zha/entity.py | 4 ++-- homeassistant/components/zha/sensor.py | 2 +- homeassistant/components/zone/__init__.py | 2 +- homeassistant/components/zwave_js/services.py | 2 +- 49 files changed, 77 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index 7359273e2de..d762329d658 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -106,7 +106,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for tado.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 6dcf2ec9a4d..4a410c6b30b 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -41,7 +41,7 @@ UPDATE_FIELDS = { class TagIDExistsError(HomeAssistantError): """Raised when an item is not found.""" - def __init__(self, item_id: str): + def __init__(self, item_id: str) -> None: """Initialize tag ID exists error.""" super().__init__(f"Tag with ID {item_id} already exists.") self.item_id = item_id diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index d470964465a..56e0e11edb0 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -224,7 +224,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): device_class: str | None, attribute_templates: dict[str, template.Template], unique_id: str | None, - ): + ) -> None: """Initialize the sensor.""" super().__init__( attribute_templates=attribute_templates, diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 4f72511fe24..522eb7d89ba 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -33,7 +33,7 @@ class _TemplateAttribute: validator: Callable[[Any], Any] = None, on_update: Callable[[Any], None] | None = None, none_on_template_error: bool | None = False, - ): + ) -> None: """Template attribute.""" self._entity = entity self._attribute = attribute diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index 4ba2a549e63..ee9c60293df 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -29,7 +29,7 @@ class TriggerEntity(update_coordinator.CoordinatorEntity): hass: HomeAssistant, coordinator: TriggerUpdateCoordinator, config: dict, - ): + ) -> None: """Initialize the entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index cbb25f8663b..89dc8d2a2b4 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -36,7 +36,7 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the tesla flow.""" self.username = None self.reauth = False diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index ded59a0a6d1..2e92ddf6fd8 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -188,7 +188,7 @@ class TimerStorageCollection(collection.StorageCollection): class Timer(RestoreEntity): """Representation of a timer.""" - def __init__(self, config: dict): + def __init__(self, config: dict) -> None: """Initialize a timer.""" self._config: dict = config self.editable: bool = True diff --git a/homeassistant/components/toon/coordinator.py b/homeassistant/components/toon/coordinator.py index 069bd58d922..9405de22bf8 100644 --- a/homeassistant/components/toon/coordinator.py +++ b/homeassistant/components/toon/coordinator.py @@ -27,7 +27,7 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator[Status]): def __init__( self, hass: HomeAssistant, *, entry: ConfigEntry, session: OAuth2Session - ): + ) -> None: """Initialize global Toon data updater.""" self.session = session self.entry = entry diff --git a/homeassistant/components/toon/oauth2.py b/homeassistant/components/toon/oauth2.py index 7539224ebba..64abeb26992 100644 --- a/homeassistant/components/toon/oauth2.py +++ b/homeassistant/components/toon/oauth2.py @@ -58,7 +58,7 @@ class ToonLocalOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implemen name: str, tenant_id: str, issuer: str | None = None, - ): + ) -> None: """Local Toon Oauth Implementation.""" self._name = name self.tenant_id = tenant_id diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 4129a80f83c..f9ed57d26fb 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -32,7 +32,7 @@ class SmartDevices: def __init__( self, lights: list[SmartDevice] = None, switches: list[SmartDevice] = None - ): + ) -> None: """Initialize device holder.""" self._lights = lights or [] self._switches = switches or [] diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index ab8b3290b30..011caa463b2 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie class SmartPlugSwitch(SwitchEntity): """Representation of a TPLink Smart Plug switch.""" - def __init__(self, smartplug: SmartPlug): + def __init__(self, smartplug: SmartPlug) -> None: """Initialize the switch.""" self.smartplug = smartplug self._sysinfo = None diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index bf78f754b86..c1113467661 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -60,7 +60,7 @@ class ActionTrace: config: dict[str, Any], blueprint_inputs: dict[str, Any], context: Context, - ): + ) -> None: """Container for script trace.""" self._trace: dict[str, deque[TraceElement]] | None = None self._config: dict[str, Any] = config diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 1004a7844aa..ee17715b5e9 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -83,7 +83,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize flow.""" self._country_code = None self._password = None @@ -151,7 +151,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Tuya.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self._conf_devs_id = None diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 0c03ceaf74a..b0f94a1c52f 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -47,7 +47,7 @@ class TwinklyLight(LightEntity): self, conf: ConfigEntry, hass: HomeAssistant, - ): + ) -> None: """Initialize a TwinklyLight entity.""" self._id = conf.data[CONF_ENTRY_ID] self._hass = hass diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index 2a2a1b3798b..8a03b283236 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -54,7 +54,7 @@ async def async_get_scanner(hass, config): class UPCDeviceScanner(DeviceScanner): """This class queries a router running UPC ConnectBox firmware.""" - def __init__(self, connect_box: ConnectBox): + def __init__(self, connect_box: ConnectBox) -> None: """Initialize the scanner.""" self.connect_box: ConnectBox = connect_box diff --git a/homeassistant/components/upcloud/config_flow.py b/homeassistant/components/upcloud/config_flow.py index 3ad5976df1f..1a16a78cfa1 100644 --- a/homeassistant/components/upcloud/config_flow.py +++ b/homeassistant/components/upcloud/config_flow.py @@ -101,7 +101,7 @@ class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class UpCloudOptionsFlow(config_entries.OptionsFlow): """UpCloud options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 9e3c504e4be..f624cf87eda 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -44,7 +44,9 @@ RESPONSE_SCHEMA = vol.Schema( class Updater: """Updater class for data exchange.""" - def __init__(self, update_available: bool, newest_version: str, release_notes: str): + def __init__( + self, update_available: bool, newest_version: str, release_notes: str + ) -> None: """Initialize attributes.""" self.update_available = update_available self.release_notes = release_notes diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 9feba3cd08d..bf5eb9182e6 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -212,7 +212,9 @@ DeviceType = TypeVar("DeviceType", bound=veraApi.VeraDevice) class VeraDevice(Generic[DeviceType], Entity): """Representation of a Vera device entity.""" - def __init__(self, vera_device: DeviceType, controller_data: ControllerData): + def __init__( + self, vera_device: DeviceType, controller_data: ControllerData + ) -> None: """Initialize the device.""" self.vera_device = vera_device self.controller = controller_data.controller diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 1eca4f208f4..f41ce5cfd08 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -37,7 +37,7 @@ class VeraBinarySensor(VeraDevice[veraApi.VeraBinarySensor], BinarySensorEntity) def __init__( self, vera_device: veraApi.VeraBinarySensor, controller_data: ControllerData - ): + ) -> None: """Initialize the binary_sensor.""" self._state = False VeraDevice.__init__(self, vera_device, controller_data) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index c4c7cae8f85..cde36dcc623 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -56,7 +56,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): def __init__( self, vera_device: veraApi.VeraThermostat, controller_data: ControllerData - ): + ) -> None: """Initialize the Vera device.""" VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index a5450cd4a65..ae4fdf8cefa 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -67,7 +67,7 @@ def options_data(user_input: dict) -> dict: class OptionsFlowHandler(config_entries.OptionsFlow): """Options for the component.""" - def __init__(self, config_entry: ConfigEntry): + def __init__(self, config_entry: ConfigEntry) -> None: """Init object.""" self.config_entry = config_entry diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index 248465ed842..62a04d4e6f3 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -40,7 +40,7 @@ class VeraCover(VeraDevice[veraApi.VeraCurtain], CoverEntity): def __init__( self, vera_device: veraApi.VeraCurtain, controller_data: ControllerData - ): + ) -> None: """Initialize the Vera device.""" VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index 5cc19d78164..7f67f065c91 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -44,7 +44,7 @@ class VeraLight(VeraDevice[veraApi.VeraDimmer], LightEntity): def __init__( self, vera_device: veraApi.VeraDimmer, controller_data: ControllerData - ): + ) -> None: """Initialize the light.""" self._state = False self._color = None diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index 2c656cba5de..b99728d0fcf 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -41,7 +41,9 @@ async def async_setup_entry( class VeraLock(VeraDevice[veraApi.VeraLock], LockEntity): """Representation of a Vera lock.""" - def __init__(self, vera_device: veraApi.VeraLock, controller_data: ControllerData): + def __init__( + self, vera_device: veraApi.VeraLock, controller_data: ControllerData + ) -> None: """Initialize the Vera device.""" self._state = None VeraDevice.__init__(self, vera_device, controller_data) diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index 543ba3b517b..c1381f488dd 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -30,7 +30,9 @@ async def async_setup_entry( class VeraScene(Scene): """Representation of a Vera scene entity.""" - def __init__(self, vera_scene: veraApi.VeraScene, controller_data: ControllerData): + def __init__( + self, vera_scene: veraApi.VeraScene, controller_data: ControllerData + ) -> None: """Initialize the scene.""" self.vera_scene = vera_scene self.controller = controller_data.controller diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index e1e95820a2e..878f6ff376d 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -44,7 +44,7 @@ class VeraSensor(VeraDevice[veraApi.VeraSensor], SensorEntity): def __init__( self, vera_device: veraApi.VeraSensor, controller_data: ControllerData - ): + ) -> None: """Initialize the sensor.""" self.current_value = None self._temperature_units = None diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index 7531819d90d..304441037ec 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -40,7 +40,7 @@ class VeraSwitch(VeraDevice[veraApi.VeraSwitch], SwitchEntity): def __init__( self, vera_device: veraApi.VeraSwitch, controller_data: ControllerData - ): + ) -> None: """Initialize the Vera device.""" self._state = False VeraDevice.__init__(self, vera_device, controller_data) diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index f52a7a38e59..0dfda45999a 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -53,7 +53,7 @@ class VerisureSmartcam(CoordinatorEntity, Camera): coordinator: VerisureDataUpdateCoordinator, serial_number: str, directory_path: str, - ): + ) -> None: """Initialize Verisure File Camera component.""" super().__init__(coordinator) Camera.__init__(self) diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index d438f391334..04165ec9db1 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -111,7 +111,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class VersionData: """Get the latest data and update the states.""" - def __init__(self, api: HaVersion): + def __init__(self, api: HaVersion) -> None: """Initialize the data object.""" self.api = api @@ -131,7 +131,7 @@ class VersionData: class VersionSensor(SensorEntity): """Representation of a Home Assistant version sensor.""" - def __init__(self, data: VersionData, name: str): + def __init__(self, data: VersionData, name: str) -> None: """Initialize the Version sensor.""" self.data = data self._name = name diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index d7569a6329f..cb3beb9b67a 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -131,7 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): class WemoDispatcher: """Dispatch WeMo devices to the correct platform.""" - def __init__(self, config_entry: ConfigEntry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize the WemoDispatcher.""" self._config_entry = config_entry self._added_serial_numbers = set() diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index c2a91275d72..a187786c995 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -480,7 +480,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): hass: HomeAssistant, config_entry: ConfigEntry, implementation: AbstractOAuth2Implementation, - ): + ) -> None: """Initialize object.""" self._hass = hass self._config_entry = config_entry @@ -564,7 +564,7 @@ class DataManager: api: ConfigEntryWithingsApi, user_id: int, webhook_config: WebhookConfig, - ): + ) -> None: """Initialize the data manager.""" self._hass = hass self._api = api diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index f7b9a69d1de..717bb87e057 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -102,7 +102,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): hass: HomeAssistant, *, host: str, - ): + ) -> None: """Initialize global WLED data updater.""" self.wled = WLED(host, session=async_get_clientsession(hass)) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index f6a51e6159a..6c3b11a65e2 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -96,7 +96,7 @@ async def async_setup_entry( class WLEDMasterLight(LightEntity, WLEDDeviceEntity): """Defines a WLED master light.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator): + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED master light.""" super().__init__( entry_id=entry_id, @@ -177,7 +177,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): def __init__( self, entry_id: str, coordinator: WLEDDataUpdateCoordinator, segment: int - ): + ) -> None: """Initialize WLED segment light.""" self._rgbw = coordinator.data.info.leds.rgbw self._segment = segment diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index e08998308a6..ebd3f7826e6 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -135,7 +135,7 @@ class WLEDSyncSendSwitch(WLEDSwitch): class WLEDSyncReceiveSwitch(WLEDSwitch): """Defines a WLED sync receive switch.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator): + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync receive switch.""" super().__init__( coordinator=coordinator, diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 67eab97b4c3..887e2264a70 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -74,7 +74,7 @@ class WUSensorConfig: icon: str = "mdi:gauge", extra_state_attributes=None, device_class=None, - ): + ) -> None: """Initialize sensor configuration. :param friendly_name: Friendly name @@ -106,7 +106,7 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): icon: str | None = "mdi:gauge", unit_of_measurement: str | None = None, device_class=None, - ): + ) -> None: """Initialize current conditions sensor configuration. :param friendly_name: Friendly name of sensor @@ -133,7 +133,9 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): class WUDailyTextForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for daily text forecasts.""" - def __init__(self, period: int, field: str, unit_of_measurement: str | None = None): + def __init__( + self, period: int, field: str, unit_of_measurement: str | None = None + ) -> None: """Initialize daily text forecast sensor configuration. :param period: forecast period number @@ -170,7 +172,7 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): ha_unit: str | None = None, icon=None, device_class=None, - ): + ) -> None: """Initialize daily simple forecast sensor configuration. :param friendly_name: friendly_name of the sensor @@ -213,7 +215,7 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): class WUHourlyForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for hourly text forecasts.""" - def __init__(self, period: int, field: int): + def __init__(self, period: int, field: int) -> None: """Initialize hourly forecast sensor configuration. :param period: forecast period number @@ -280,7 +282,7 @@ class WUAlmanacSensorConfig(WUSensorConfig): unit_of_measurement: str, icon: str, device_class=None, - ): + ) -> None: """Initialize almanac sensor configuration. :param friendly_name: Friendly name @@ -303,7 +305,7 @@ class WUAlmanacSensorConfig(WUSensorConfig): class WUAlertsSensorConfig(WUSensorConfig): """Helper for defining field configuration for alerts.""" - def __init__(self, friendly_name: str | Callable): + def __init__(self, friendly_name: str | Callable) -> None: """Initialiize alerts sensor configuration. :param friendly_name: Friendly name @@ -1120,7 +1122,9 @@ async def async_setup_platform( class WUndergroundSensor(SensorEntity): """Implementing the WUnderground sensor.""" - def __init__(self, hass: HomeAssistant, rest, condition, unique_id_base: str): + def __init__( + self, hass: HomeAssistant, rest, condition, unique_id_base: str + ) -> None: """Initialize the sensor.""" self.rest = rest self._condition = condition diff --git a/homeassistant/components/xbox/api.py b/homeassistant/components/xbox/api.py index bb38b235c0c..04714afa33e 100644 --- a/homeassistant/components/xbox/api.py +++ b/homeassistant/components/xbox/api.py @@ -14,7 +14,7 @@ class AsyncConfigEntryAuth(AuthenticationManager): self, websession: ClientSession, oauth_session: config_entry_oauth2_flow.OAuth2Session, - ): + ) -> None: """Initialize xbox auth.""" # Leaving out client credentials as they are handled by Home Assistant super().__init__(websession, "", "", "") diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index c149ce74c32..894213fd94c 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -12,7 +12,9 @@ from .const import DOMAIN class XboxBaseSensorEntity(CoordinatorEntity): """Base Sensor for the Xbox Integration.""" - def __init__(self, coordinator: XboxUpdateCoordinator, xuid: str, attribute: str): + def __init__( + self, coordinator: XboxUpdateCoordinator, xuid: str, attribute: str + ) -> None: """Initialize Xbox binary sensor.""" super().__init__(coordinator) self.xuid = xuid diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index aeaa233a6ed..06581088823 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -74,7 +74,7 @@ class XboxSource(MediaSource): name: str = "Xbox Game Media" - def __init__(self, hass: HomeAssistant, client: XboxLiveClient): + def __init__(self, hass: HomeAssistant, client: XboxLiveClient) -> None: """Initialize Xbox source.""" super().__init__(DOMAIN) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 3f7df115015..3f79be43f6e 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -77,7 +77,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( class YamahaConfigInfo: """Configuration Info for Yamaha Receivers.""" - def __init__(self, config: None, discovery_info: None): + def __init__(self, config: None, discovery_info: None) -> None: """Initialize the Configuration Info for Yamaha Receiver.""" self.name = config.get(CONF_NAME) self.host = config.get(CONF_HOST) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 352dddb7705..b18f18b6fa4 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -316,7 +316,7 @@ class YeelightScanner: cls._scanner = cls(hass) return cls._scanner - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize class.""" self._hass = hass self._seen = {} @@ -573,7 +573,7 @@ class YeelightDevice: class YeelightEntity(Entity): """Represents single Yeelight entity.""" - def __init__(self, device: YeelightDevice, entry: ConfigEntry): + def __init__(self, device: YeelightDevice, entry: ConfigEntry) -> None: """Initialize the entity.""" self._device = device self._unique_id = entry.entry_id diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index e6d2d722f61..3661d3b17d9 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -176,7 +176,7 @@ class Channels: class ChannelPool: """All channels of an endpoint.""" - def __init__(self, channels: Channels, ep_id: int): + def __init__(self, channels: Channels, ep_id: int) -> None: """Initialize instance.""" self._all_channels: ChannelsDict = {} self._channels: Channels = channels diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 2110cc9546d..dcc2a080a76 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -258,7 +258,7 @@ class RadioType(enum.Enum): return radio.name raise ValueError - def __init__(self, description: str, controller_cls: CALLABLE_T): + def __init__(self, description: str, controller_cls: CALLABLE_T) -> None: """Init instance.""" self._desc = description self._ctrl_cls = controller_cls diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 6497a85b8f9..37608287609 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -91,7 +91,7 @@ class ZHADevice(LogMixin): hass: HomeAssistant, zigpy_device: zha_typing.ZigpyDeviceType, zha_gateway: zha_typing.ZhaGatewayType, - ): + ) -> None: """Initialize the gateway.""" self.hass = hass self._zigpy_device = zigpy_device diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 90dcb6fffc3..c8970b2d393 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -33,7 +33,7 @@ class ZHAGroupMember(LogMixin): def __init__( self, zha_group: ZhaGroupType, zha_device: ZhaDeviceType, endpoint_id: int - ): + ) -> None: """Initialize the group member.""" self._zha_group: ZhaGroupType = zha_group self._zha_device: ZhaDeviceType = zha_device @@ -116,7 +116,7 @@ class ZHAGroup(LogMixin): hass: HomeAssistant, zha_gateway: ZhaGatewayType, zigpy_group: ZigpyGroupType, - ): + ) -> None: """Initialize the group.""" self.hass: HomeAssistant = hass self._zigpy_group: ZigpyGroupType = zigpy_group diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 5530cd3e3f5..35080c56921 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -183,7 +183,7 @@ class Shade(ZhaEntity, CoverEntity): zha_device: ZhaDeviceType, channels: list[ChannelType], **kwargs, - ): + ) -> None: """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 860183a79b6..a5259deea5d 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -40,7 +40,7 @@ UPDATE_GROUP_FROM_CHILD_DELAY = 0.2 class BaseZhaEntity(LogMixin, entity.Entity): """A base class for ZHA entities.""" - def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs): + def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs) -> None: """Init ZHA entity.""" self._name: str = "" self._force_update: bool = False @@ -147,7 +147,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): zha_device: ZhaDeviceType, channels: list[ChannelType], **kwargs, - ): + ) -> None: """Init ZHA entity.""" super().__init__(unique_id, zha_device, **kwargs) ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 816db67816a..616e0345828 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -109,7 +109,7 @@ class Sensor(ZhaEntity, SensorEntity): zha_device: ZhaDeviceType, channels: list[ChannelType], **kwargs, - ): + ) -> None: """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._channel: ChannelType = channels[0] diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index b224c2a47d7..8ab0e9b2703 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -279,7 +279,7 @@ async def async_unload_entry( class Zone(entity.Entity): """Representation of a Zone.""" - def __init__(self, config: dict): + def __init__(self, config: dict) -> None: """Initialize the zone.""" self._config = config self.editable = True diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 16bf9c7eb94..48719063376 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -55,7 +55,7 @@ BITMASK_SCHEMA = vol.All( class ZWaveServices: """Class that holds our services (Zwave Commands) that should be published to hass.""" - def __init__(self, hass: HomeAssistant, ent_reg: EntityRegistry): + def __init__(self, hass: HomeAssistant, ent_reg: EntityRegistry) -> None: """Initialize with hass object.""" self._hass = hass self._ent_reg = ent_reg From d7c0da90c57a1dc71eb26034abaeabf2fae4b0d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 20 May 2021 17:02:25 +0200 Subject: [PATCH 604/852] Add support for DS2405 (#50148) --- homeassistant/components/onewire/switch.py | 8 ++++++++ tests/components/onewire/const.py | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index ed7b0df73a1..228c8f9d78b 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -19,6 +19,14 @@ from .onewirehub import OneWireHub DEVICE_SWITCHES: dict[str, list[DeviceComponentDescription]] = { # Family : { owfs path } + "05": [ + { + "path": "PIO", + "name": "PIO", + "type": SWITCH_TYPE_PIO, + "default_disabled": True, + }, + ], "12": [ { "path": "PIO.A", diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 57d54e0dcc3..1eb2b4b390a 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -31,6 +31,28 @@ MOCK_OWPROXY_DEVICES = { ], SENSOR_DOMAIN: [], }, + "05.111111111111": { + "inject_reads": [ + b"DS2405", # read device type + ], + "device_info": { + "identifiers": {(DOMAIN, "05.111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "DS2405", + "name": "05.111111111111", + }, + SWITCH_DOMAIN: [ + { + "entity_id": "switch.05_111111111111_pio", + "unique_id": "/05.111111111111/PIO", + "injected_value": b" 1", + "result": STATE_ON, + "unit": None, + "class": None, + "disabled": True, + }, + ], + }, "10.111111111111": { "inject_reads": [ b"DS18S20", # read device type From b1138b1aabd0afd02781a8fdc8b11789971f4a88 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 20 May 2021 17:47:30 +0200 Subject: [PATCH 605/852] Add constructor return type in integrations E-K (#50902) --- homeassistant/components/emonitor/sensor.py | 2 +- .../components/entur_public_transport/sensor.py | 4 +++- homeassistant/components/esphome/__init__.py | 4 ++-- homeassistant/components/esphome/camera.py | 2 +- homeassistant/components/fints/sensor.py | 2 +- homeassistant/components/firmata/board.py | 2 +- homeassistant/components/firmata/entity.py | 2 +- homeassistant/components/firmata/light.py | 2 +- homeassistant/components/firmata/pin.py | 10 +++++----- homeassistant/components/flick_electric/__init__.py | 2 +- homeassistant/components/flick_electric/sensor.py | 2 +- homeassistant/components/flo/device.py | 2 +- homeassistant/components/flo/entity.py | 2 +- homeassistant/components/flo/switch.py | 2 +- homeassistant/components/gogogate2/common.py | 2 +- homeassistant/components/google_assistant/helpers.py | 6 ++++-- homeassistant/components/gree/bridge.py | 2 +- homeassistant/components/guardian/switch.py | 2 +- homeassistant/components/guardian/util.py | 2 +- homeassistant/components/harmony/config_flow.py | 4 ++-- homeassistant/components/hassio/auth.py | 2 +- homeassistant/components/hassio/http.py | 2 +- homeassistant/components/hassio/ingress.py | 2 +- homeassistant/components/home_connect/api.py | 2 +- homeassistant/components/home_plus_control/helpers.py | 2 +- homeassistant/components/homekit/aidmanager.py | 2 +- homeassistant/components/homekit/config_flow.py | 4 ++-- homeassistant/components/huawei_lte/config_flow.py | 2 +- homeassistant/components/huisbaasje/sensor.py | 2 +- homeassistant/components/hyperion/config_flow.py | 2 +- homeassistant/components/iaqualink/__init__.py | 2 +- homeassistant/components/icloud/account.py | 4 ++-- homeassistant/components/icloud/device_tracker.py | 2 +- homeassistant/components/icloud/sensor.py | 2 +- homeassistant/components/image/__init__.py | 2 +- homeassistant/components/input_boolean/__init__.py | 2 +- homeassistant/components/input_number/__init__.py | 2 +- homeassistant/components/input_select/__init__.py | 2 +- homeassistant/components/input_text/__init__.py | 2 +- homeassistant/components/ipp/__init__.py | 2 +- homeassistant/components/isy994/config_flow.py | 4 ++-- .../components/keenetic_ndms2/binary_sensor.py | 2 +- homeassistant/components/keenetic_ndms2/config_flow.py | 2 +- .../components/keenetic_ndms2/device_tracker.py | 2 +- homeassistant/components/keenetic_ndms2/router.py | 2 +- homeassistant/components/knx/cover.py | 2 +- homeassistant/components/konnected/config_flow.py | 4 ++-- homeassistant/components/kostal_plenticore/helper.py | 2 +- homeassistant/components/kulersky/light.py | 2 +- 49 files changed, 64 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index d06e77f74e7..2b9d715ba51 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): """Representation of an Emonitor power sensor entity.""" - def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int): + def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int) -> None: """Initialize the channel sensor.""" self.channel_number = channel_number super().__init__(coordinator) diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index c9c530c6b08..0852f95bd99 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -150,7 +150,9 @@ class EnturProxy: class EnturPublicTransportSensor(SensorEntity): """Implementation of a Entur public transport sensor.""" - def __init__(self, api: EnturProxy, name: str, stop: str, show_on_map: bool): + def __init__( + self, api: EnturProxy, name: str, stop: str, show_on_map: bool + ) -> None: """Initialize the sensor.""" self.api = api self._stop = stop diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 94e0089042d..607af8cc47d 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -724,7 +724,7 @@ def esphome_state_property(func): class EsphomeEnumMapper: """Helper class to convert between hass and esphome enum values.""" - def __init__(self, func: Callable[[], dict[int, str]]): + def __init__(self, func: Callable[[], dict[int, str]]) -> None: """Construct a EsphomeEnumMapper.""" self._func = func @@ -750,7 +750,7 @@ def esphome_map_enum(func: Callable[[], dict[int, str]]): class EsphomeBaseEntity(Entity): """Define a base esphome entity.""" - def __init__(self, entry_id: str, component_key: str, key: int): + def __init__(self, entry_id: str, component_key: str, key: int) -> None: """Initialize.""" self._entry_id = entry_id self._component_key = component_key diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 105d77637a7..6b553de1a13 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -32,7 +32,7 @@ async def async_setup_entry( class EsphomeCamera(Camera, EsphomeBaseEntity): """A camera implementation for ESPHome.""" - def __init__(self, entry_id: str, component_key: str, key: int): + def __init__(self, entry_id: str, component_key: str, key: int) -> None: """Initialize.""" Camera.__init__(self) EsphomeBaseEntity.__init__(self, entry_id, component_key, key) diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index e7faff46155..3487444c735 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -107,7 +107,7 @@ class FinTsClient: Use this class as Context Manager to get the FinTS3Client object. """ - def __init__(self, credentials: BankCredentials, name: str): + def __init__(self, credentials: BankCredentials, name: str) -> None: """Initialize a FinTsClient.""" self._credentials = credentials self.name = name diff --git a/homeassistant/components/firmata/board.py b/homeassistant/components/firmata/board.py index 73e3c004cb9..1f0052732ea 100644 --- a/homeassistant/components/firmata/board.py +++ b/homeassistant/components/firmata/board.py @@ -32,7 +32,7 @@ FirmataPinType = Union[int, str] class FirmataBoard: """Manages a single Firmata board.""" - def __init__(self, config: dict): + def __init__(self, config: dict) -> None: """Initialize the board.""" self.config = config self.api = None diff --git a/homeassistant/components/firmata/entity.py b/homeassistant/components/firmata/entity.py index 7a576c09cd1..e9f9f3619fe 100644 --- a/homeassistant/components/firmata/entity.py +++ b/homeassistant/components/firmata/entity.py @@ -37,7 +37,7 @@ class FirmataPinEntity(FirmataEntity): config_entry: ConfigEntry, name: str, pin: FirmataPinType, - ): + ) -> None: """Initialize the pin entity.""" super().__init__(api) self._name = name diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py index 3c50a559e51..97901ea2f2b 100644 --- a/homeassistant/components/firmata/light.py +++ b/homeassistant/components/firmata/light.py @@ -59,7 +59,7 @@ class FirmataLight(FirmataPinEntity, LightEntity): config_entry: ConfigEntry, name: str, pin: FirmataPinType, - ): + ) -> None: """Initialize the light pin entity.""" super().__init__(api, config_entry, name, pin) diff --git a/homeassistant/components/firmata/pin.py b/homeassistant/components/firmata/pin.py index 3259d76cbb3..af07871efc7 100644 --- a/homeassistant/components/firmata/pin.py +++ b/homeassistant/components/firmata/pin.py @@ -15,7 +15,7 @@ class FirmataPinUsedException(Exception): class FirmataBoardPin: """Manages a single Firmata board pin.""" - def __init__(self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str): + def __init__(self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str) -> None: """Initialize the pin.""" self.board = board self._pin = pin @@ -43,7 +43,7 @@ class FirmataBinaryDigitalOutput(FirmataBoardPin): pin_mode: str, initial: bool, negate: bool, - ): + ) -> None: """Initialize the digital output pin.""" self._initial = initial self._negate = negate @@ -98,7 +98,7 @@ class FirmataPWMOutput(FirmataBoardPin): initial: bool, minimum: int, maximum: int, - ): + ) -> None: """Initialize the PWM/analog output pin.""" self._initial = initial self._min = minimum @@ -139,7 +139,7 @@ class FirmataBinaryDigitalInput(FirmataBoardPin): def __init__( self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str, negate: bool - ): + ) -> None: """Initialize the digital input pin.""" self._negate = negate self._forward_callback = None @@ -206,7 +206,7 @@ class FirmataAnalogInput(FirmataBoardPin): def __init__( self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str, differential: int - ): + ) -> None: """Initialize the analog input pin.""" self._differential = differential self._forward_callback = None diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 54167b6a55f..ff9b737cd00 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -47,7 +47,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class HassFlickAuth(AbstractFlickAuth): """Implementation of AbstractFlickAuth based on a Home Assistant entity config.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Flick authention based on a Home Assistant entity config.""" super().__init__(aiohttp_client.async_get_clientsession(hass)) self._entry = entry diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index e6880600212..c523271716a 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -36,7 +36,7 @@ async def async_setup_entry( class FlickPricingSensor(SensorEntity): """Entity object for Flick Electric sensor.""" - def __init__(self, api: FlickAPI): + def __init__(self, api: FlickAPI) -> None: """Entity object for Flick Electric sensor.""" self._api: FlickAPI = api self._price: FlickPrice = None diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index e955c784ae4..f32aa7e6e32 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -21,7 +21,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, api_client: API, location_id: str, device_id: str - ): + ) -> None: """Initialize the device.""" self.hass: HomeAssistant = hass self.api_client: API = api_client diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 26aef603a22..94683e2cb20 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -19,7 +19,7 @@ class FloEntity(Entity): name: str, device: FloDeviceDataUpdateCoordinator, **kwargs, - ): + ) -> None: """Init Flo entity.""" self._unique_id: str = f"{device.mac_address}_{entity_type}" self._name: str = name diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index ce9b48d1421..15bbbc78c69 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class FloSwitch(FloEntity, SwitchEntity): """Switch class for the Flo by Moen valve.""" - def __init__(self, device: FloDeviceDataUpdateCoordinator): + def __init__(self, device: FloDeviceDataUpdateCoordinator) -> None: """Initialize the Flo switch.""" super().__init__("shutoff_valve", "Shutoff Valve", device) self._state = self._device.last_known_valve_state == "open" diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 9345f8d5fed..5f37b135ace 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -51,7 +51,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): update_interval: timedelta, update_method: Callable[[], Awaitable] | None = None, request_refresh_debouncer: Debouncer | None = None, - ): + ) -> None: """Initialize the data update coordinator.""" DataUpdateCoordinator.__init__( self, diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 752f40a0ead..885e79994ff 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -364,7 +364,7 @@ class RequestData: source: str, request_id: str, devices: list[dict] | None, - ): + ) -> None: """Initialize the request data.""" self.config = config self.source = source @@ -388,7 +388,9 @@ def get_google_type(domain, device_class): class GoogleEntity: """Adaptation of Entity expressed in Google's terms.""" - def __init__(self, hass: HomeAssistant, config: AbstractConfig, state: State): + def __init__( + self, hass: HomeAssistant, config: AbstractConfig, state: State + ) -> None: """Initialize a Google entity.""" self.hass = hass self.config = config diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index 87f02ab82c4..9a927d13d29 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) class DeviceDataUpdateCoordinator(DataUpdateCoordinator): """Manages polling for state changes from the device.""" - def __init__(self, hass: HomeAssistant, device: Device): + def __init__(self, hass: HomeAssistant, device: Device) -> None: """Initialize the data update coordinator.""" DataUpdateCoordinator.__init__( self, diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index c8ec4c6c645..fe39ee635f4 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -86,7 +86,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): entry: ConfigEntry, client: Client, coordinators: dict[str, DataUpdateCoordinator], - ): + ) -> None: """Initialize.""" super().__init__( entry, coordinators, "valve", "Valve Controller", None, "mdi:water" diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 884bbcde7c1..beaf71dea51 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -29,7 +29,7 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): api_coro: Callable[..., Awaitable], api_lock: asyncio.Lock, valve_controller_uid: str, - ): + ) -> None: """Initialize.""" super().__init__( hass, diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index be765ee4cb0..b1e71ac2dab 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -49,7 +49,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the Harmony config flow.""" self.harmony_config = {} @@ -159,7 +159,7 @@ def _options_from_user_input(user_input): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Harmony.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 6c9b36fb3a0..78925ed73fc 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -34,7 +34,7 @@ def async_setup_auth_view(hass: HomeAssistant, user: User): class HassIOBaseAuth(HomeAssistantView): """Hass.io view to handle auth requests.""" - def __init__(self, hass: HomeAssistant, user: User): + def __init__(self, hass: HomeAssistant, user: User) -> None: """Initialize WebView.""" self.hass = hass self.user = user diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index e1bd1cb095c..47131b80de3 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -51,7 +51,7 @@ class HassIOView(HomeAssistantView): url = "/api/hassio/{path:.+}" requires_auth = False - def __init__(self, host: str, websession: aiohttp.ClientSession): + def __init__(self, host: str, websession: aiohttp.ClientSession) -> None: """Initialize a Hass.io base view.""" self._host = host self._websession = websession diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 7519c860398..61cec64bfda 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -35,7 +35,7 @@ class HassIOIngress(HomeAssistantView): url = "/api/hassio_ingress/{token}/{path:.*}" requires_auth = False - def __init__(self, host: str, websession: aiohttp.ClientSession): + def __init__(self, host: str, websession: aiohttp.ClientSession) -> None: """Initialize a Hass.io ingress view.""" self._host = host self._websession = websession diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index da5f1df20c6..cf14de7cc42 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -46,7 +46,7 @@ class ConfigEntryAuth(homeconnect.HomeConnectAPI): hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, - ): + ) -> None: """Initialize Home Connect Auth.""" self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/components/home_plus_control/helpers.py b/homeassistant/components/home_plus_control/helpers.py index 95d538def01..773732b1a50 100644 --- a/homeassistant/components/home_plus_control/helpers.py +++ b/homeassistant/components/home_plus_control/helpers.py @@ -27,7 +27,7 @@ class HomePlusControlOAuth2Implementation( self, hass: HomeAssistant, config_data: dict, - ): + ) -> None: """HomePlusControlOAuth2Implementation Constructor. Initialize the authentication implementation for the Legrand Home+ Control API. diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index 220014d7d4e..5af5559b2ef 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -65,7 +65,7 @@ class AccessoryAidStorage: persist over reboots. """ - def __init__(self, hass: HomeAssistant, entry: ConfigEntry): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Create a new entity map store.""" self.hass = hass self.allocations = {} diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 6e1cd9bcaed..506b4f5c7a8 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -113,7 +113,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize config flow.""" self.hk_data = {} @@ -263,7 +263,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for homekit.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self.hk_options = {} diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 3e19e18151f..3a0a0c32404 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -249,7 +249,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Huawei LTE options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 3acf39a140d..1ea392b8269 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -38,7 +38,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): unit_of_measurement: str = POWER_WATT, icon: str = "mdi:lightning-bolt", precision: int = 0, - ): + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self._user_id = user_id diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 61299c16c13..3e82f796c0e 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -432,7 +432,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): class HyperionOptionsFlow(OptionsFlow): """Hyperion options flow.""" - def __init__(self, config_entry: ConfigEntry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize a Hyperion options flow.""" self._config_entry = config_entry diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index a95ec804890..4ed9efd06f2 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -201,7 +201,7 @@ class AqualinkEntity(Entity): class. """ - def __init__(self, dev: AqualinkDevice): + def __init__(self, dev: AqualinkDevice) -> None: """Initialize the entity.""" self.dev = dev diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 5a33b5d9508..95b90791165 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -85,7 +85,7 @@ class IcloudAccount: max_interval: int, gps_accuracy_threshold: int, config_entry: ConfigEntry, - ): + ) -> None: """Initialize an iCloud account.""" self.hass = hass self._username = username @@ -374,7 +374,7 @@ class IcloudAccount: class IcloudDevice: """Representation of a iCloud device.""" - def __init__(self, account: IcloudAccount, device: AppleDevice, status): + def __init__(self, account: IcloudAccount, device: AppleDevice, status) -> None: """Initialize the iCloud device.""" self._account = account diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 808689bc00a..2f53e782750 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -61,7 +61,7 @@ def add_entities(account, async_add_entities, tracked): class IcloudTrackerEntity(TrackerEntity): """Represent a tracked device.""" - def __init__(self, account: IcloudAccount, device: IcloudDevice): + def __init__(self, account: IcloudAccount, device: IcloudDevice) -> None: """Set up the iCloud tracker entity.""" self._account = account self._device = device diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 6a7304dfc9d..0e1bda16d60 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -53,7 +53,7 @@ def add_entities(account, async_add_entities, tracked): class IcloudDeviceBatterySensor(SensorEntity): """Representation of a iCloud device battery sensor.""" - def __init__(self, account: IcloudAccount, device: IcloudDevice): + def __init__(self, account: IcloudAccount, device: IcloudDevice) -> None: """Initialize the battery sensor.""" self._account = account self._device = device diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 37b3bd7ff6a..e27abf70127 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -159,7 +159,7 @@ class ImageServeView(HomeAssistantView): def __init__( self, image_folder: pathlib.Path, image_collection: ImageStorageCollection - ): + ) -> None: """Initialize image serve view.""" self.transform_lock = asyncio.Lock() self.image_folder = image_folder diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 399ab73783b..661d58da989 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -144,7 +144,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class InputBoolean(ToggleEntity, RestoreEntity): """Representation of a boolean input.""" - def __init__(self, config: dict | None): + def __init__(self, config: dict | None) -> None: """Initialize a boolean input.""" self._config = config self.editable = True diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 7c6a34f6e5b..9c0cc202cfa 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -196,7 +196,7 @@ class NumberStorageCollection(collection.StorageCollection): class InputNumber(RestoreEntity): """Representation of a slider.""" - def __init__(self, config: dict): + def __init__(self, config: dict) -> None: """Initialize an input number.""" self._config = config self.editable = True diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index b53b907ca10..859ae6f91e1 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -201,7 +201,7 @@ class InputSelectStorageCollection(collection.StorageCollection): class InputSelect(RestoreEntity): """Representation of a select input.""" - def __init__(self, config: dict): + def __init__(self, config: dict) -> None: """Initialize a select input.""" self._config = config self.editable = True diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index f4daec0a4d4..417dbf81249 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -190,7 +190,7 @@ class InputTextStorageCollection(collection.StorageCollection): class InputText(RestoreEntity): """Represent a text box.""" - def __init__(self, config: dict): + def __init__(self, config: dict) -> None: """Initialize a text input.""" self._config = config self.editable = True diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 198fdaa602a..c1bc7ed4986 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -82,7 +82,7 @@ class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]): base_path: str, tls: bool, verify_ssl: bool, - ): + ) -> None: """Initialize global IPP data updater.""" self.ipp = IPP( host=host, diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index bd1baa66045..c9ca29e8f63 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -108,7 +108,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the isy994 config flow.""" self.discovered_conf = {} @@ -194,7 +194,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for isy994.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py index 5da52eff00d..ed366bd7402 100644 --- a/homeassistant/components/keenetic_ndms2/binary_sensor.py +++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry( class RouterOnlineBinarySensor(BinarySensorEntity): """Representation router connection status.""" - def __init__(self, router: KeeneticRouter): + def __init__(self, router: KeeneticRouter) -> None: """Initialize the APCUPSd binary device.""" self._router = router diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index 4437aa12edf..4377a1094fc 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -89,7 +89,7 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): """Handle options.""" - def __init__(self, config_entry: ConfigEntry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self._interface_options = {} diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index 461814b1917..a08d8c72f0c 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -155,7 +155,7 @@ def update_items(router: KeeneticRouter, async_add_entities, tracked: set[str]): class KeeneticTracker(ScannerEntity): """Representation of network device.""" - def __init__(self, device: Device, router: KeeneticRouter): + def __init__(self, device: Device, router: KeeneticRouter) -> None: """Initialize the tracked device.""" self._device = device self._router = router diff --git a/homeassistant/components/keenetic_ndms2/router.py b/homeassistant/components/keenetic_ndms2/router.py index 049d9aab0de..87841d8291c 100644 --- a/homeassistant/components/keenetic_ndms2/router.py +++ b/homeassistant/components/keenetic_ndms2/router.py @@ -37,7 +37,7 @@ _LOGGER = logging.getLogger(__name__) class KeeneticRouter: """Keenetic client Object.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the Client.""" self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 0ca060e6f22..86fba34a87a 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -86,7 +86,7 @@ def _async_migrate_unique_id( class KNXCover(KnxEntity, CoverEntity): """Representation of a KNX cover.""" - def __init__(self, xknx: XKNX, config: ConfigType): + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize the cover.""" self._device: XknxCover super().__init__( diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index b1099b47929..4cc53c9069a 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -168,7 +168,7 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # class variable to store/share discovered host information discovered_hosts = {} - def __init__(self): + def __init__(self) -> None: """Initialize the Konnected flow.""" self.data = {} self.options = OPTIONS_SCHEMA({CONF_IO: {}}) @@ -360,7 +360,7 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for a Konnected Panel.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.entry = config_entry self.model = self.entry.data[CONF_MODEL] diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index a78896a179d..eb4f6ce44a6 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -121,7 +121,7 @@ class PlenticoreUpdateCoordinator(DataUpdateCoordinator): name: str, update_inverval: timedelta, plenticore: Plenticore, - ): + ) -> None: """Create a new update coordinator for plenticore data.""" super().__init__( hass=hass, diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index c83b4dac9a7..48f27e91c79 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -68,7 +68,7 @@ async def async_setup_entry( class KulerskyLight(LightEntity): """Representation of an Kuler Sky Light.""" - def __init__(self, light: pykulersky.Light): + def __init__(self, light: pykulersky.Light) -> None: """Initialize a Kuler Sky light.""" self._light = light self._hs_color = None From cf228e3fe5a94938a28a4e8f1aa38886b0912656 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 20 May 2021 18:51:39 +0300 Subject: [PATCH 606/852] Add constructor return type in integrations A-D (#50903) --- homeassistant/components/acmeda/base.py | 2 +- homeassistant/components/airly/__init__.py | 2 +- homeassistant/components/alarmdecoder/config_flow.py | 2 +- homeassistant/components/alexa/capabilities.py | 2 +- homeassistant/components/alexa/entities.py | 4 +++- homeassistant/components/almond/__init__.py | 4 ++-- homeassistant/components/arris_tg2492lg/device_tracker.py | 2 +- homeassistant/components/asuswrt/config_flow.py | 2 +- homeassistant/components/atome/sensor.py | 2 +- homeassistant/components/aurora/__init__.py | 4 ++-- homeassistant/components/automation/trace.py | 2 +- homeassistant/components/azure_devops/sensor.py | 2 +- homeassistant/components/azure_event_hub/__init__.py | 2 +- homeassistant/components/blueprint/errors.py | 4 ++-- homeassistant/components/bmp280/sensor.py | 6 +++--- homeassistant/components/bond/config_flow.py | 2 +- homeassistant/components/bond/entity.py | 2 +- homeassistant/components/bond/fan.py | 4 +++- homeassistant/components/bond/light.py | 8 +++++--- homeassistant/components/bond/switch.py | 4 +++- homeassistant/components/bond/utils.py | 2 +- homeassistant/components/bsblan/climate.py | 2 +- homeassistant/components/canary/coordinator.py | 2 +- homeassistant/components/cast/media_player.py | 2 +- homeassistant/components/cloud/account_link.py | 2 +- homeassistant/components/cloud/alexa_config.py | 2 +- homeassistant/components/cloud/client.py | 2 +- homeassistant/components/cloud/tts.py | 2 +- homeassistant/components/control4/__init__.py | 2 +- homeassistant/components/control4/config_flow.py | 2 +- homeassistant/components/control4/light.py | 2 +- homeassistant/components/conversation/default_agent.py | 2 +- homeassistant/components/counter/__init__.py | 2 +- homeassistant/components/daikin/__init__.py | 2 +- homeassistant/components/denonavr/config_flow.py | 2 +- homeassistant/components/denonavr/media_player.py | 2 +- homeassistant/components/denonavr/receiver.py | 2 +- homeassistant/components/dexcom/config_flow.py | 2 +- homeassistant/components/doorbird/config_flow.py | 2 +- 39 files changed, 54 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index 15f9716db47..459f4ab2097 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -13,7 +13,7 @@ from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER class AcmedaBase(entity.Entity): """Base representation of an Acmeda roller.""" - def __init__(self, roller: aiopulse.Roller): + def __init__(self, roller: aiopulse.Roller) -> None: """Initialize the roller.""" self.roller = roller diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 26e14a7642e..58899d76ef8 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -136,7 +136,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator): longitude: float, update_interval: timedelta, use_nearest: bool, - ): + ) -> None: """Initialize.""" self.latitude = latitude self.longitude = longitude diff --git a/homeassistant/components/alarmdecoder/config_flow.py b/homeassistant/components/alarmdecoder/config_flow.py index 1c46f50f3cb..cc4a19e4755 100644 --- a/homeassistant/components/alarmdecoder/config_flow.py +++ b/homeassistant/components/alarmdecoder/config_flow.py @@ -140,7 +140,7 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow): """Handle AlarmDecoder options.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize AlarmDecoder options flow.""" self.arm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS) self.zone_options = config_entry.options.get( diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 1afe65b7bc6..483b4484261 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -73,7 +73,7 @@ class AlexaCapability: supported_locales = {"en-US"} - def __init__(self, entity: State, instance: str | None = None): + def __init__(self, entity: State, instance: str | None = None) -> None: """Initialize an Alexa capability.""" self.entity = entity self.instance = instance diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index f31901ce037..344ba2b7d21 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -254,7 +254,9 @@ class AlexaEntity: The API handlers should manipulate entities only through this interface. """ - def __init__(self, hass: HomeAssistant, config: AbstractConfig, entity: State): + def __init__( + self, hass: HomeAssistant, config: AbstractConfig, entity: State + ) -> None: """Initialize Alexa Entity.""" self.hass = hass self.config = config diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 554a4aa47bc..0c012788b0e 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -231,7 +231,7 @@ class AlmondOAuth(AbstractAlmondWebAuth): host: str, websession: ClientSession, oauth_session: config_entry_oauth2_flow.OAuth2Session, - ): + ) -> None: """Initialize Almond auth.""" super().__init__(host, websession) self._oauth_session = oauth_session @@ -249,7 +249,7 @@ class AlmondAgent(conversation.AbstractConversationAgent): def __init__( self, hass: HomeAssistant, api: WebAlmondAPI, entry: config_entries.ConfigEntry - ): + ) -> None: """Initialize the agent.""" self.hass = hass self.api = api diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index 1011d76f8aa..09ddf40e063 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -33,7 +33,7 @@ def get_scanner(hass, config): class ArrisDeviceScanner(DeviceScanner): """This class queries a Arris TG2492LG router for connected devices.""" - def __init__(self, connect_box: ConnectBox): + def __init__(self, connect_box: ConnectBox) -> None: """Initialize the scanner.""" self.connect_box = connect_box self.last_results: list[Device] = [] diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index 8028a703ac0..0ffa674e054 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -184,7 +184,7 @@ class AsusWrtFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for AsusWrt.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index d10024f64c2..285b6c70713 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -77,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AtomeData: """Stores data retrieved from Neurio sensor.""" - def __init__(self, client: AtomeClient): + def __init__(self, client: AtomeClient) -> None: """Initialize the data.""" self.atome_client = client self._live_power = None diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index e565071eae2..e8dc98a18b7 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -95,7 +95,7 @@ class AuroraDataUpdateCoordinator(DataUpdateCoordinator): latitude: float, longitude: float, threshold: float, - ): + ) -> None: """Initialize the data updater.""" super().__init__( @@ -128,7 +128,7 @@ class AuroraEntity(CoordinatorEntity): coordinator: AuroraDataUpdateCoordinator, name: str, icon: str, - ): + ) -> None: """Initialize the Aurora Entity.""" super().__init__(coordinator=coordinator) diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index 102aeda5a65..1fbc7e5cbc9 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -21,7 +21,7 @@ class AutomationTrace(ActionTrace): config: dict[str, Any], blueprint_inputs: dict[str, Any], context: Context, - ): + ) -> None: """Container for automation trace.""" key = ("automation", item_id) super().__init__(key, config, blueprint_inputs, context) diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index ef6697dea5f..170cd244884 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -109,7 +109,7 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): def __init__( self, client: DevOpsClient, organization: str, project: str, build: DevOpsBuild - ): + ) -> None: """Initialize Azure DevOps sensor.""" self.build: DevOpsBuild = build super().__init__( diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 0473c4ff5a7..0c5ae2b81b8 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -102,7 +102,7 @@ class AzureEventHub: entities_filter: vol.Schema, send_interval: int, max_delay: int, - ): + ) -> None: """Initialize the listener.""" self.hass = hass self.queue = asyncio.PriorityQueue() diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index b5032af9326..4b14201652f 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -45,7 +45,7 @@ class InvalidBlueprint(BlueprintWithNameException): blueprint_name: str, blueprint_data: Any, msg_or_exc: vol.Invalid, - ): + ) -> None: """Initialize an invalid blueprint error.""" if isinstance(msg_or_exc, vol.Invalid): msg_or_exc = humanize_error(blueprint_data, msg_or_exc) @@ -61,7 +61,7 @@ class InvalidBlueprint(BlueprintWithNameException): class InvalidBlueprintInputs(BlueprintException): """When we encountered invalid blueprint inputs.""" - def __init__(self, domain: str, msg: str): + def __init__(self, domain: str, msg: str) -> None: """Initialize an invalid blueprint inputs error.""" super().__init__( domain, diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py index 60cbdb75d41..ac607578299 100644 --- a/homeassistant/components/bmp280/sensor.py +++ b/homeassistant/components/bmp280/sensor.py @@ -74,7 +74,7 @@ class Bmp280Sensor(SensorEntity): name: str, unit_of_measurement: str, device_class: str, - ): + ) -> None: """Initialize the sensor.""" self._bmp280 = bmp280 self._name = name @@ -112,7 +112,7 @@ class Bmp280Sensor(SensorEntity): class Bmp280TemperatureSensor(Bmp280Sensor): """Representation of a Bosch BMP280 Temperature Sensor.""" - def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str): + def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str) -> None: """Initialize the entity.""" super().__init__( bmp280, f"{name} Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE @@ -137,7 +137,7 @@ class Bmp280TemperatureSensor(Bmp280Sensor): class Bmp280PressureSensor(Bmp280Sensor): """Representation of a Bosch BMP280 Barometric Pressure Sensor.""" - def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str): + def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str) -> None: """Initialize the entity.""" super().__init__( bmp280, f"{name} Pressure", PRESSURE_HPA, DEVICE_CLASS_PRESSURE diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 6265b5ba942..9285b580851 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -179,7 +179,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class InputValidationError(exceptions.HomeAssistantError): """Error to indicate we cannot proceed due to invalid input.""" - def __init__(self, base: str): + def __init__(self, base: str) -> None: """Initialize with error base.""" super().__init__() self.base = base diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index d435faf0a7c..59425af54d0 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -31,7 +31,7 @@ class BondEntity(Entity): device: BondDevice, bpup_subs: BPUPSubscriptions, sub_device: str | None = None, - ): + ) -> None: """Initialize entity with API and device info.""" self._hub = hub self._device = device diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 3d611eb3f8c..92ce0b81658 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -53,7 +53,9 @@ async def async_setup_entry( class BondFan(BondEntity, FanEntity): """Representation of a Bond fan.""" - def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions): + def __init__( + self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions + ) -> None: """Create HA entity representing Bond fan.""" super().__init__(hub, device, bpup_subs) diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 887e7901d7d..ca9cbf45a7a 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -87,7 +87,7 @@ class BondBaseLight(BondEntity, LightEntity): device: BondDevice, bpup_subs: BPUPSubscriptions, sub_device: str | None = None, - ): + ) -> None: """Create HA entity representing Bond light.""" super().__init__(hub, device, bpup_subs, sub_device) self._light: int | None = None @@ -112,7 +112,7 @@ class BondLight(BondBaseLight, BondEntity, LightEntity): device: BondDevice, bpup_subs: BPUPSubscriptions, sub_device: str | None = None, - ): + ) -> None: """Create HA entity representing Bond light.""" super().__init__(hub, device, bpup_subs, sub_device) self._brightness: int | None = None @@ -193,7 +193,9 @@ class BondUpLight(BondBaseLight, BondEntity, LightEntity): class BondFireplace(BondEntity, LightEntity): """Representation of a Bond-controlled fireplace.""" - def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions): + def __init__( + self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions + ) -> None: """Create HA entity representing Bond fireplace.""" super().__init__(hub, device, bpup_subs) diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index 36d23547d7e..0f323b1609b 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -38,7 +38,9 @@ async def async_setup_entry( class BondSwitch(BondEntity, SwitchEntity): """Representation of a Bond generic device.""" - def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions): + def __init__( + self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions + ) -> None: """Create HA entity representing Bond generic device (switch).""" super().__init__(hub, device, bpup_subs) diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 916da69a06c..6ace83831fe 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -103,7 +103,7 @@ class BondDevice: class BondHub: """Hub device representing Bond Bridge.""" - def __init__(self, bond: Bond): + def __init__(self, bond: Bond) -> None: """Initialize Bond Hub.""" self.bond: Bond = bond self._bridge: dict[str, Any] = {} diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 7533e7e07f9..d6ec805af55 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -93,7 +93,7 @@ class BSBLanClimate(ClimateEntity): entry_id: str, bsblan: BSBLan, info: Info, - ): + ) -> None: """Initialize BSBLan climate device.""" self._current_temperature: float | None = None self._available = True diff --git a/homeassistant/components/canary/coordinator.py b/homeassistant/components/canary/coordinator.py index a7f8ea7c8de..d1bdac5eee9 100644 --- a/homeassistant/components/canary/coordinator.py +++ b/homeassistant/components/canary/coordinator.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) class CanaryDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Canary data.""" - def __init__(self, hass: HomeAssistant, *, api: Api): + def __init__(self, hass: HomeAssistant, *, api: Api) -> None: """Initialize global Canary data updater.""" self.canary = api update_interval = timedelta(seconds=30) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 9cad02f6c74..969e690fcc2 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -160,7 +160,7 @@ class CastDevice(MediaPlayerEntity): "elected leader" itself. """ - def __init__(self, cast_info: ChromecastInfo): + def __init__(self, cast_info: ChromecastInfo) -> None: """Initialize the cast device.""" self._cast_info = cast_info diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 4a3a2dd77f8..df93ca6a6ab 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -94,7 +94,7 @@ async def _get_services(hass): class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implementation): """Cloud implementation of the OAuth2 flow.""" - def __init__(self, hass: HomeAssistant, service: str): + def __init__(self, hass: HomeAssistant, service: str) -> None: """Initialize cloud OAuth2 implementation.""" self.hass = hass self.service = service diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 393bfdfc2cd..c7568d7ae25 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -42,7 +42,7 @@ class AlexaConfig(alexa_config.AbstractConfig): cloud_user: str, prefs: CloudPreferences, cloud: Cloud, - ): + ) -> None: """Initialize the Alexa config.""" super().__init__(hass) self._config = config diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 6c09169ef34..c29e79f4e84 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -35,7 +35,7 @@ class CloudClient(Interface): websession: aiohttp.ClientSession, alexa_user_config: dict[str, Any], google_user_config: dict[str, Any], - ): + ) -> None: """Initialize client interface to Cloud.""" self._hass = hass self._prefs = prefs diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 4d19547d30c..51c3e5f3a4e 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -61,7 +61,7 @@ async def async_get_engine(hass, config, discovery_info=None): class CloudProvider(Provider): """NabuCasa Cloud speech API provider.""" - def __init__(self, cloud: Cloud, language: str, gender: str): + def __init__(self, cloud: Cloud, language: str, gender: str) -> None: """Initialize cloud provider.""" self.cloud = cloud self.name = "Cloud" diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 01958ef3453..78e27d86f8e 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -153,7 +153,7 @@ class Control4Entity(CoordinatorEntity): device_manufacturer: str, device_model: str, device_id: int, - ): + ) -> None: """Initialize a Control4 entity.""" super().__init__(coordinator) self.entry = entry diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index 1f476054e31..d13fe31601f 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -144,7 +144,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Control4.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index f8c94c6a932..46fc35398fe 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -150,7 +150,7 @@ class Control4Light(Control4Entity, LightEntity): device_model: str, device_id: int, is_dimmer: bool, - ): + ) -> None: """Initialize Control4 light entity.""" super().__init__( entry_data, diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index a98f685ea1d..1773ca46cb5 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -53,7 +53,7 @@ def async_register(hass, intent_type, utterances): class DefaultAgent(AbstractConversationAgent): """Default agent for conversation agent.""" - def __init__(self, hass: core.HomeAssistant): + def __init__(self, hass: core.HomeAssistant) -> None: """Initialize the default agent.""" self.hass = hass diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index ecb405a81cd..75b2b4902cb 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -173,7 +173,7 @@ class CounterStorageCollection(collection.StorageCollection): class Counter(RestoreEntity): """Representation of a counter.""" - def __init__(self, config: dict): + def __init__(self, config: dict) -> None: """Initialize a counter.""" self._config: dict = config self._state: int | None = config[CONF_INITIAL] diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 9d0d189248f..63b0c7de25e 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -86,7 +86,7 @@ async def daikin_api_setup(hass, host, key, uuid, password): class DaikinApi: """Keep the Daikin instance in one place and centralize the update.""" - def __init__(self, device: Appliance): + def __init__(self, device: Appliance) -> None: """Initialize the Daikin Handle.""" self.device = device self.name = device.values.get("name", "Daikin AC") diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index dc97e81bafd..1005858e729 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -45,7 +45,7 @@ CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str}) class OptionsFlowHandler(config_entries.OptionsFlow): """Options for the component.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Init object.""" self.config_entry = config_entry diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 2b8420e6774..caa34e352d0 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -142,7 +142,7 @@ class DenonDevice(MediaPlayerEntity): unique_id: str, config_entry: config_entries.ConfigEntry, update_audyssey: bool, - ): + ) -> None: """Initialize the device.""" self._receiver = receiver self._unique_id = unique_id diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py index c5d4661b1a8..a35b6c80fcd 100644 --- a/homeassistant/components/denonavr/receiver.py +++ b/homeassistant/components/denonavr/receiver.py @@ -20,7 +20,7 @@ class ConnectDenonAVR: zone2: bool, zone3: bool, async_client_getter: Callable, - ): + ) -> None: """Initialize the class.""" self._async_client_getter = async_client_getter self._receiver = None diff --git a/homeassistant/components/dexcom/config_flow.py b/homeassistant/components/dexcom/config_flow.py index 37e6c5e9756..063d14549db 100644 --- a/homeassistant/components/dexcom/config_flow.py +++ b/homeassistant/components/dexcom/config_flow.py @@ -61,7 +61,7 @@ class DexcomConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class DexcomOptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Dexcom.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 5d207fbbbce..16b9725cf83 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -149,7 +149,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for doorbird.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry From 391b2f8ccd67d3e99ac24bfa0af36de2d0532540 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Thu, 20 May 2021 16:53:29 +0100 Subject: [PATCH 607/852] Add missing return type in Core constructors (#50884) --- homeassistant/auth/__init__.py | 2 +- homeassistant/config_entries.py | 2 +- homeassistant/core.py | 2 +- homeassistant/data_entry_flow.py | 4 +++- homeassistant/helpers/collection.py | 10 ++++++---- homeassistant/helpers/config_entry_oauth2_flow.py | 4 ++-- homeassistant/helpers/debounce.py | 2 +- homeassistant/helpers/entity_component.py | 2 +- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/entity_registry.py | 2 +- homeassistant/helpers/event.py | 4 ++-- homeassistant/helpers/ratelimit.py | 2 +- homeassistant/helpers/script_variables.py | 2 +- homeassistant/helpers/service.py | 2 +- homeassistant/helpers/storage.py | 2 +- homeassistant/helpers/template.py | 2 +- homeassistant/helpers/trace.py | 2 +- homeassistant/helpers/update_coordinator.py | 2 +- homeassistant/loader.py | 2 +- homeassistant/util/yaml/loader.py | 2 +- 20 files changed, 29 insertions(+), 25 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 14981d0df09..931c2f4c11a 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -79,7 +79,7 @@ async def auth_manager_from_config( class AuthManagerFlowManager(data_entry_flow.FlowManager): """Manage authentication flows.""" - def __init__(self, hass: HomeAssistant, auth_manager: AuthManager): + def __init__(self, hass: HomeAssistant, auth_manager: AuthManager) -> None: """Init auth manager flows.""" super().__init__(hass) self.auth_manager = auth_manager diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c586fcad79f..729d8ae94fa 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -569,7 +569,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): def __init__( self, hass: HomeAssistant, config_entries: ConfigEntries, hass_config: dict - ): + ) -> None: """Initialize the config entry flow manager.""" super().__init__(hass) self.config_entries = config_entries diff --git a/homeassistant/core.py b/homeassistant/core.py index b1610faad6e..5447277e835 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -163,7 +163,7 @@ class HassJob: __slots__ = ("job_type", "target") - def __init__(self, target: Callable): + def __init__(self, target: Callable) -> None: """Create a job object.""" if asyncio.iscoroutine(target): raise ValueError("Coroutine not allowed to be passed to HassJob") diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index dd8b1c53a68..786cfe7e286 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -44,7 +44,9 @@ class UnknownStep(FlowError): class AbortFlow(FlowError): """Exception to indicate a flow needs to be aborted.""" - def __init__(self, reason: str, description_placeholders: dict | None = None): + def __init__( + self, reason: str, description_placeholders: dict | None = None + ) -> None: """Initialize an abort flow exception.""" super().__init__(f"Flow aborted: {reason}") self.reason = reason diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index bfffb8523dd..6d9815e54d5 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -66,7 +66,7 @@ class CollectionError(HomeAssistantError): class ItemNotFound(CollectionError): """Raised when an item is not found.""" - def __init__(self, item_id: str): + def __init__(self, item_id: str) -> None: """Initialize item not found error.""" super().__init__(f"Item {item_id} not found.") self.item_id = item_id @@ -103,7 +103,9 @@ class IDManager: class ObservableCollection(ABC): """Base collection type that can be observed.""" - def __init__(self, logger: logging.Logger, id_manager: IDManager | None = None): + def __init__( + self, logger: logging.Logger, id_manager: IDManager | None = None + ) -> None: """Initialize the base collection.""" self.logger = logger self.id_manager = id_manager or IDManager() @@ -190,7 +192,7 @@ class StorageCollection(ObservableCollection): store: Store, logger: logging.Logger, id_manager: IDManager | None = None, - ): + ) -> None: """Initialize the storage collection.""" super().__init__(logger, id_manager) self.store = store @@ -389,7 +391,7 @@ class StorageCollectionWebsocket: model_name: str, create_schema: dict, update_schema: dict, - ): + ) -> None: """Initialize a websocket CRUD.""" self.storage_collection = storage_collection self.api_prefix = api_prefix diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 514c31f355b..8704932db73 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -107,7 +107,7 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): client_secret: str, authorize_url: str, token_url: str, - ): + ) -> None: """Initialize local auth implementation.""" self.hass = hass self._domain = domain @@ -437,7 +437,7 @@ class OAuth2Session: hass: HomeAssistant, config_entry: config_entries.ConfigEntry, implementation: AbstractOAuth2Implementation, - ): + ) -> None: """Initialize an OAuth2 session.""" self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 8e7e57fa142..75e0215d2cb 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -20,7 +20,7 @@ class Debouncer: cooldown: float, immediate: bool, function: Callable[..., Awaitable[Any]] | None = None, - ): + ) -> None: """Initialize debounce. immediate: indicate if the function needs to be called right away and diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index f1623889946..37c0a7620ab 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -76,7 +76,7 @@ class EntityComponent: domain: str, hass: HomeAssistant, scan_interval: timedelta = DEFAULT_SCAN_INTERVAL, - ): + ) -> None: """Initialize an entity component.""" self.logger = logger self.hass = hass diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 3331c71dd32..26bfdb43e66 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -85,7 +85,7 @@ class EntityPlatform: platform: ModuleType | None, scan_interval: timedelta, entity_namespace: str | None, - ): + ) -> None: """Initialize the entity platform.""" self.hass = hass self.logger = logger diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 5d3ede31ee6..af0c5f4f20f 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -146,7 +146,7 @@ class RegistryEntry: class EntityRegistry: """Class to hold a registry of entities.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize the registry.""" self.hass = hass self.entities: dict[str, RegistryEntry] diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index cdcacb8871b..48dd05d2311 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -534,7 +534,7 @@ class _TrackStateChangeFiltered: hass: HomeAssistant, track_states: TrackStates, action: Callable[[Event], Any], - ): + ) -> None: """Handle removal / refresh of tracker init.""" self.hass = hass self._action = action @@ -775,7 +775,7 @@ class _TrackTemplateResultInfo: hass: HomeAssistant, track_templates: Iterable[TrackTemplate], action: Callable, - ): + ) -> None: """Handle removal / refresh of tracker init.""" self.hass = hass self._job = HassJob(action) diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py index c34cfb72b36..389b3f4d2d5 100644 --- a/homeassistant/helpers/ratelimit.py +++ b/homeassistant/helpers/ratelimit.py @@ -19,7 +19,7 @@ class KeyedRateLimit: def __init__( self, hass: HomeAssistant, - ): + ) -> None: """Initialize ratelimit tracker.""" self.hass = hass self._last_triggered: dict[Hashable, datetime] = {} diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index a72d0b5543f..23241f22d1e 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -12,7 +12,7 @@ from . import template class ScriptVariables: """Class to hold and render script variables.""" - def __init__(self, variables: dict[str, Any]): + def __init__(self, variables: dict[str, Any]) -> None: """Initialize script variables.""" self.variables = variables self._has_template: bool | None = None diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ed23926b0a3..31befb36531 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -73,7 +73,7 @@ class ServiceParams(TypedDict): class ServiceTargetSelector: """Class to hold a target selector for a service.""" - def __init__(self, service_call: ServiceCall): + def __init__(self, service_call: ServiceCall) -> None: """Extract ids from service call data.""" entity_ids: str | list | None = service_call.data.get(ATTR_ENTITY_ID) device_ids: str | list | None = service_call.data.get(ATTR_DEVICE_ID) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 456e9b04709..5700a7f854b 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -75,7 +75,7 @@ class Store: private: bool = False, *, encoder: type[JSONEncoder] | None = None, - ): + ) -> None: """Initialize storage class.""" self.version = version self.key = key diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 40101e17128..86223d2a950 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -175,7 +175,7 @@ class TupleWrapper(tuple, ResultWrapper): # pylint: disable=super-init-not-called - def __init__(self, value: tuple, *, render_result: str | None = None): + def __init__(self, value: tuple, *, render_result: str | None = None) -> None: """Initialize a new tuple class.""" self.render_result = render_result diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index bfa713a3b2a..33fe76c9eab 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -15,7 +15,7 @@ import homeassistant.util.dt as dt_util class TraceElement: """Container for trace data.""" - def __init__(self, variables: TemplateVarsType, path: str): + def __init__(self, variables: TemplateVarsType, path: str) -> None: """Container for trace data.""" self._child_key: tuple[str, str] | None = None self._child_run_id: str | None = None diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index f3eea63b1d4..c15d6534626 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -42,7 +42,7 @@ class DataUpdateCoordinator(Generic[T]): update_interval: timedelta | None = None, update_method: Callable[[], Awaitable[T]] | None = None, request_refresh_debouncer: Debouncer | None = None, - ): + ) -> None: """Initialize global data updater.""" self.hass = hass self.logger = logger diff --git a/homeassistant/loader.py b/homeassistant/loader.py index adebe535f6a..444e35add33 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -305,7 +305,7 @@ class Integration: pkg_path: str, file_path: pathlib.Path, manifest: Manifest, - ): + ) -> None: """Initialize an integration.""" self.hass = hass self.pkg_path = pkg_path diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index dbff753aa68..5e98e4cfc6f 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) class Secrets: """Store secrets while loading YAML.""" - def __init__(self, config_dir: Path): + def __init__(self, config_dir: Path) -> None: """Initialize secrets.""" self.config_dir = config_dir self._cache: dict[Path, dict[str, str]] = {} From fd2e640c7418d6b76e12ef006e1e7147e4b1cc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 20 May 2021 18:23:00 +0200 Subject: [PATCH 608/852] Use sensor constants in recorder (#50906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/sensor/recorder.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 34e373dae50..e3da50d9738 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -6,14 +6,22 @@ import itertools from statistics import fmean from homeassistant.components.recorder import history, statistics -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, +) from homeassistant.const import ATTR_DEVICE_CLASS from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util from . import DOMAIN -DEVICE_CLASS_STATISTICS = {"temperature": {"mean", "min", "max"}, "energy": {"sum"}} +DEVICE_CLASS_STATISTICS = { + DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, + DEVICE_CLASS_ENERGY: {"sum"}, +} def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]: From 0e7409e617cf69366b55544c277410cfe125a325 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Thu, 20 May 2021 18:00:10 +0100 Subject: [PATCH 609/852] Pylint plugin to check __init__ return type (#50868) * Pylint plugin to check __init__ return type * Support *args add **kwargs, type hints * Use 'in' instead of 'any()' * Fix last few places --- homeassistant/components/harmony/switch.py | 2 +- homeassistant/components/tesla/config_flow.py | 2 +- pylint/plugins/hass_constructor.py | 52 +++++++++++++++++++ pylint/plugins/hass_logger.py | 41 +++++---------- pyproject.toml | 3 +- 5 files changed, 68 insertions(+), 32 deletions(-) create mode 100644 pylint/plugins/hass_constructor.py diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py index 16b83c80478..a45b43fce0f 100644 --- a/homeassistant/components/harmony/switch.py +++ b/homeassistant/components/harmony/switch.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity): """Switch representation of a Harmony activity.""" - def __init__(self, name: str, activity: dict, data: HarmonyData): + def __init__(self, name: str, activity: dict, data: HarmonyData) -> None: """Initialize HarmonyActivitySwitch class.""" super().__init__() self._name = name diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index 89dc8d2a2b4..af2fd7ae769 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -114,7 +114,7 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Tesla.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/pylint/plugins/hass_constructor.py b/pylint/plugins/hass_constructor.py new file mode 100644 index 00000000000..3a3012b9f69 --- /dev/null +++ b/pylint/plugins/hass_constructor.py @@ -0,0 +1,52 @@ +"""Plugin for constructor definitions.""" +from astroid import ClassDef, Const, FunctionDef +from pylint.checkers import BaseChecker +from pylint.interfaces import IAstroidChecker +from pylint.lint import PyLinter + + +class HassConstructorFormatChecker(BaseChecker): # type: ignore[misc] + """Checker for __init__ definitions.""" + + __implements__ = IAstroidChecker + + name = "hass_constructor" + priority = -1 + msgs = { + "W0006": ( + '__init__ should have explicit return type "None"', + "hass-constructor-return", + "Used when __init__ has all arguments typed " + "but doesn't have return type declared", + ), + } + options = () + + def visit_functiondef(self, node: FunctionDef) -> None: + """Called when a FunctionDef node is visited.""" + if not node.is_method() or node.name != "__init__": + return + + # Check that all arguments are annotated. + # The first argument is "self". + args = node.args + annotations = ( + args.posonlyargs_annotations + + args.annotations + + args.kwonlyargs_annotations + )[1:] + if args.vararg is not None: + annotations.append(args.varargannotation) + if args.kwarg is not None: + annotations.append(args.kwargannotation) + if not annotations or None in annotations: + return + + # Check that return type is specified and it is "None". + if not isinstance(node.returns, Const) or node.returns.value != None: + self.add_message("hass-constructor-return", node=node) + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(HassConstructorFormatChecker(linter)) diff --git a/pylint/plugins/hass_logger.py b/pylint/plugins/hass_logger.py index b771b07aa5e..0ca57b8da19 100644 --- a/pylint/plugins/hass_logger.py +++ b/pylint/plugins/hass_logger.py @@ -1,18 +1,18 @@ +"""Plugin for logger invocations.""" import astroid from pylint.checkers import BaseChecker from pylint.interfaces import IAstroidChecker +from pylint.lint import PyLinter LOGGER_NAMES = ("LOGGER", "_LOGGER") LOG_LEVEL_ALLOWED_LOWER_START = ("debug",) -# This is our checker class. -# Checkers should always inherit from `BaseChecker`. -class HassLoggerFormatChecker(BaseChecker): - """Add class member attributes to the class locals dictionary.""" + +class HassLoggerFormatChecker(BaseChecker): # type: ignore[misc] + """Checker for logger invocations.""" __implements__ = IAstroidChecker - # The name defines a custom section of the config for this checker. name = "hass_logger" priority = -1 msgs = { @@ -27,24 +27,10 @@ class HassLoggerFormatChecker(BaseChecker): "All logger messages must start with a capital letter", ), } - options = ( - ( - "hass-logger", - { - "default": "properties", - "help": ( - "Validate _LOGGER or LOGGER messages conform to Home Assistant standards." - ), - }, - ), - ) + options = () - def visit_call(self, node): - """Called when a :class:`.astroid.node_classes.Call` node is visited. - See :mod:`astroid` for the description of available nodes. - :param node: The node to check. - :type node: astroid.node_classes.Call - """ + def visit_call(self, node: astroid.Call) -> None: + """Called when a Call node is visited.""" if not isinstance(node.func, astroid.Attribute) or not isinstance( node.func.expr, astroid.Name ): @@ -67,19 +53,16 @@ class HassLoggerFormatChecker(BaseChecker): return if log_message[-1] == ".": - self.add_message("hass-logger-period", args=node.args, node=node) + self.add_message("hass-logger-period", node=node) if ( isinstance(node.func.attrname, str) and node.func.attrname not in LOG_LEVEL_ALLOWED_LOWER_START and log_message[0].upper() != log_message[0] ): - self.add_message("hass-logger-capital", args=node.args, node=node) + self.add_message("hass-logger-capital", node=node) -def register(linter): - """This required method auto registers the checker. - :param linter: The linter to register the checker to. - :type linter: pylint.lint.PyLinter - """ +def register(linter: PyLinter) -> None: + """Register the checker.""" linter.register_checker(HassLoggerFormatChecker(linter)) diff --git a/pyproject.toml b/pyproject.toml index 0e38a197319..33af823fae4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,8 @@ init-hook='from pylint.config.find_default_config_files import find_default_conf load-plugins = [ "pylint.extensions.typing", "pylint_strict_informational", - "hass_logger" + "hass_constructor", + "hass_logger", ] persistent = false extension-pkg-whitelist = [ From 19d25cd90165125d1e75884db5703dfebb73fb1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 20 May 2021 20:19:20 +0300 Subject: [PATCH 610/852] Change config entry state to an enum (#49654) * Change config entry state to an enum * Allow but deprecate EntryState str equality comparison * Test fixes * Rename to ConfigEntryState * Remove str comparability backcompat * Update new occurrences of strs cropped up during review --- .../components/config/config_entries.py | 2 +- homeassistant/components/dsmr/config_flow.py | 4 +- homeassistant/components/ozw/__init__.py | 6 +- homeassistant/components/ozw/config_flow.py | 2 +- homeassistant/components/plex/__init__.py | 4 +- .../components/somfy_mylink/config_flow.py | 2 +- homeassistant/components/tuya/config_flow.py | 2 +- homeassistant/components/vizio/__init__.py | 4 +- homeassistant/components/zwave_js/api.py | 4 +- homeassistant/config_entries.py | 92 ++++++----- tests/components/abode/test_init.py | 6 +- tests/components/accuweather/test_init.py | 16 +- tests/components/advantage_air/test_init.py | 12 +- tests/components/aemet/test_config_flow.py | 10 +- tests/components/aemet/test_init.py | 6 +- tests/components/airly/test_init.py | 20 +-- tests/components/almond/test_init.py | 8 +- tests/components/atag/test_init.py | 4 +- tests/components/august/test_init.py | 36 ++-- tests/components/blebox/test_config_flow.py | 4 +- tests/components/blebox/test_init.py | 8 +- tests/components/bond/test_init.py | 18 +- tests/components/broadlink/test_device.py | 35 ++-- tests/components/brother/test_init.py | 12 +- tests/components/bsblan/test_init.py | 6 +- tests/components/buienradar/test_init.py | 9 +- tests/components/canary/test_init.py | 12 +- tests/components/cert_expiry/test_init.py | 8 +- tests/components/cert_expiry/test_sensors.py | 4 +- tests/components/cloudflare/test_init.py | 14 +- .../components/config/test_config_entries.py | 30 ++-- tests/components/coronavirus/test_init.py | 4 +- .../devolo_home_control/test_init.py | 17 +- tests/components/dexcom/test_init.py | 6 +- tests/components/directv/test_init.py | 12 +- tests/components/dsmr/test_sensor.py | 5 +- tests/components/dynalite/test_config_flow.py | 8 +- tests/components/eafm/test_sensor.py | 2 +- tests/components/elgato/test_init.py | 4 +- tests/components/freebox/test_init.py | 8 +- tests/components/fritzbox/test_init.py | 16 +- tests/components/gios/test_init.py | 12 +- tests/components/gree/test_init.py | 6 +- .../components/home_plus_control/test_init.py | 8 +- .../home_plus_control/test_switch.py | 4 +- .../specific_devices/test_ecobee3.py | 4 +- .../components/homematicip_cloud/test_hap.py | 4 +- .../components/homematicip_cloud/test_init.py | 15 +- tests/components/huisbaasje/test_init.py | 18 +- tests/components/hyperion/test_light.py | 6 +- tests/components/ialarm/test_init.py | 14 +- tests/components/insteon/test_config_flow.py | 4 +- tests/components/ipp/test_init.py | 12 +- .../islamic_prayer_times/test_init.py | 6 +- tests/components/kmtronic/test_config_flow.py | 6 +- tests/components/kmtronic/test_init.py | 12 +- tests/components/kodi/test_init.py | 6 +- tests/components/litterrobot/test_init.py | 11 +- tests/components/lyric/test_config_flow.py | 2 +- tests/components/mazda/test_init.py | 19 +-- tests/components/met/test_init.py | 12 +- tests/components/met_eireann/test_init.py | 6 +- tests/components/mikrotik/test_hub.py | 2 +- tests/components/nam/test_init.py | 12 +- tests/components/neato/test_config_flow.py | 2 +- tests/components/nest/test_init_sdm.py | 19 +-- tests/components/netatmo/test_init.py | 2 +- tests/components/nightscout/test_init.py | 12 +- tests/components/nzbget/test_init.py | 12 +- tests/components/onewire/test_init.py | 17 +- .../openweathermap/test_config_flow.py | 12 +- tests/components/ozw/common.py | 4 +- tests/components/ozw/conftest.py | 4 +- tests/components/ozw/test_init.py | 26 +-- tests/components/panasonic_viera/test_init.py | 4 +- tests/components/plex/test_config_flow.py | 14 +- tests/components/plex/test_init.py | 37 ++--- .../components/plugwise/test_binary_sensor.py | 8 +- tests/components/plugwise/test_climate.py | 12 +- tests/components/plugwise/test_init.py | 20 +-- tests/components/plugwise/test_sensor.py | 12 +- tests/components/plugwise/test_switch.py | 12 +- tests/components/rfxtrx/test_switch.py | 3 +- tests/components/roku/test_init.py | 12 +- .../components/ruckus_unleashed/test_init.py | 14 +- .../components/smart_meter_texas/test_init.py | 15 +- tests/components/smarttub/test_init.py | 10 +- tests/components/somfy/test_config_flow.py | 4 +- tests/components/sonarr/test_init.py | 16 +- tests/components/speedtestdotnet/test_init.py | 6 +- tests/components/srp_energy/test_init.py | 3 +- tests/components/subaru/conftest.py | 4 +- tests/components/subaru/test_init.py | 21 +-- tests/components/totalconnect/test_init.py | 4 +- tests/components/vera/test_init.py | 4 +- tests/components/volumio/test_config_flow.py | 2 +- tests/components/wilight/test_init.py | 12 +- tests/components/wled/test_init.py | 4 +- tests/components/yeelight/test_init.py | 4 +- tests/components/zwave_js/test_init.py | 41 ++--- tests/test_config_entries.py | 154 +++++++++--------- 101 files changed, 557 insertions(+), 688 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 11c39089f35..efc60288439 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -385,7 +385,7 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "domain": entry.domain, "title": entry.title, "source": entry.source, - "state": entry.state, + "state": entry.state.value, "supports_options": supports_options, "supports_unload": entry.supports_unload, "disabled_by": entry.disabled_by, diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index bdaaea22f64..b349afb28c7 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -149,8 +149,8 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): and reload_on_update and entry.state in ( - config_entries.ENTRY_STATE_LOADED, - config_entries.ENTRY_STATE_SETUP_RETRY, + config_entries.ConfigEntryState.LOADED, + config_entries.ConfigEntryState.SETUP_RETRY, ) ): self.hass.async_create_task( diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 17ab4ca7eb8..7890129dd91 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -22,7 +22,7 @@ from openzwavemqtt.util.mqtt_client import MQTTClient from homeassistant.components import mqtt from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.config_entries import ENTRY_STATE_LOADED, ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C else: mqtt_entries = hass.config_entries.async_entries("mqtt") - if not mqtt_entries or mqtt_entries[0].state != ENTRY_STATE_LOADED: + if not mqtt_entries or mqtt_entries[0].state is not ConfigEntryState.LOADED: _LOGGER.error("MQTT integration is not set up") return False @@ -100,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C @callback def send_message(topic, payload): - if mqtt_entry.state != ENTRY_STATE_LOADED: + if mqtt_entry.state is not ConfigEntryState.LOADED: _LOGGER.error("MQTT integration is not set up") return diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 4ff8a512c75..cc07d738488 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -98,7 +98,7 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): mqtt_entries = self.hass.config_entries.async_entries("mqtt") if ( not mqtt_entries - or mqtt_entries[0].state != config_entries.ENTRY_STATE_LOADED + or mqtt_entries[0].state is not config_entries.ConfigEntryState.LOADED ): return self.async_abort(reason="mqtt_required") return self._async_create_entry_from_vars() diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index c534384a7eb..ffdf9aa5554 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -14,7 +14,7 @@ from plexwebsocket import ( import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -107,7 +107,7 @@ async def async_setup_entry(hass, entry): entry, data={**entry.data, PLEX_SERVER_CONFIG: new_server_data} ) except requests.exceptions.ConnectionError as error: - if entry.state != ENTRY_STATE_SETUP_RETRY: + if entry.state is not ConfigEntryState.SETUP_RETRY: _LOGGER.error( "Plex server (%s) could not be reached: [%s]", server_config[CONF_URL], diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index aac04a34a9a..ecbd9abd402 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -144,7 +144,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" - if self.config_entry.state != config_entries.ENTRY_STATE_LOADED: + if self.config_entry.state is not config_entries.ConfigEntryState.LOADED: _LOGGER.error("MyLink must be connected to manage device options") return self.async_abort(reason="cannot_connect") diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index ee17715b5e9..970a1c54f1e 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -245,7 +245,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" - if self.config_entry.state != config_entries.ENTRY_STATE_LOADED: + if self.config_entry.state is not config_entries.ConfigEntryState.LOADED: _LOGGER.error("Tuya integration not yet loaded") return self.async_abort(reason=RESULT_CONN_ERROR) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index d51b6a4fbca..fb3a399327d 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -10,7 +10,7 @@ from pyvizio.util import gen_apps_list_from_url import voluptuous as vol from homeassistant.components.media_player import DEVICE_CLASS_TV -from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -80,7 +80,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) # Exclude this config entry because its not unloaded yet if not any( - entry.state == ENTRY_STATE_LOADED + entry.state is ConfigEntryState.LOADED and entry.entry_id != config_entry.entry_id and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV for entry in hass.config_entries.async_entries(DOMAIN) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index e722cac9e09..1c2b345faec 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -25,7 +25,7 @@ from homeassistant.components.websocket_api.const import ( ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, ) -from homeassistant.config_entries import ENTRY_STATE_LOADED, ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -85,7 +85,7 @@ def async_get_entry(orig_func: Callable) -> Callable: ) return - if entry.state != ENTRY_STATE_LOADED: + if entry.state is not ConfigEntryState.LOADED: connection.send_error( msg[ID], ERR_NOT_LOADED, f"Config entry {entry_id} not loaded" ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 729d8ae94fa..7719d8edcc9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable, Mapping from contextvars import ContextVar +from enum import Enum import functools import logging from types import MappingProxyType, MethodType @@ -63,20 +64,37 @@ PATH_CONFIG = ".config_entries.json" SAVE_DELAY = 1 -# The config entry has been set up successfully -ENTRY_STATE_LOADED = "loaded" -# There was an error while trying to set up this config entry -ENTRY_STATE_SETUP_ERROR = "setup_error" -# There was an error while trying to migrate the config entry to a new version -ENTRY_STATE_MIGRATION_ERROR = "migration_error" -# The config entry was not ready to be set up yet, but might be later -ENTRY_STATE_SETUP_RETRY = "setup_retry" -# The config entry has not been loaded -ENTRY_STATE_NOT_LOADED = "not_loaded" -# An error occurred when trying to unload the entry -ENTRY_STATE_FAILED_UNLOAD = "failed_unload" -UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD) +class ConfigEntryState(Enum): + """Config entry state.""" + + LOADED = "loaded", True + """The config entry has been set up successfully""" + SETUP_ERROR = "setup_error", True + """There was an error while trying to set up this config entry""" + MIGRATION_ERROR = "migration_error", False + """There was an error while trying to migrate the config entry to a new version""" + SETUP_RETRY = "setup_retry", True + """The config entry was not ready to be set up yet, but might be later""" + NOT_LOADED = "not_loaded", True + """The config entry has not been loaded""" + FAILED_UNLOAD = "failed_unload", False + """An error occurred when trying to unload the entry""" + + _recoverable: bool + + def __new__(cls: type[object], value: str, recoverable: bool) -> ConfigEntryState: + """Create new ConfigEntryState.""" + obj = object.__new__(cls) + obj._value_ = value + obj._recoverable = recoverable + return cast("ConfigEntryState", obj) + + @property + def recoverable(self) -> bool: + """Get if the state is recoverable.""" + return self._recoverable + DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id" DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" @@ -156,7 +174,7 @@ class ConfigEntry: options: Mapping[str, Any] | None = None, unique_id: str | None = None, entry_id: str | None = None, - state: str = ENTRY_STATE_NOT_LOADED, + state: ConfigEntryState = ConfigEntryState.NOT_LOADED, disabled_by: str | None = None, ) -> None: """Initialize a config entry.""" @@ -237,7 +255,7 @@ class ConfigEntry: err, ) if self.domain == integration.domain: - self.state = ENTRY_STATE_SETUP_ERROR + self.state = ConfigEntryState.SETUP_ERROR self.reason = "Import error" return @@ -251,13 +269,13 @@ class ConfigEntry: self.domain, err, ) - self.state = ENTRY_STATE_SETUP_ERROR + self.state = ConfigEntryState.SETUP_ERROR self.reason = "Import error" return # Perform migration if not await self.async_migrate(hass): - self.state = ENTRY_STATE_MIGRATION_ERROR + self.state = ConfigEntryState.MIGRATION_ERROR self.reason = None return @@ -288,7 +306,7 @@ class ConfigEntry: self.async_start_reauth(hass) result = False except ConfigEntryNotReady as ex: - self.state = ENTRY_STATE_SETUP_RETRY + self.state = ConfigEntryState.SETUP_RETRY self.reason = str(ex) or None wait_time = 2 ** min(tries, 4) * 5 tries += 1 @@ -337,10 +355,10 @@ class ConfigEntry: return if result: - self.state = ENTRY_STATE_LOADED + self.state = ConfigEntryState.LOADED self.reason = None else: - self.state = ENTRY_STATE_SETUP_ERROR + self.state = ConfigEntryState.SETUP_ERROR self.reason = error_reason async def async_shutdown(self) -> None: @@ -362,7 +380,7 @@ class ConfigEntry: Returns if unload is possible and was successful. """ if self.source == SOURCE_IGNORE: - self.state = ENTRY_STATE_NOT_LOADED + self.state = ConfigEntryState.NOT_LOADED self.reason = None return True @@ -374,20 +392,20 @@ class ConfigEntry: # that was uninstalled, or an integration # that has been renamed without removing the config # entry. - self.state = ENTRY_STATE_NOT_LOADED + self.state = ConfigEntryState.NOT_LOADED self.reason = None return True component = integration.get_component() if integration.domain == self.domain: - if self.state in UNRECOVERABLE_STATES: + if not self.state.recoverable: return False - if self.state != ENTRY_STATE_LOADED: + if self.state is not ConfigEntryState.LOADED: self.async_cancel_retry_setup() - self.state = ENTRY_STATE_NOT_LOADED + self.state = ConfigEntryState.NOT_LOADED self.reason = None return True @@ -395,7 +413,7 @@ class ConfigEntry: if not supports_unload: if integration.domain == self.domain: - self.state = ENTRY_STATE_FAILED_UNLOAD + self.state = ConfigEntryState.FAILED_UNLOAD self.reason = "Unload not supported" return False @@ -406,7 +424,7 @@ class ConfigEntry: # Only adjust state if we unloaded the component if result and integration.domain == self.domain: - self.state = ENTRY_STATE_NOT_LOADED + self.state = ConfigEntryState.NOT_LOADED self.reason = None self._async_process_on_unload() @@ -417,7 +435,7 @@ class ConfigEntry: "Error unloading entry %s for %s", self.title, integration.domain ) if integration.domain == self.domain: - self.state = ENTRY_STATE_FAILED_UNLOAD + self.state = ConfigEntryState.FAILED_UNLOAD self.reason = "Unknown error" return False @@ -620,10 +638,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): # Unload the entry before setting up the new one. # We will remove it only after the other one is set up, # so that device customizations are not getting lost. - if ( - existing_entry is not None - and existing_entry.state not in UNRECOVERABLE_STATES - ): + if existing_entry is not None and existing_entry.state.recoverable: await self.config_entries.async_unload(existing_entry.entry_id) entry = ConfigEntry( @@ -770,8 +785,8 @@ class ConfigEntries: if entry is None: raise UnknownEntry - if entry.state in UNRECOVERABLE_STATES: - unload_success = entry.state != ENTRY_STATE_FAILED_UNLOAD + if not entry.state.recoverable: + unload_success = entry.state is not ConfigEntryState.FAILED_UNLOAD else: unload_success = await self.async_unload(entry_id) @@ -855,7 +870,7 @@ class ConfigEntries: if entry is None: raise UnknownEntry - if entry.state != ENTRY_STATE_NOT_LOADED: + if entry.state is not ConfigEntryState.NOT_LOADED: raise OperationNotAllowed # Setup Component if not set up yet @@ -870,7 +885,7 @@ class ConfigEntries: if not result: return result - return entry.state == ENTRY_STATE_LOADED + return entry.state is ConfigEntryState.LOADED # type: ignore[comparison-overlap] # mypy bug? async def async_unload(self, entry_id: str) -> bool: """Unload a config entry.""" @@ -879,7 +894,7 @@ class ConfigEntries: if entry is None: raise UnknownEntry - if entry.state in UNRECOVERABLE_STATES: + if not entry.state.recoverable: raise OperationNotAllowed return await entry.async_unload(self.hass) @@ -1115,7 +1130,8 @@ class ConfigFlow(data_entry_flow.FlowHandler): if ( changed and reload_on_update - and entry.state in (ENTRY_STATE_LOADED, ENTRY_STATE_SETUP_RETRY) + and entry.state + in (ConfigEntryState.LOADED, ConfigEntryState.SETUP_RETRY) ): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 41219f5ccef..5e58695ace6 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -11,7 +11,7 @@ from homeassistant.components.abode import ( SERVICE_TRIGGER_AUTOMATION, ) from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_USERNAME, HTTP_BAD_REQUEST from .common import setup_platform @@ -79,7 +79,7 @@ async def test_invalid_credentials(hass): async def test_raise_config_entry_not_ready_when_offline(hass): - """Config entry state is ENTRY_STATE_SETUP_RETRY when abode is offline.""" + """Config entry state is SETUP_RETRY when abode is offline.""" with patch( "homeassistant.components.abode.Abode", side_effect=AbodeException("any"), @@ -87,6 +87,6 @@ async def test_raise_config_entry_not_ready_when_offline(hass): config_entry = await setup_platform(hass, ALARM_DOMAIN) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert hass.config_entries.flow.async_progress() == [] diff --git a/tests/components/accuweather/test_init.py b/tests/components/accuweather/test_init.py index bb45e894e74..d6f76f113b3 100644 --- a/tests/components/accuweather/test_init.py +++ b/tests/components/accuweather/test_init.py @@ -6,11 +6,7 @@ from unittest.mock import patch from accuweather import ApiError from homeassistant.components.accuweather.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.util.dt import utcnow @@ -48,7 +44,7 @@ async def test_config_not_ready(hass): ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass): @@ -56,12 +52,12 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -69,7 +65,7 @@ async def test_update_interval(hass): """Test correct update interval.""" entry = await init_integration(hass) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED current = json.loads(load_fixture("accuweather/current_conditions_data.json")) future = utcnow() + timedelta(minutes=40) @@ -91,7 +87,7 @@ async def test_update_interval_forecast(hass): """Test correct update interval when forecast is True.""" entry = await init_integration(hass, forecast=True) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED current = json.loads(load_fixture("accuweather/current_conditions_data.json")) forecast = json.loads(load_fixture("accuweather/forecast_data.json")) diff --git a/tests/components/advantage_air/test_init.py b/tests/components/advantage_air/test_init.py index 1567b9ee8ad..ca8ecff359e 100644 --- a/tests/components/advantage_air/test_init.py +++ b/tests/components/advantage_air/test_init.py @@ -1,10 +1,6 @@ """Test the Advantage Air Initialization.""" -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from tests.components.advantage_air import ( TEST_SYSTEM_DATA, @@ -22,11 +18,11 @@ async def test_async_setup_entry(hass, aioclient_mock): ) entry = await add_mock_config(hass) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_async_setup_entry_failure(hass, aioclient_mock): @@ -38,4 +34,4 @@ async def test_async_setup_entry_failure(hass, aioclient_mock): ) entry = await add_mock_config(hass) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index 36713a02903..af14c170f40 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -6,7 +6,7 @@ import requests_mock from homeassistant import data_entry_flow from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.util.dt as dt_util @@ -47,7 +47,7 @@ async def test_form(hass): conf_entries = hass.config_entries.async_entries(DOMAIN) entry = conf_entries[0] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] @@ -75,7 +75,7 @@ async def test_form_options(hass): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == "loaded" + assert entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(entry.entry_id) @@ -93,7 +93,7 @@ async def test_form_options(hass): await hass.async_block_till_done() - assert entry.state == "loaded" + assert entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(entry.entry_id) @@ -111,7 +111,7 @@ async def test_form_options(hass): await hass.async_block_till_done() - assert entry.state == "loaded" + assert entry.state is ConfigEntryState.LOADED async def test_form_duplicated_id(hass): diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index f1c6c48f3f3..b1f452c1b46 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import patch import requests_mock from homeassistant.components.aemet.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.util.dt as dt_util @@ -37,8 +37,8 @@ async def test_unload_entry(hass): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index da545eb5193..a20ae6ddd1a 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -5,11 +5,7 @@ import pytest from homeassistant.components.airly import set_update_interval from homeassistant.components.airly.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.util.dt import utcnow @@ -52,7 +48,7 @@ async def test_config_not_ready(hass, aioclient_mock): aioclient_mock.get(API_POINT_URL, exc=ConnectionError()) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_config_without_unique_id(hass, aioclient_mock): @@ -71,7 +67,7 @@ async def test_config_without_unique_id(hass, aioclient_mock): aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.unique_id == "123-456" @@ -92,7 +88,7 @@ async def test_config_with_turned_off_station(hass, aioclient_mock): aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_update_interval(hass, aioclient_mock): @@ -127,7 +123,7 @@ async def test_update_interval(hass, aioclient_mock): assert aioclient_mock.call_count == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED update_interval = set_update_interval(instances, REMAINING_RQUESTS) future = utcnow() + update_interval @@ -164,7 +160,7 @@ async def test_update_interval(hass, aioclient_mock): assert aioclient_mock.call_count == 3 assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED update_interval = set_update_interval(instances, REMAINING_RQUESTS) future = utcnow() + update_interval @@ -181,12 +177,12 @@ async def test_unload_entry(hass, aioclient_mock): entry = await init_integration(hass, aioclient_mock) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py index fb74bdfa7f5..64537aa9465 100644 --- a/tests/components/almond/test_init.py +++ b/tests/components/almond/test_init.py @@ -38,7 +38,7 @@ async def test_set_up_oauth_remote_url(hass, aioclient_mock): ): assert await async_setup_component(hass, "almond", {}) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED hass.config.components.add("cloud") with patch("homeassistant.components.almond.ALMOND_SETUP_DELAY", 0), patch( @@ -71,7 +71,7 @@ async def test_set_up_oauth_no_external_url(hass, aioclient_mock): ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: assert await async_setup_component(hass, "almond", {}) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(mock_create_device.mock_calls) == 0 @@ -90,7 +90,7 @@ async def test_set_up_hassio(hass, aioclient_mock): with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: assert await async_setup_component(hass, "almond", {}) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(mock_create_device.mock_calls) == 0 @@ -112,5 +112,5 @@ async def test_set_up_local(hass, aioclient_mock): with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: assert await async_setup_component(hass, "almond", {}) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(mock_create_device.mock_calls) == 1 diff --git a/tests/components/atag/test_init.py b/tests/components/atag/test_init.py index 7b7f3c1e33a..59f38ae7bfe 100644 --- a/tests/components/atag/test_init.py +++ b/tests/components/atag/test_init.py @@ -1,7 +1,7 @@ """Tests for the ATAG integration.""" from homeassistant.components.atag import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from . import init_integration, mock_connection @@ -15,7 +15,7 @@ async def test_config_entry_not_ready( """Test configuration entry not ready on library error.""" mock_connection(aioclient_mock, conn_error=True) entry = await init_integration(hass, aioclient_mock) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index bc9f0048738..44594239d74 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -9,11 +9,7 @@ from yalexs.exceptions import AugustApiAIOHTTPError from homeassistant import setup from homeassistant.components.august.const import DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_LOCK, @@ -36,7 +32,7 @@ from tests.components.august.mocks import ( async def test_august_is_offline(hass): - """Config entry state is ENTRY_STATE_SETUP_RETRY when august is offline.""" + """Config entry state is SETUP_RETRY when august is offline.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -53,7 +49,7 @@ async def test_august_is_offline(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_unlock_throws_august_api_http_error(hass): @@ -141,7 +137,7 @@ async def test_lock_has_doorsense(hass): async def test_auth_fails(hass): - """Config entry state is ENTRY_STATE_SETUP_ERROR when auth fails.""" + """Config entry state is SETUP_ERROR when auth fails.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -159,7 +155,7 @@ async def test_auth_fails(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() @@ -167,7 +163,7 @@ async def test_auth_fails(hass): async def test_bad_password(hass): - """Config entry state is ENTRY_STATE_SETUP_ERROR when the password has been changed.""" + """Config entry state is SETUP_ERROR when the password has been changed.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -187,7 +183,7 @@ async def test_bad_password(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() @@ -195,7 +191,7 @@ async def test_bad_password(hass): async def test_http_failure(hass): - """Config entry state is ENTRY_STATE_SETUP_RETRY when august is offline.""" + """Config entry state is SETUP_RETRY when august is offline.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -213,13 +209,13 @@ async def test_http_failure(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert hass.config_entries.flow.async_progress() == [] async def test_unknown_auth_state(hass): - """Config entry state is ENTRY_STATE_SETUP_ERROR when august is in an unknown auth state.""" + """Config entry state is SETUP_ERROR when august is in an unknown auth state.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -237,7 +233,7 @@ async def test_unknown_auth_state(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() @@ -245,7 +241,7 @@ async def test_unknown_auth_state(hass): async def test_requires_validation_state(hass): - """Config entry state is ENTRY_STATE_SETUP_ERROR when august requires validation.""" + """Config entry state is SETUP_ERROR when august requires validation.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -265,14 +261,14 @@ async def test_requires_validation_state(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR assert len(hass.config_entries.flow.async_progress()) == 1 assert hass.config_entries.flow.async_progress()[0]["context"]["source"] == "reauth" async def test_unknown_auth_http_401(hass): - """Config entry state is ENTRY_STATE_SETUP_ERROR when august gets an http.""" + """Config entry state is SETUP_ERROR when august gets an http.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -290,7 +286,7 @@ async def test_unknown_auth_http_401(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() @@ -306,7 +302,7 @@ async def test_load_unload(hass): hass, [august_operative_lock, august_inoperative_lock] ) - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index 965c707d2af..03f5d0b4f2a 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -173,7 +173,7 @@ async def test_async_setup_entry(hass, valid_feature_mock): await hass.async_block_till_done() assert hass.config_entries.async_entries() == [config] - assert config.state == config_entries.ENTRY_STATE_LOADED + assert config.state is config_entries.ConfigEntryState.LOADED async def test_async_remove_entry(hass, valid_feature_mock): @@ -189,4 +189,4 @@ async def test_async_remove_entry(hass, valid_feature_mock): await hass.async_block_till_done() assert hass.config_entries.async_entries() == [] - assert config.state == config_entries.ENTRY_STATE_NOT_LOADED + assert config.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/blebox/test_init.py b/tests/components/blebox/test_init.py index 098c10f2cfc..c0add2696b5 100644 --- a/tests/components/blebox/test_init.py +++ b/tests/components/blebox/test_init.py @@ -5,7 +5,7 @@ import logging import blebox_uniapi from homeassistant.components.blebox.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from .conftest import mock_config, patch_product_identify @@ -23,7 +23,7 @@ async def test_setup_failure(hass, caplog): await hass.async_block_till_done() assert "Identify failed at 172.100.123.4:80 ()" in caplog.text - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_failure_on_connection(hass, caplog): @@ -39,7 +39,7 @@ async def test_setup_failure_on_connection(hass, caplog): await hass.async_block_till_done() assert "Identify failed at 172.100.123.4:80 ()" in caplog.text - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry(hass): @@ -57,4 +57,4 @@ async def test_unload_config_entry(hass): await hass.async_block_till_done() assert not hass.data.get(DOMAIN) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 0bba04b4d97..4ba105248df 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -5,11 +5,7 @@ from aiohttp import ClientConnectionError, ClientResponseError from bond_api import DeviceType from homeassistant.components.bond.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -47,7 +43,7 @@ async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant): with patch_bond_version(side_effect=ClientConnectionError()): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAssistant): @@ -75,7 +71,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss await hass.async_block_till_done() assert config_entry.entry_id in hass.data[DOMAIN] - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "test-bond-id" # verify hub device is registered correctly @@ -115,7 +111,7 @@ async def test_unload_config_entry(hass: HomeAssistant): await hass.async_block_till_done() assert config_entry.entry_id not in hass.data[DOMAIN] - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_old_identifiers_are_removed(hass: HomeAssistant): @@ -159,7 +155,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): await hass.async_block_till_done() assert config_entry.entry_id in hass.data[DOMAIN] - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "test-bond-id" # verify the device info is cleaned up @@ -201,7 +197,7 @@ async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant): await hass.async_block_till_done() assert config_entry.entry_id in hass.data[DOMAIN] - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "test-bond-id" device_registry = dr.async_get(hass) @@ -247,7 +243,7 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant): await hass.async_block_till_done() assert config_entry.entry_id in hass.data[DOMAIN] - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "test-bond-id" device_registry = dr.async_get(hass) diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index 8e53fd74c1c..2ee8f7a5218 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -5,12 +5,7 @@ import broadlink.exceptions as blke from homeassistant.components.broadlink.const import DOMAIN from homeassistant.components.broadlink.device import get_domains -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers.entity_registry import async_entries_for_device from . import get_device @@ -29,7 +24,7 @@ async def test_device_setup(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass) - assert mock_entry.state == ENTRY_STATE_LOADED + assert mock_entry.state == ConfigEntryState.LOADED assert mock_api.auth.call_count == 1 assert mock_api.get_fwversion.call_count == 1 forward_entries = {c[1][1] for c in mock_forward.mock_calls} @@ -52,7 +47,7 @@ async def test_device_setup_authentication_error(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_ERROR + assert mock_entry.state == ConfigEntryState.SETUP_ERROR assert mock_api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 1 @@ -76,7 +71,7 @@ async def test_device_setup_network_timeout(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -95,7 +90,7 @@ async def test_device_setup_os_error(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -114,7 +109,7 @@ async def test_device_setup_broadlink_exception(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_ERROR + assert mock_entry.state is ConfigEntryState.SETUP_ERROR assert mock_api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -133,7 +128,7 @@ async def test_device_setup_update_network_timeout(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_api.auth.call_count == 1 assert mock_api.check_sensors.call_count == 1 assert mock_forward.call_count == 0 @@ -156,7 +151,7 @@ async def test_device_setup_update_authorization_error(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_LOADED + assert mock_entry.state is ConfigEntryState.LOADED assert mock_api.auth.call_count == 2 assert mock_api.check_sensors.call_count == 2 forward_entries = {c[1][1] for c in mock_forward.mock_calls} @@ -180,7 +175,7 @@ async def test_device_setup_update_authentication_error(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_api.auth.call_count == 2 assert mock_api.check_sensors.call_count == 1 assert mock_forward.call_count == 0 @@ -205,7 +200,7 @@ async def test_device_setup_update_broadlink_exception(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_api.auth.call_count == 1 assert mock_api.check_sensors.call_count == 1 assert mock_forward.call_count == 0 @@ -221,7 +216,7 @@ async def test_device_setup_get_fwversion_broadlink_exception(hass): with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_LOADED + assert mock_entry.state is ConfigEntryState.LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} domains = get_domains(mock_api.type) assert mock_forward.call_count == len(domains) @@ -237,7 +232,7 @@ async def test_device_setup_get_fwversion_os_error(hass): with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: _, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_LOADED + assert mock_entry.state is ConfigEntryState.LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} domains = get_domains(mock_api.type) assert mock_forward.call_count == len(domains) @@ -279,7 +274,7 @@ async def test_device_unload_works(hass): ) as mock_forward: await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == ENTRY_STATE_NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} domains = get_domains(mock_api.type) assert mock_forward.call_count == len(domains) @@ -302,7 +297,7 @@ async def test_device_unload_authentication_error(hass): ) as mock_forward: await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == ENTRY_STATE_NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED assert mock_forward.call_count == 0 @@ -320,7 +315,7 @@ async def test_device_unload_update_failed(hass): ) as mock_forward: await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == ENTRY_STATE_NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED assert mock_forward.call_count == 0 diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index 7b85586ce28..76b999c3e54 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -2,11 +2,7 @@ from unittest.mock import patch from homeassistant.components.brother.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_TYPE, STATE_UNAVAILABLE from tests.common import MockConfigEntry @@ -35,7 +31,7 @@ async def test_config_not_ready(hass): with patch("brother.Brother._get_data", side_effect=ConnectionError()): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass): @@ -43,10 +39,10 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py index b6096ced0ac..c7937daf786 100644 --- a/tests/components/bsblan/test_init.py +++ b/tests/components/bsblan/test_init.py @@ -2,7 +2,7 @@ import aiohttp from homeassistant.components.bsblan.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.bsblan import init_integration, init_integration_without_auth @@ -19,7 +19,7 @@ async def test_config_entry_not_ready( ) entry = await init_integration(hass, aioclient_mock) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( @@ -44,4 +44,4 @@ async def test_config_entry_no_authentication( ) entry = await init_integration_without_auth(hass, aioclient_mock) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/buienradar/test_init.py b/tests/components/buienradar/test_init.py index ea91291a3de..0c25fcc1886 100644 --- a/tests/components/buienradar/test_init.py +++ b/tests/components/buienradar/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch from homeassistant import setup from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers.entity_registry import async_get_registry @@ -39,7 +40,7 @@ async def test_import_all(hass): entry = conf_entries[0] - assert entry.state == "loaded" + assert entry.state is ConfigEntryState.LOADED assert entry.data == { "latitude": hass.config.latitude, "longitude": hass.config.longitude, @@ -77,7 +78,7 @@ async def test_import_camera(hass): entry = conf_entries[0] - assert entry.state == "loaded" + assert entry.state is ConfigEntryState.LOADED assert entry.data == { "latitude": hass.config.latitude, "longitude": hass.config.longitude, @@ -112,9 +113,9 @@ async def test_load_unload(aioclient_mock, hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == "loaded" + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == "not_loaded" + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index a767eb0ec51..21b897509eb 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -5,11 +5,7 @@ from requests import ConnectTimeout from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN from homeassistant.components.canary.const import CONF_FFMPEG_ARGUMENTS, DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.setup import async_setup_component @@ -64,12 +60,12 @@ async def test_unload_entry(hass, canary): assert entry assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -79,4 +75,4 @@ async def test_async_setup_raises_entry_not_ready(hass, canary): entry = await init_integration(hass) assert entry - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index 1c62782107b..dbe5e74d891 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -63,7 +63,7 @@ async def test_update_unique_id(hass): assert await async_setup_component(hass, DOMAIN, {}) is True await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.unique_id == f"{HOST}:{PORT}" @@ -89,7 +89,7 @@ async def test_unload_config_entry(mock_now, hass): assert await async_setup_component(hass, DOMAIN, {}) is True await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state.state == timestamp.isoformat() assert state.attributes.get("error") == "None" @@ -97,7 +97,7 @@ async def test_unload_config_entry(mock_now, hass): await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py index 375b676eaf8..099fe78ca39 100644 --- a/tests/components/cert_expiry/test_sensors.py +++ b/tests/components/cert_expiry/test_sensors.py @@ -5,7 +5,7 @@ import ssl from unittest.mock import patch from homeassistant.components.cert_expiry.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.util.dt import utcnow @@ -81,7 +81,7 @@ async def test_async_setup_entry_host_unavailable(hass): assert await hass.config_entries.async_setup(entry.entry_id) is False await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY next_update = utcnow() + timedelta(seconds=45) async_fire_time_changed(hass, next_update) diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index 1fb4af7f9aa..5a42ca9f09c 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -2,11 +2,7 @@ from pycfdns.exceptions import CloudflareConnectionException from homeassistant.components.cloudflare.const import DOMAIN, SERVICE_UPDATE_RECORDS -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from . import ENTRY_CONFIG, init_integration @@ -18,12 +14,12 @@ async def test_unload_entry(hass, cfupdate): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -37,7 +33,7 @@ async def test_async_setup_raises_entry_not_ready(hass, cfupdate): instance.get_zone_id.side_effect = CloudflareConnectionException() await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_integration_services(hass, cfupdate): @@ -45,7 +41,7 @@ async def test_integration_services(hass, cfupdate): instance = cfupdate.return_value entry = await init_integration(hass) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( DOMAIN, diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 1ad2bd978fc..10fc3aadba0 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -64,7 +64,7 @@ async def test_get_entries(hass, client): domain="comp2", title="Test 2", source="bla2", - state=core_ce.ENTRY_STATE_SETUP_ERROR, + state=core_ce.ConfigEntryState.SETUP_ERROR, reason="Unsupported API", ).add_to_hass(hass) MockConfigEntry( @@ -84,7 +84,7 @@ async def test_get_entries(hass, client): "domain": "comp1", "title": "Test 1", "source": "bla", - "state": "not_loaded", + "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": True, "supports_unload": True, "disabled_by": None, @@ -94,7 +94,7 @@ async def test_get_entries(hass, client): "domain": "comp2", "title": "Test 2", "source": "bla2", - "state": "setup_error", + "state": core_ce.ConfigEntryState.SETUP_ERROR.value, "supports_options": False, "supports_unload": False, "disabled_by": None, @@ -104,7 +104,7 @@ async def test_get_entries(hass, client): "domain": "comp3", "title": "Test 3", "source": "bla3", - "state": "not_loaded", + "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": False, "supports_unload": False, "disabled_by": core_ce.DISABLED_USER, @@ -115,7 +115,7 @@ async def test_get_entries(hass, client): async def test_remove_entry(hass, client): """Test removing an entry via the API.""" - entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) resp = await client.delete(f"/api/config/config_entries/entry/{entry.entry_id}") assert resp.status == 200 @@ -126,7 +126,7 @@ async def test_remove_entry(hass, client): async def test_reload_entry(hass, client): """Test reloading an entry via the API.""" - entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" @@ -146,7 +146,7 @@ async def test_reload_invalid_entry(hass, client): async def test_remove_entry_unauth(hass, client, hass_admin_user): """Test removing an entry via the API.""" hass_admin_user.groups = [] - entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) resp = await client.delete(f"/api/config/config_entries/entry/{entry.entry_id}") assert resp.status == 401 @@ -156,7 +156,7 @@ async def test_remove_entry_unauth(hass, client, hass_admin_user): async def test_reload_entry_unauth(hass, client, hass_admin_user): """Test reloading an entry via the API.""" hass_admin_user.groups = [] - entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" @@ -167,7 +167,7 @@ async def test_reload_entry_unauth(hass, client, hass_admin_user): async def test_reload_entry_in_failed_state(hass, client, hass_admin_user): """Test reloading an entry via the API that has already failed to unload.""" - entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_FAILED_UNLOAD) + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.FAILED_UNLOAD) entry.add_to_hass(hass) resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" @@ -325,7 +325,7 @@ async def test_create_account(hass, client, enable_custom_integrations): "domain": "test", "entry_id": entries[0].entry_id, "source": core_ce.SOURCE_USER, - "state": "loaded", + "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, "title": "Test Entry", @@ -396,7 +396,7 @@ async def test_two_step_flow(hass, client, enable_custom_integrations): "domain": "test", "entry_id": entries[0].entry_id, "source": core_ce.SOURCE_USER, - "state": "loaded", + "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, "title": "user-title", @@ -788,7 +788,7 @@ async def test_disable_entry(hass, hass_ws_client): assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) - entry = MockConfigEntry(domain="demo", state="loaded") + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) assert entry.disabled_by is None @@ -806,7 +806,7 @@ async def test_disable_entry(hass, hass_ws_client): assert response["success"] assert response["result"] == {"require_restart": True} assert entry.disabled_by == core_ce.DISABLED_USER - assert entry.state == "failed_unload" + assert entry.state is core_ce.ConfigEntryState.FAILED_UNLOAD # Enable await ws_client.send_json( @@ -822,7 +822,7 @@ async def test_disable_entry(hass, hass_ws_client): assert response["success"] assert response["result"] == {"require_restart": True} assert entry.disabled_by is None - assert entry.state == "failed_unload" + assert entry.state == core_ce.ConfigEntryState.FAILED_UNLOAD # Enable again -> no op await ws_client.send_json( @@ -838,7 +838,7 @@ async def test_disable_entry(hass, hass_ws_client): assert response["success"] assert response["result"] == {"require_restart": False} assert entry.disabled_by is None - assert entry.state == "failed_unload" + assert entry.state == core_ce.ConfigEntryState.FAILED_UNLOAD async def test_disable_entry_nonexisting(hass, hass_ws_client): diff --git a/tests/components/coronavirus/test_init.py b/tests/components/coronavirus/test_init.py index c36255db9d1..eeb91e77239 100644 --- a/tests/components/coronavirus/test_init.py +++ b/tests/components/coronavirus/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from aiohttp import ClientError from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -69,4 +69,4 @@ async def test_config_entry_not_ready( assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/devolo_home_control/test_init.py b/tests/components/devolo_home_control/test_init.py index 3f124ee2098..657836f9d16 100644 --- a/tests/components/devolo_home_control/test_init.py +++ b/tests/components/devolo_home_control/test_init.py @@ -4,12 +4,7 @@ from unittest.mock import patch from devolo_home_control_api.exceptions.gateway import GatewayOfflineError import pytest -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.devolo_home_control import configure_integration @@ -20,7 +15,7 @@ async def test_setup_entry(hass: HomeAssistant): entry = configure_integration(hass) with patch("homeassistant.components.devolo_home_control.HomeControl"): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED @pytest.mark.credentials_invalid @@ -28,7 +23,7 @@ async def test_setup_entry_credentials_invalid(hass: HomeAssistant): """Test setup entry fails if credentials are invalid.""" entry = configure_integration(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR @pytest.mark.maintenance @@ -36,7 +31,7 @@ async def test_setup_entry_maintenance(hass: HomeAssistant): """Test setup entry fails if mydevolo is in maintenance mode.""" entry = configure_integration(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_gateway_offline(hass: HomeAssistant): @@ -47,7 +42,7 @@ async def test_setup_gateway_offline(hass: HomeAssistant): side_effect=GatewayOfflineError, ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass: HomeAssistant): @@ -57,4 +52,4 @@ async def test_unload_entry(hass: HomeAssistant): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/dexcom/test_init.py b/tests/components/dexcom/test_init.py index a155450bf26..2509ba25f33 100644 --- a/tests/components/dexcom/test_init.py +++ b/tests/components/dexcom/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pydexcom import AccountError, SessionError from homeassistant.components.dexcom.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from tests.common import MockConfigEntry from tests.components.dexcom import CONFIG, init_integration @@ -55,10 +55,10 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/directv/test_init.py b/tests/components/directv/test_init.py index 96fd27a30eb..3ef151f4257 100644 --- a/tests/components/directv/test_init.py +++ b/tests/components/directv/test_init.py @@ -1,10 +1,6 @@ """Tests for the DirecTV integration.""" from homeassistant.components.directv.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.directv import setup_integration @@ -19,7 +15,7 @@ async def test_config_entry_not_ready( """Test the DirecTV configuration entry not ready.""" entry = await setup_integration(hass, aioclient_mock, setup_error=True) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( @@ -29,10 +25,10 @@ async def test_unload_config_entry( entry = await setup_integration(hass, aioclient_mock) assert entry.entry_id in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index e037585f8ff..29ab29a0af6 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -11,6 +11,7 @@ from decimal import Decimal from itertools import chain, repeat from unittest.mock import DEFAULT, MagicMock +from homeassistant import config_entries from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.dsmr.sensor import DerivativeDSMREntity from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -59,7 +60,7 @@ async def test_setup_platform(hass, dsmr_connection_fixture): entry = conf_entries[0] - assert entry.state == "loaded" + assert entry.state == config_entries.ConfigEntryState.LOADED assert entry.data == {**entry_data, **serial_data} @@ -625,4 +626,4 @@ async def test_reconnect(hass, dsmr_connection_fixture): await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == "not_loaded" + assert mock_entry.state == config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index e21c82d7c20..a513fe27567 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -13,9 +13,9 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( "first_con, second_con,exp_type, exp_result, exp_reason", [ - (True, True, "create_entry", "loaded", ""), - (False, False, "abort", "", "no_connection"), - (True, False, "create_entry", "setup_retry", ""), + (True, True, "create_entry", config_entries.ConfigEntryState.LOADED, ""), + (False, False, "abort", None, "no_connection"), + (True, False, "create_entry", config_entries.ConfigEntryState.SETUP_RETRY, ""), ], ) async def test_flow(hass, first_con, second_con, exp_type, exp_result, exp_reason): @@ -104,4 +104,4 @@ async def test_two_entries(hass): data={dynalite.CONF_HOST: host2}, ) assert result["type"] == "create_entry" - assert result["result"].state == "loaded" + assert result["result"].state == config_entries.ConfigEntryState.LOADED diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py index 6f5dcaa9284..d8d48367a64 100644 --- a/tests/components/eafm/test_sensor.py +++ b/tests/components/eafm/test_sensor.py @@ -35,7 +35,7 @@ async def async_setup_test_fixture(hass, mock_get_station, initial_value): entry.add_to_hass(hass) assert await async_setup_component(hass, "eafm", {}) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED await hass.async_block_till_done() async def poll(value): diff --git a/tests/components/elgato/test_init.py b/tests/components/elgato/test_init.py index 069e533c423..f764ecdba80 100644 --- a/tests/components/elgato/test_init.py +++ b/tests/components/elgato/test_init.py @@ -2,7 +2,7 @@ import aiohttp from homeassistant.components.elgato.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.elgato import init_integration @@ -18,7 +18,7 @@ async def test_config_entry_not_ready( ) entry = await init_integration(hass, aioclient_mock) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( diff --git a/tests/components/freebox/test_init.py b/tests/components/freebox/test_init.py index 6b5589ac647..44af000f79a 100644 --- a/tests/components/freebox/test_init.py +++ b/tests/components/freebox/test_init.py @@ -5,7 +5,7 @@ from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN from homeassistant.components.freebox.const import DOMAIN as DOMAIN, SERVICE_REBOOT from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -85,7 +85,7 @@ async def test_unload_remove(hass: HomeAssistant, router: Mock): assert await async_setup_component(hass, DOMAIN, {}) is True await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state_dt = hass.states.get(entity_id_dt) assert state_dt state_sensor = hass.states.get(entity_id_sensor) @@ -95,7 +95,7 @@ async def test_unload_remove(hass: HomeAssistant, router: Mock): await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED state_dt = hass.states.get(entity_id_dt) assert state_dt.state == STATE_UNAVAILABLE state_sensor = hass.states.get(entity_id_sensor) @@ -110,7 +110,7 @@ async def test_unload_remove(hass: HomeAssistant, router: Mock): await hass.async_block_till_done() assert router().close.call_count == 1 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED state_dt = hass.states.get(entity_id_dt) assert state_dt is None state_sensor = hass.states.get(entity_id_sensor) diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 14df6f869f8..438335868cd 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -8,11 +8,7 @@ from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -94,14 +90,14 @@ async def test_unload_remove(hass: HomeAssistant, fritz: Mock): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state await hass.config_entries.async_unload(entry.entry_id) assert fritz().logout.call_count == 1 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(entity_id) assert state.state == STATE_UNAVAILABLE @@ -109,13 +105,13 @@ async def test_unload_remove(hass: HomeAssistant, fritz: Mock): await hass.async_block_till_done() assert fritz().logout.call_count == 1 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(entity_id) assert state is None async def test_raise_config_entry_not_ready_when_offline(hass: HomeAssistant): - """Config entry state is ENTRY_STATE_SETUP_RETRY when fritzbox is offline.""" + """Config entry state is SETUP_RETRY when fritzbox is offline.""" entry = MockConfigEntry( domain=FB_DOMAIN, data={CONF_HOST: "any", **MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0]}, @@ -132,4 +128,4 @@ async def test_raise_config_entry_not_ready_when_offline(hass: HomeAssistant): entries = hass.config_entries.async_entries() config_entry = entries[0] - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py index 85834571c86..08629608cd4 100644 --- a/tests/components/gios/test_init.py +++ b/tests/components/gios/test_init.py @@ -3,11 +3,7 @@ import json from unittest.mock import patch from homeassistant.components.gios.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from . import STATIONS @@ -41,7 +37,7 @@ async def test_config_not_ready(hass): ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass): @@ -49,12 +45,12 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/gree/test_init.py b/tests/components/gree/test_init.py index 7443ae1e94c..82b082b03cb 100644 --- a/tests/components/gree/test_init.py +++ b/tests/components/gree/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -25,7 +25,7 @@ async def test_setup_simple(hass): assert len(climate_setup.mock_calls) == 1 assert len(switch_setup.mock_calls) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED # No flows started assert len(hass.config_entries.flow.async_progress()) == 0 @@ -43,4 +43,4 @@ async def test_unload_config_entry(hass): await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/home_plus_control/test_init.py b/tests/components/home_plus_control/test_init.py index e48a9dc1f85..4da913047a2 100644 --- a/tests/components/home_plus_control/test_init.py +++ b/tests/components/home_plus_control/test_init.py @@ -36,7 +36,7 @@ async def test_loading(hass, mock_config_entry): await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 - assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED + assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED async def test_loading_with_no_config(hass, mock_config_entry): @@ -44,7 +44,7 @@ async def test_loading_with_no_config(hass, mock_config_entry): mock_config_entry.add_to_hass(hass) await setup.async_setup_component(hass, DOMAIN, {}) # Component setup fails because the oauth2 implementation could not be registered - assert mock_config_entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert mock_config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR async def test_unloading(hass, mock_config_entry): @@ -68,8 +68,8 @@ async def test_unloading(hass, mock_config_entry): await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 - assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED + assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED # We now unload the entry assert await hass.config_entries.async_unload(mock_config_entry.entry_id) - assert mock_config_entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert mock_config_entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/home_plus_control/test_switch.py b/tests/components/home_plus_control/test_switch.py index f699fe08d05..aec23f0d32a 100644 --- a/tests/components/home_plus_control/test_switch.py +++ b/tests/components/home_plus_control/test_switch.py @@ -388,7 +388,7 @@ async def test_initial_api_error( assert len(mock_check.mock_calls) == 1 # The component has been loaded - assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED + assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED # Check the entities and devices - None have been configured entity_assertions(hass, num_exp_entities=0) @@ -421,7 +421,7 @@ async def test_update_with_api_error( assert len(mock_check.mock_calls) == 1 # The component has been loaded - assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED + assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED # Check the entities and devices - all entities should be there entity_assertions( diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index ae050f67324..cc6c7ae4b9f 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -14,7 +14,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.components.homekit_controller.common import ( @@ -143,7 +143,7 @@ async def test_ecobee3_setup_connection_failure(hass): # If there is no cached entity map and the accessory connection is # failing then we have to fail the config entry setup. config_entry, pairing = await setup_test_accessories(hass, accessories) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY climate = entity_registry.async_get("climate.homew") assert climate is None diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 1f85626980c..a8b3229e8db 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -18,7 +18,7 @@ from homeassistant.components.homematicip_cloud.hap import ( HomematicipAuth, HomematicipHAP, ) -from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.exceptions import ConfigEntryNotReady from .helper import HAPID, HAPPIN @@ -109,7 +109,7 @@ async def test_hap_reset_unloads_entry_if_setup(hass, default_mock_hap_factory): # hap_reset is called during unload await hass.config_entries.async_unload(config_entries[0].entry_id) # entry is unloaded - assert config_entries[0].state == ENTRY_STATE_NOT_LOADED + assert config_entries[0].state is ConfigEntryState.NOT_LOADED assert hass.data[HMIPC_DOMAIN] == {} diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 250cba81637..5354070c062 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -13,12 +13,7 @@ from homeassistant.components.homematicip_cloud.const import ( HMIPC_NAME, ) from homeassistant.components.homematicip_cloud.hap import HomematicipHAP -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component @@ -116,7 +111,7 @@ async def test_load_entry_fails_due_to_connection_error( assert await async_setup_component(hass, HMIPC_DOMAIN, {}) assert hass.data[HMIPC_DOMAIN][hmip_config_entry.unique_id] - assert hmip_config_entry.state == ENTRY_STATE_SETUP_RETRY + assert hmip_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_load_entry_fails_due_to_generic_exception(hass, hmip_config_entry): @@ -132,7 +127,7 @@ async def test_load_entry_fails_due_to_generic_exception(hass, hmip_config_entry assert await async_setup_component(hass, HMIPC_DOMAIN, {}) assert hass.data[HMIPC_DOMAIN][hmip_config_entry.unique_id] - assert hmip_config_entry.state == ENTRY_STATE_SETUP_ERROR + assert hmip_config_entry.state is ConfigEntryState.SETUP_ERROR async def test_unload_entry(hass): @@ -157,9 +152,9 @@ async def test_unload_entry(hass): assert hass.data[HMIPC_DOMAIN]["ABC123"] config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 1 - assert config_entries[0].state == ENTRY_STATE_LOADED + assert config_entries[0].state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entries[0].entry_id) - assert config_entries[0].state == ENTRY_STATE_NOT_LOADED + assert config_entries[0].state is ConfigEntryState.NOT_LOADED assert mock_hap.return_value.mock_calls[2][0] == "async_reset" # entry is unloaded assert hass.data[HMIPC_DOMAIN] == {} diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index 1c081936cb8..dde62a9c78b 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -4,11 +4,7 @@ from unittest.mock import patch from huisbaasje import HuisbaasjeException from homeassistant.components import huisbaasje -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -49,12 +45,12 @@ async def test_setup_entry(hass: HomeAssistant): ) config_entry.add_to_hass(hass) - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # Assert integration is loaded - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED assert huisbaasje.DOMAIN in hass.config.components assert huisbaasje.DOMAIN in hass.data assert config_entry.entry_id in hass.data[huisbaasje.DOMAIN] @@ -89,12 +85,12 @@ async def test_setup_entry_error(hass: HomeAssistant): ) config_entry.add_to_hass(hass) - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # Assert integration is loaded with error - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR assert huisbaasje.DOMAIN not in hass.data # Assert entities are not loaded @@ -133,13 +129,13 @@ async def test_unload_entry(hass: HomeAssistant): # Load config entry assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED entities = hass.states.async_entity_ids("sensor") assert len(entities) == 14 # Unload config entry await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED entities = hass.states.async_entity_ids("sensor") assert len(entities) == 14 for entity in entities: diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index de0110cb19f..0c6b2cf41df 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -25,10 +25,10 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, ) from homeassistant.config_entries import ( - ENTRY_STATE_SETUP_ERROR, RELOAD_AFTER_UPDATE_DELAY, SOURCE_REAUTH, ConfigEntry, + ConfigEntryState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -900,7 +900,7 @@ async def test_setup_entry_no_token_reauth(hass: HomeAssistant) -> None: }, data=config_entry.data, ) - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_setup_entry_bad_token_reauth(hass: HomeAssistant) -> None: @@ -928,7 +928,7 @@ async def test_setup_entry_bad_token_reauth(hass: HomeAssistant) -> None: }, data=config_entry.data, ) - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_priority_light_async_updates( diff --git a/tests/components/ialarm/test_init.py b/tests/components/ialarm/test_init.py index 8998b4e0d18..f33234c7256 100644 --- a/tests/components/ialarm/test_init.py +++ b/tests/components/ialarm/test_init.py @@ -5,11 +5,7 @@ from uuid import uuid4 import pytest from homeassistant.components.ialarm.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT from tests.common import MockConfigEntry @@ -41,7 +37,7 @@ async def test_setup_entry(hass, ialarm_api, mock_config_entry): await hass.async_block_till_done() ialarm_api.return_value.get_mac.assert_called_once() - assert mock_config_entry.state == ENTRY_STATE_LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED async def test_setup_not_ready(hass, ialarm_api, mock_config_entry): @@ -51,7 +47,7 @@ async def test_setup_not_ready(hass, ialarm_api, mock_config_entry): mock_config_entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass, ialarm_api, mock_config_entry): @@ -62,6 +58,6 @@ async def test_unload_entry(hass, ialarm_api, mock_config_entry): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ENTRY_STATE_LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(mock_config_entry.entry_id) - assert mock_config_entry.state == ENTRY_STATE_NOT_LOADED + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 796c9b69d59..172cb6936b3 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -110,7 +110,7 @@ async def test_fail_on_existing(hass: HomeAssistant): options={}, ) config_entry.add_to_hass(hass) - assert config_entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_init( DOMAIN, @@ -276,7 +276,7 @@ async def test_import_existing(hass: HomeAssistant): options={}, ) config_entry.add_to_hass(hass) - assert config_entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await _import_config( hass, {**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2} diff --git a/tests/components/ipp/test_init.py b/tests/components/ipp/test_init.py index f06be4fc8b5..5caffc62d7b 100644 --- a/tests/components/ipp/test_init.py +++ b/tests/components/ipp/test_init.py @@ -1,10 +1,6 @@ """Tests for the IPP integration.""" from homeassistant.components.ipp.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.ipp import init_integration @@ -16,7 +12,7 @@ async def test_config_entry_not_ready( ) -> None: """Test the IPP configuration entry not ready.""" entry = await init_integration(hass, aioclient_mock, conn_error=True) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( @@ -27,10 +23,10 @@ async def test_unload_config_entry( assert hass.data[DOMAIN] assert entry.entry_id in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 850edc4b76d..8ca16c50467 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -52,7 +52,7 @@ async def test_successful_config_entry(hass, legacy_patchable_time): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.options == { islamic_prayer_times.CONF_CALC_METHOD: islamic_prayer_times.DEFAULT_CALC_METHOD } @@ -74,7 +74,7 @@ async def test_setup_failed(hass, legacy_patchable_time): ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass, legacy_patchable_time): @@ -93,7 +93,7 @@ async def test_unload_entry(hass, legacy_patchable_time): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert islamic_prayer_times.DOMAIN not in hass.data diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index 71482d6f7b2..b2f0f4b0515 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -5,7 +5,7 @@ from aiohttp import ClientConnectorError, ClientResponseError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.kmtronic.const import CONF_REVERSE, DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from tests.common import MockConfigEntry @@ -65,7 +65,7 @@ async def test_form_options(hass, aioclient_mock): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -80,7 +80,7 @@ async def test_form_options(hass, aioclient_mock): await hass.async_block_till_done() - assert config_entry.state == "loaded" + assert config_entry.state == config_entries.ConfigEntryState.LOADED async def test_form_invalid_auth(hass): diff --git a/tests/components/kmtronic/test_init.py b/tests/components/kmtronic/test_init.py index 1b9cf7cb407..da1efa25d58 100644 --- a/tests/components/kmtronic/test_init.py +++ b/tests/components/kmtronic/test_init.py @@ -2,11 +2,7 @@ import asyncio from homeassistant.components.kmtronic.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from tests.common import MockConfigEntry @@ -31,12 +27,12 @@ async def test_unload_config_entry(hass, aioclient_mock): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_not_ready(hass, aioclient_mock): @@ -59,4 +55,4 @@ async def test_config_entry_not_ready(hass, aioclient_mock): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/kodi/test_init.py b/tests/components/kodi/test_init.py index aa206270d35..6294eea45df 100644 --- a/tests/components/kodi/test_init.py +++ b/tests/components/kodi/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant.components.kodi.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from . import init_integration @@ -16,10 +16,10 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index 22a6ea21022..d8ca690d965 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -10,10 +10,7 @@ from homeassistant.components.vacuum import ( SERVICE_START, STATE_DOCKED, ) -from homeassistant.config_entries import ( - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID from .common import CONFIG, VACUUM_ENTITY_ID @@ -46,8 +43,8 @@ async def test_unload_entry(hass, mock_account): @pytest.mark.parametrize( "side_effect,expected_state", ( - (LitterRobotLoginException, ENTRY_STATE_SETUP_ERROR), - (LitterRobotException, ENTRY_STATE_SETUP_RETRY), + (LitterRobotLoginException, ConfigEntryState.SETUP_ERROR), + (LitterRobotException, ConfigEntryState.SETUP_RETRY), ), ) async def test_entry_not_setup(hass, side_effect, expected_state): @@ -63,4 +60,4 @@ async def test_entry_not_setup(hass, side_effect, expected_state): side_effect=side_effect, ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == expected_state + assert entry.state is expected_state diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index f5f41da08fd..c8197e9d678 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -109,7 +109,7 @@ async def test_full_flow( assert DOMAIN in hass.config.components entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index c8c631b48af..c9370459f1c 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -6,12 +6,7 @@ from unittest.mock import patch from pymazda import MazdaAuthenticationException, MazdaException from homeassistant.components.mazda.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_EMAIL, CONF_PASSWORD, @@ -44,7 +39,7 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_init_auth_failure(hass: HomeAssistant): @@ -61,7 +56,7 @@ async def test_init_auth_failure(hass: HomeAssistant): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -93,7 +88,7 @@ async def test_update_auth_failure(hass: HomeAssistant): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_LOADED + assert entries[0].state is ConfigEntryState.LOADED with patch( "homeassistant.components.mazda.MazdaAPI.get_vehicles", @@ -132,7 +127,7 @@ async def test_update_general_failure(hass: HomeAssistant): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_LOADED + assert entries[0].state is ConfigEntryState.LOADED with patch( "homeassistant.components.mazda.MazdaAPI.get_vehicles", @@ -153,11 +148,11 @@ async def test_unload_config_entry(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_LOADED + assert entries[0].state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert entries[0].state == ENTRY_STATE_NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED async def test_device_nickname(hass): diff --git a/tests/components/met/test_init.py b/tests/components/met/test_init.py index 074293249c8..64323af56ce 100644 --- a/tests/components/met/test_init.py +++ b/tests/components/met/test_init.py @@ -5,11 +5,7 @@ from homeassistant.components.met.const import ( DOMAIN, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, -) +from homeassistant.config_entries import ConfigEntryState from . import init_integration @@ -19,12 +15,12 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -41,7 +37,7 @@ async def test_fail_default_home_entry(hass, caplog): entry = await init_integration(hass, track_home=True) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert ( "Skip setting up met.no integration; No Home location has been set" diff --git a/tests/components/met_eireann/test_init.py b/tests/components/met_eireann/test_init.py index 8f95013cd72..c5d95ca14ca 100644 --- a/tests/components/met_eireann/test_init.py +++ b/tests/components/met_eireann/test_init.py @@ -1,6 +1,6 @@ """Test the Met Éireann integration init.""" from homeassistant.components.met_eireann.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from . import init_integration @@ -10,10 +10,10 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py index 27c53786519..859c7d20d04 100644 --- a/tests/components/mikrotik/test_hub.py +++ b/tests/components/mikrotik/test_hub.py @@ -86,7 +86,7 @@ async def test_hub_setup_failed(hass): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY # error when username or password is invalid config_entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA) diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index 01cf97fa6ab..943ea53f360 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -4,11 +4,7 @@ from unittest.mock import patch from nettigo_air_monitor import ApiError from homeassistant.components.nam.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from tests.common import MockConfigEntry @@ -40,7 +36,7 @@ async def test_config_not_ready(hass): ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass): @@ -48,10 +44,10 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 3650dae8a5c..3f07e4c4b0a 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -152,6 +152,6 @@ async def test_reauth( assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result3["reason"] == "reauth_successful" - assert new_entry.state == "loaded" + assert new_entry.state == config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index 27bc02e3ea8..db5e0c2fc33 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -11,12 +11,7 @@ from unittest.mock import patch from google_nest_sdm.exceptions import AuthException, GoogleNestException from homeassistant.components.nest import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.setup import async_setup_component from .common import CONFIG, async_setup_sdm_platform, create_config_entry @@ -32,7 +27,7 @@ async def test_setup_success(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_LOADED + assert entries[0].state is ConfigEntryState.LOADED async def async_setup_sdm(hass, config=CONFIG): @@ -54,7 +49,7 @@ async def test_setup_configuration_failure(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR # This error comes from the python google-nest-sdm library, as a check added # to prevent common misconfigurations (e.g. confusing topic and subscriber) @@ -73,7 +68,7 @@ async def test_setup_susbcriber_failure(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_SETUP_RETRY + assert entries[0].state is ConfigEntryState.SETUP_RETRY async def test_setup_device_manager_failure(hass, caplog): @@ -89,7 +84,7 @@ async def test_setup_device_manager_failure(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_SETUP_RETRY + assert entries[0].state is ConfigEntryState.SETUP_RETRY async def test_subscriber_auth_failure(hass, caplog): @@ -103,7 +98,7 @@ async def test_subscriber_auth_failure(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -121,7 +116,7 @@ async def test_setup_missing_subscriber_id(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED async def test_empty_config(hass, caplog): diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index b81c6f6ad16..fba85d9d45c 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -85,7 +85,7 @@ async def test_setup_component(hass): mock_impl.assert_called_once() mock_webhook.assert_called_once() - assert config_entry.state == config_entries.ENTRY_STATE_LOADED + assert config_entry.state is config_entries.ConfigEntryState.LOADED assert hass.config_entries.async_entries(DOMAIN) assert len(hass.states.async_all()) > 0 diff --git a/tests/components/nightscout/test_init.py b/tests/components/nightscout/test_init.py index 88ca141b999..04824139e20 100644 --- a/tests/components/nightscout/test_init.py +++ b/tests/components/nightscout/test_init.py @@ -4,11 +4,7 @@ from unittest.mock import patch from aiohttp import ClientError from homeassistant.components.nightscout.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_URL from tests.common import MockConfigEntry @@ -20,12 +16,12 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -42,4 +38,4 @@ async def test_async_setup_raises_entry_not_ready(hass): side_effect=ClientError(), ): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/nzbget/test_init.py b/tests/components/nzbget/test_init.py index 2dcdab5754e..e83672769da 100644 --- a/tests/components/nzbget/test_init.py +++ b/tests/components/nzbget/test_init.py @@ -4,11 +4,7 @@ from unittest.mock import patch from pynzbgetapi import NZBGetAPIException from homeassistant.components.nzbget.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.setup import async_setup_component @@ -44,12 +40,12 @@ async def test_unload_entry(hass, nzbget_api): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -64,4 +60,4 @@ async def test_async_setup_raises_entry_not_ready(hass): ): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index c715adcc16b..01426e1faf1 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -5,12 +5,7 @@ from pyownet.protocol import ConnError, OwnetError from homeassistant.components.onewire.const import CONF_TYPE_OWSERVER, DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, - SOURCE_USER, -) +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -48,7 +43,7 @@ async def test_owserver_connect_failure(hass): await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry_owserver.state == ENTRY_STATE_SETUP_RETRY + assert config_entry_owserver.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -81,15 +76,15 @@ async def test_unload_entry(hass): config_entry_sysbus = await setup_onewire_sysbus_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert config_entry_owserver.state == ENTRY_STATE_LOADED - assert config_entry_sysbus.state == ENTRY_STATE_LOADED + assert config_entry_owserver.state is ConfigEntryState.LOADED + assert config_entry_sysbus.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry_owserver.entry_id) assert await hass.config_entries.async_unload(config_entry_sysbus.entry_id) await hass.async_block_till_done() - assert config_entry_owserver.state == ENTRY_STATE_NOT_LOADED - assert config_entry_sysbus.state == ENTRY_STATE_NOT_LOADED + assert config_entry_owserver.state is ConfigEntryState.NOT_LOADED + assert config_entry_sysbus.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index daa38bc1dc7..ba1be4afb4c 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.components.openweathermap.const import ( DEFAULT_LANGUAGE, DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, @@ -57,11 +57,11 @@ async def test_form(hass): conf_entries = hass.config_entries.async_entries(DOMAIN) entry = conf_entries[0] - assert entry.state == "loaded" + assert entry.state == ConfigEntryState.LOADED await hass.config_entries.async_unload(conf_entries[0].entry_id) await hass.async_block_till_done() - assert entry.state == "not_loaded" + assert entry.state == ConfigEntryState.NOT_LOADED assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] @@ -86,7 +86,7 @@ async def test_form_options(hass): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == "loaded" + assert config_entry.state == ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -105,7 +105,7 @@ async def test_form_options(hass): await hass.async_block_till_done() - assert config_entry.state == "loaded" + assert config_entry.state == ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -124,7 +124,7 @@ async def test_form_options(hass): await hass.async_block_till_done() - assert config_entry.state == "loaded" + assert config_entry.state == ConfigEntryState.LOADED async def test_form_invalid_api_key(hass): diff --git a/tests/components/ozw/common.py b/tests/components/ozw/common.py index 2c1c81aea6d..450066c5aed 100644 --- a/tests/components/ozw/common.py +++ b/tests/components/ozw/common.py @@ -10,7 +10,9 @@ from tests.common import MockConfigEntry async def setup_ozw(hass, entry=None, fixture=None): """Set up OZW and load a dump.""" - mqtt_entry = MockConfigEntry(domain="mqtt", state=config_entries.ENTRY_STATE_LOADED) + mqtt_entry = MockConfigEntry( + domain="mqtt", state=config_entries.ConfigEntryState.LOADED + ) mqtt_entry.add_to_hass(hass) if entry is None: diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index 1df365054d4..d09259654de 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from .common import MQTTMessage @@ -275,6 +275,6 @@ def mock_get_addon_discovery_info(): @pytest.fixture(name="mqtt") async def mock_mqtt_fixture(hass): """Mock the MQTT integration.""" - mqtt_entry = MockConfigEntry(domain="mqtt", state=ENTRY_STATE_LOADED) + mqtt_entry = MockConfigEntry(domain="mqtt", state=ConfigEntryState.LOADED) mqtt_entry.add_to_hass(hass) return mqtt_entry diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index e0232955997..9719c483800 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -65,16 +65,16 @@ async def test_unload_entry(hass, generic_data, switch_msg, caplog): title="Z-Wave", ) entry.add_to_hass(hass) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED receive_message = await setup_ozw(hass, entry=entry, fixture=generic_data) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(hass.states.async_entity_ids("switch")) == 1 await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED entities = hass.states.async_entity_ids("switch") assert len(entities) == 1 for entity in entities: @@ -98,7 +98,7 @@ async def test_unload_entry(hass, generic_data, switch_msg, caplog): await setup_ozw(hass, entry=entry, fixture=generic_data) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(hass.states.async_entity_ids("switch")) == 1 for record in caplog.records: assert record.levelname != "ERROR" @@ -113,12 +113,12 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): data={"integration_created_addon": False}, ) entry.add_to_hass(hass) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 await hass.config_entries.async_remove(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 # test successful remove with created add-on @@ -134,7 +134,7 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): assert stop_addon.call_count == 1 assert uninstall_addon.call_count == 1 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 stop_addon.reset_mock() uninstall_addon.reset_mock() @@ -148,7 +148,7 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): assert stop_addon.call_count == 1 assert uninstall_addon.call_count == 0 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to stop the OpenZWave add-on" in caplog.text stop_addon.side_effect = None @@ -164,7 +164,7 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): assert stop_addon.call_count == 1 assert uninstall_addon.call_count == 1 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the OpenZWave add-on" in caplog.text @@ -210,7 +210,7 @@ async def test_setup_entry_without_addon_info(hass, get_addon_discovery_info): assert not await hass.config_entries.async_setup(entry.entry_id) assert mock_client.return_value.start_client.call_count == 0 - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY async def test_unload_entry_with_addon( @@ -224,15 +224,15 @@ async def test_unload_entry_with_addon( ) entry.add_to_hass(hass) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert mock_client.return_value.start_client.call_count == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py index 7351b4e5544..0f30e315683 100644 --- a/tests/components/panasonic_viera/test_init.py +++ b/tests/components/panasonic_viera/test_init.py @@ -7,7 +7,7 @@ from homeassistant.components.panasonic_viera.const import ( DEFAULT_NAME, DOMAIN, ) -from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component @@ -209,7 +209,7 @@ async def test_setup_unload_entry(hass, mock_remote): await hass.async_block_till_done() await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == ENTRY_STATE_NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED state_tv = hass.states.get("media_player.panasonic_viera_tv") state_remote = hass.states.get("remote.panasonic_viera_tv") diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 05bf15b4729..716864c1cb1 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -23,10 +23,10 @@ from homeassistant.components.plex.const import ( SERVERS, ) from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, SOURCE_INTEGRATION_DISCOVERY, SOURCE_REAUTH, SOURCE_USER, + ConfigEntryState, ) from homeassistant.const import ( CONF_HOST, @@ -354,7 +354,7 @@ async def test_all_available_servers_configured( async def test_option_flow(hass, entry, mock_plex_server): """Test config options flow selection.""" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None @@ -386,7 +386,7 @@ async def test_option_flow(hass, entry, mock_plex_server): async def test_missing_option_flow(hass, entry, mock_plex_server): """Test config options flow selection when no options stored.""" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None @@ -676,7 +676,7 @@ async def test_setup_with_limited_credentials(hass, entry, setup_plex_server): assert plex_server.owner is None assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED async def test_integration_discovery(hass): @@ -707,7 +707,7 @@ async def test_trigger_reauth( """Test setup and reauthorization of a Plex token.""" await async_setup_component(hass, "persistent_notification", {}) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "plexapi.server.PlexServer.clients", side_effect=plexapi.exceptions.Unauthorized @@ -716,7 +716,7 @@ async def test_trigger_reauth( await wait_for_debouncer(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state != ENTRY_STATE_LOADED + assert entry.state is not ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -741,7 +741,7 @@ async def test_trigger_reauth( assert len(hass.config_entries.flow.async_progress()) == 0 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.data[CONF_SERVER] == mock_plex_server.friendly_name assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == PLEX_DIRECT_URL diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index dc46c2ca771..530f265f3f0 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -13,12 +13,7 @@ from homeassistant.components.plex.models import ( TRANSIENT_SECTION, UNKNOWN_SECTION, ) -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_TOKEN, CONF_URL, @@ -38,7 +33,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_set_config_entry_unique_id(hass, entry, mock_plex_server): """Test updating missing unique_id from config entry.""" assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert ( hass.config_entries.async_entries(const.DOMAIN)[0].unique_id @@ -57,7 +52,7 @@ async def test_setup_config_entry_with_error(hass, entry): await hass.async_block_till_done() assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY with patch( "homeassistant.components.plex.PlexServer.connect", @@ -68,7 +63,7 @@ async def test_setup_config_entry_with_error(hass, entry): await hass.async_block_till_done() assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_setup_with_insecure_config_entry(hass, entry, setup_plex_server): @@ -80,7 +75,7 @@ async def test_setup_with_insecure_config_entry(hass, entry, setup_plex_server): await setup_plex_server(config_entry=entry) assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED async def test_unload_config_entry(hass, entry, mock_plex_server): @@ -88,7 +83,7 @@ async def test_unload_config_entry(hass, entry, mock_plex_server): config_entries = hass.config_entries.async_entries(const.DOMAIN) assert len(config_entries) == 1 assert entry is config_entries[0] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED server_id = mock_plex_server.machine_identifier loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] @@ -97,7 +92,7 @@ async def test_unload_config_entry(hass, entry, mock_plex_server): websocket = hass.data[const.DOMAIN][const.WEBSOCKETS][server_id] await hass.config_entries.async_unload(entry.entry_id) assert websocket.close.called - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_setup_with_photo_session(hass, entry, setup_plex_server): @@ -105,7 +100,7 @@ async def test_setup_with_photo_session(hass, entry, setup_plex_server): await setup_plex_server(session_type="photo") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() media_player = hass.states.get( @@ -124,7 +119,7 @@ async def test_setup_with_live_tv_session(hass, entry, setup_plex_server): await setup_plex_server(session_type="live_tv") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() media_player = hass.states.get( @@ -144,7 +139,7 @@ async def test_setup_with_transient_session(hass, entry, setup_plex_server): await setup_plex_server(session_type="transient") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() media_player = hass.states.get( @@ -164,7 +159,7 @@ async def test_setup_with_unknown_session(hass, entry, setup_plex_server): await setup_plex_server(session_type="unknown") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() media_player = hass.states.get( @@ -226,7 +221,7 @@ async def test_setup_when_certificate_changed( assert await hass.config_entries.async_setup(old_entry.entry_id) is False await hass.async_block_till_done() - assert old_entry.state == ENTRY_STATE_SETUP_ERROR + assert old_entry.state is ConfigEntryState.SETUP_ERROR await hass.config_entries.async_unload(old_entry.entry_id) # Test with no servers found @@ -236,7 +231,7 @@ async def test_setup_when_certificate_changed( assert await hass.config_entries.async_setup(old_entry.entry_id) is False await hass.async_block_till_done() - assert old_entry.state == ENTRY_STATE_SETUP_ERROR + assert old_entry.state is ConfigEntryState.SETUP_ERROR await hass.config_entries.async_unload(old_entry.entry_id) # Test with success @@ -249,7 +244,7 @@ async def test_setup_when_certificate_changed( await hass.async_block_till_done() assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert old_entry.state == ENTRY_STATE_LOADED + assert old_entry.state is ConfigEntryState.LOADED assert old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] == new_url @@ -261,7 +256,7 @@ async def test_tokenless_server(entry, setup_plex_server): entry.data = TOKENLESS_DATA await setup_plex_server(config_entry=entry) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED async def test_bad_token_with_tokenless_server( @@ -272,7 +267,7 @@ async def test_bad_token_with_tokenless_server( await setup_plex_server() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED # Ensure updates that rely on account return nothing trigger_plex_update(mock_websocket) diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index 6df5b90878a..5d802fb42a0 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -1,6 +1,6 @@ """Tests for the Plugwise binary_sensor integration.""" -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON from tests.components.plugwise.common import async_init_integration @@ -9,7 +9,7 @@ from tests.components.plugwise.common import async_init_integration async def test_anna_climate_binary_sensor_entities(hass, mock_smile_anna): """Test creation of climate related binary_sensor entities.""" entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("binary_sensor.auxiliary_slave_boiler_state") assert str(state.state) == STATE_OFF @@ -21,7 +21,7 @@ async def test_anna_climate_binary_sensor_entities(hass, mock_smile_anna): async def test_anna_climate_binary_sensor_change(hass, mock_smile_anna): """Test change of climate related binary_sensor entities.""" entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED hass.states.async_set("binary_sensor.auxiliary_dhw_state", STATE_ON, {}) await hass.async_block_till_done() @@ -40,7 +40,7 @@ async def test_anna_climate_binary_sensor_change(hass, mock_smile_anna): async def test_adam_climate_binary_sensor_change(hass, mock_smile_adam): """Test change of climate related binary_sensor entities.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("binary_sensor.adam_plugwise_notification") assert str(state.state) == STATE_ON diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index e85140660fd..2fed3d18fd2 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -3,7 +3,7 @@ from plugwise.exceptions import PlugwiseException from homeassistant.components.climate.const import HVAC_MODE_AUTO, HVAC_MODE_HEAT -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from tests.components.plugwise.common import async_init_integration @@ -11,7 +11,7 @@ from tests.components.plugwise.common import async_init_integration async def test_adam_climate_entity_attributes(hass, mock_smile_adam): """Test creation of adam climate device environment.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("climate.zone_lisa_wk") attrs = state.attributes @@ -50,7 +50,7 @@ async def test_adam_climate_adjust_negative_testing(hass, mock_smile_adam): mock_smile_adam.set_schedule_state.side_effect = PlugwiseException mock_smile_adam.set_temperature.side_effect = PlugwiseException entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "climate", @@ -85,7 +85,7 @@ async def test_adam_climate_adjust_negative_testing(hass, mock_smile_adam): async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): """Test handling of user requests in adam climate device environment.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "climate", @@ -138,7 +138,7 @@ async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): async def test_anna_climate_entity_attributes(hass, mock_smile_anna): """Test creation of anna climate device environment.""" entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("climate.anna") attrs = state.attributes @@ -163,7 +163,7 @@ async def test_anna_climate_entity_attributes(hass, mock_smile_anna): async def test_anna_climate_entity_climate_changes(hass, mock_smile_anna): """Test handling of user requests in anna climate device environment.""" entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "climate", diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index eded1e55406..c4f7e1c6b3d 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -5,11 +5,7 @@ import asyncio from plugwise.exceptions import XMLDataMissingError from homeassistant.components.plugwise.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from tests.common import AsyncMock, MockConfigEntry from tests.components.plugwise.common import async_init_integration @@ -18,34 +14,34 @@ from tests.components.plugwise.common import async_init_integration async def test_smile_unauthorized(hass, mock_smile_unauth): """Test failing unauthorization by Smile.""" entry = await async_init_integration(hass, mock_smile_unauth) - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_smile_error(hass, mock_smile_error): """Test server error handling by Smile.""" entry = await async_init_integration(hass, mock_smile_error) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_smile_notconnect(hass, mock_smile_notconnect): """Connection failure error handling by Smile.""" mock_smile_notconnect.connect.return_value = False entry = await async_init_integration(hass, mock_smile_notconnect) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_smile_timeout(hass, mock_smile_notconnect): """Timeout error handling by Smile.""" mock_smile_notconnect.connect.side_effect = asyncio.TimeoutError entry = await async_init_integration(hass, mock_smile_notconnect) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_smile_adam_xmlerror(hass, mock_smile_adam): """Detect malformed XML by Smile in Adam environment.""" mock_smile_adam.full_update_device.side_effect = XMLDataMissingError entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass, mock_smile_adam): @@ -55,7 +51,7 @@ async def test_unload_entry(hass, mock_smile_adam): mock_smile_adam.async_reset = AsyncMock(return_value=True) await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data[DOMAIN] @@ -66,4 +62,4 @@ async def test_async_setup_entry_fail(hass): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index a2bf4ebc50e..3b5bff781e5 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -1,6 +1,6 @@ """Tests for the Plugwise Sensor integration.""" -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from tests.common import Mock from tests.components.plugwise.common import async_init_integration @@ -9,7 +9,7 @@ from tests.components.plugwise.common import async_init_integration async def test_adam_climate_sensor_entities(hass, mock_smile_adam): """Test creation of climate related sensor entities.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.adam_outdoor_temperature") assert float(state.state) == 7.81 @@ -34,7 +34,7 @@ async def test_adam_climate_sensor_entities(hass, mock_smile_adam): async def test_anna_as_smt_climate_sensor_entities(hass, mock_smile_anna): """Test creation of climate related sensor entities.""" entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.auxiliary_outdoor_temperature") assert float(state.state) == 18.0 @@ -50,7 +50,7 @@ async def test_anna_climate_sensor_entities(hass, mock_smile_anna): """Test creation of climate related sensor entities as single master thermostat.""" mock_smile_anna.single_master_thermostat.side_effect = Mock(return_value=False) entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.auxiliary_outdoor_temperature") assert float(state.state) == 18.0 @@ -59,7 +59,7 @@ async def test_anna_climate_sensor_entities(hass, mock_smile_anna): async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): """Test creation of power related sensor entities.""" entry = await async_init_integration(hass, mock_smile_p1) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.p1_net_electricity_point") assert float(state.state) == -2761.0 @@ -80,7 +80,7 @@ async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): async def test_stretch_sensor_entities(hass, mock_stretch): """Test creation of power related sensor entities.""" entry = await async_init_integration(hass, mock_stretch) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.koelkast_92c4a_electricity_consumed") assert float(state.state) == 50.5 diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py index b7237a26150..6355362fbd9 100644 --- a/tests/components/plugwise/test_switch.py +++ b/tests/components/plugwise/test_switch.py @@ -2,7 +2,7 @@ from plugwise.exceptions import PlugwiseException -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from tests.components.plugwise.common import async_init_integration @@ -10,7 +10,7 @@ from tests.components.plugwise.common import async_init_integration async def test_adam_climate_switch_entities(hass, mock_smile_adam): """Test creation of climate related switch entities.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("switch.cv_pomp") assert str(state.state) == "on" @@ -23,7 +23,7 @@ async def test_adam_climate_switch_negative_testing(hass, mock_smile_adam): """Test exceptions of climate related switch entities.""" mock_smile_adam.set_relay_state.side_effect = PlugwiseException entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "switch", @@ -47,7 +47,7 @@ async def test_adam_climate_switch_negative_testing(hass, mock_smile_adam): async def test_adam_climate_switch_changes(hass, mock_smile_adam): """Test changing of climate related switch entities.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "switch", @@ -80,7 +80,7 @@ async def test_adam_climate_switch_changes(hass, mock_smile_adam): async def test_stretch_switch_entities(hass, mock_stretch): """Test creation of climate related switch entities.""" entry = await async_init_integration(hass, mock_stretch) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("switch.koelkast_92c4a") assert str(state.state) == "on" @@ -92,7 +92,7 @@ async def test_stretch_switch_entities(hass, mock_stretch): async def test_stretch_switch_changes(hass, mock_stretch): """Test changing of power related switch entities.""" entry = await async_init_integration(hass, mock_stretch) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "switch", diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index ee4fd265fc9..12064911bb6 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -3,6 +3,7 @@ from unittest.mock import call import pytest +from homeassistant import config_entries from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import State @@ -168,4 +169,4 @@ async def test_unknown_event_code(hass, rfxtrx): assert len(conf_entries) == 1 entry = conf_entries[0] - assert entry.state == "loaded" + assert entry.state == config_entries.ConfigEntryState.LOADED diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index be9131d5f91..fc624f5cb64 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -2,11 +2,7 @@ from unittest.mock import patch from homeassistant.components.roku.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.roku import setup_integration @@ -19,7 +15,7 @@ async def test_config_entry_not_ready( """Test the Roku configuration entry not ready.""" entry = await setup_integration(hass, aioclient_mock, error=True) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( @@ -36,10 +32,10 @@ async def test_unload_config_entry( entry = await setup_integration(hass, aioclient_mock) assert hass.data[DOMAIN][entry.entry_id] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index 0340f72891a..e9ac9ec7cd8 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -14,11 +14,7 @@ from homeassistant.components.ruckus_unleashed import ( DOMAIN, MANUFACTURER, ) -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -56,7 +52,7 @@ async def test_setup_entry_connection_error(hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_router_device_setup(hass): @@ -84,12 +80,12 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -113,4 +109,4 @@ async def test_config_not_ready_during_setup(hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/smart_meter_texas/test_init.py b/tests/components/smart_meter_texas/test_init.py index 7db4113e3cf..0c49e6285ae 100644 --- a/tests/components/smart_meter_texas/test_init.py +++ b/tests/components/smart_meter_texas/test_init.py @@ -6,12 +6,7 @@ from homeassistant.components.homeassistant import ( SERVICE_UPDATE_ENTITY, ) from homeassistant.components.smart_meter_texas.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID from homeassistant.setup import async_setup_component @@ -31,14 +26,14 @@ async def test_auth_failure(hass, config_entry, aioclient_mock): """Test if user's username or password is not accepted.""" await setup_integration(hass, config_entry, aioclient_mock, auth_fail=True) - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_api_timeout(hass, config_entry, aioclient_mock): """Test that a timeout results in ConfigEntryNotReady.""" await setup_integration(hass, config_entry, aioclient_mock, auth_timeout=True) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_update_failure(hass, config_entry, aioclient_mock): @@ -64,9 +59,9 @@ async def test_unload_config_entry(hass, config_entry, aioclient_mock): config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert config_entries[0] is config_entry - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index df44edb3da3..f316a66c5d1 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -7,11 +7,7 @@ from smarttub import LoginFailed from homeassistant.components import smarttub from homeassistant.components.smarttub.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, - SOURCE_REAUTH, -) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.setup import async_setup_component @@ -30,7 +26,7 @@ async def test_setup_entry_not_ready(setup_component, hass, config_entry, smartt config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_auth_failed(setup_component, hass, config_entry, smarttub_api): @@ -40,7 +36,7 @@ async def test_setup_auth_failed(setup_component, hass, config_entry, smarttub_a config_entry.add_to_hass(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR mock_flow_init.assert_called_with( DOMAIN, context={ diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index 4e969358b2a..b7d78883706 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -97,10 +97,10 @@ async def test_full_flow( assert DOMAIN in hass.config.components entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED async def test_abort_if_authorization_timeout(hass, current_request_with_host): diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py index 0e9c253f1b8..6dfce4ee2fe 100644 --- a/tests/components/sonarr/test_init.py +++ b/tests/components/sonarr/test_init.py @@ -2,13 +2,7 @@ from unittest.mock import patch from homeassistant.components.sonarr.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, - SOURCE_REAUTH, -) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant @@ -21,7 +15,7 @@ async def test_config_entry_not_ready( ) -> None: """Test the configuration entry not ready.""" entry = await setup_integration(hass, aioclient_mock, connection_error=True) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_config_entry_reauth( @@ -31,7 +25,7 @@ async def test_config_entry_reauth( with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: entry = await setup_integration(hass, aioclient_mock, invalid_auth=True) - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR mock_flow_init.assert_called_once_with( DOMAIN, @@ -56,10 +50,10 @@ async def test_unload_config_entry( assert hass.data[DOMAIN] assert entry.entry_id in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 72bcb743a8d..30d3d2a1d63 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -38,7 +38,7 @@ async def test_successful_config_entry(hass): ) as forward_entry_setup: await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert forward_entry_setup.mock_calls[0][1] == ( entry, "sensor", @@ -58,7 +58,7 @@ async def test_setup_failed(hass): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass): @@ -75,5 +75,5 @@ async def test_unload_entry(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert speedtestdotnet.DOMAIN not in hass.data diff --git a/tests/components/srp_energy/test_init.py b/tests/components/srp_energy/test_init.py index 8e758d05114..8c8d87674fe 100644 --- a/tests/components/srp_energy/test_init.py +++ b/tests/components/srp_energy/test_init.py @@ -1,4 +1,5 @@ """Tests for Srp Energy component Init.""" +from homeassistant import config_entries from homeassistant.components import srp_energy from tests.components.srp_energy import init_integration @@ -7,7 +8,7 @@ from tests.components.srp_energy import init_integration async def test_setup_entry(hass): """Test setup entry fails if deCONZ is not available.""" config_entry = await init_integration(hass) - assert config_entry.state == "loaded" + assert config_entry.state == config_entries.ConfigEntryState.LOADED assert hass.data[srp_energy.SRP_ENERGY_DOMAIN] diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py index 1b8d1439e68..1ca7926cea2 100644 --- a/tests/components/subaru/conftest.py +++ b/tests/components/subaru/conftest.py @@ -18,7 +18,7 @@ from homeassistant.components.subaru.const import ( VEHICLE_HAS_SAFETY_SERVICE, VEHICLE_NAME, ) -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -144,5 +144,5 @@ async def ev_entry(hass): assert DOMAIN in hass.config_entries.async_domains() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert hass.config_entries.async_get_entry(entry.entry_id) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED return entry diff --git a/tests/components/subaru/test_init.py b/tests/components/subaru/test_init.py index 13b510e8c40..cd87ed40315 100644 --- a/tests/components/subaru/test_init.py +++ b/tests/components/subaru/test_init.py @@ -8,12 +8,7 @@ from homeassistant.components.homeassistant import ( SERVICE_UPDATE_ENTITY, ) from homeassistant.components.subaru.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID from homeassistant.setup import async_setup_component @@ -44,7 +39,7 @@ async def test_setup_ev(hass, ev_entry): """Test setup with an EV vehicle.""" check_entry = hass.config_entries.async_get_entry(ev_entry.entry_id) assert check_entry - assert check_entry.state == ENTRY_STATE_LOADED + assert check_entry.state is ConfigEntryState.LOADED async def test_setup_g2(hass): @@ -57,7 +52,7 @@ async def test_setup_g2(hass): ) check_entry = hass.config_entries.async_get_entry(entry.entry_id) assert check_entry - assert check_entry.state == ENTRY_STATE_LOADED + assert check_entry.state is ConfigEntryState.LOADED async def test_setup_g1(hass): @@ -67,7 +62,7 @@ async def test_setup_g1(hass): ) check_entry = hass.config_entries.async_get_entry(entry.entry_id) assert check_entry - assert check_entry.state == ENTRY_STATE_LOADED + assert check_entry.state is ConfigEntryState.LOADED async def test_unsuccessful_connect(hass): @@ -81,7 +76,7 @@ async def test_unsuccessful_connect(hass): ) check_entry = hass.config_entries.async_get_entry(entry.entry_id) assert check_entry - assert check_entry.state == ENTRY_STATE_SETUP_RETRY + assert check_entry.state is ConfigEntryState.SETUP_RETRY async def test_invalid_credentials(hass): @@ -95,7 +90,7 @@ async def test_invalid_credentials(hass): ) check_entry = hass.config_entries.async_get_entry(entry.entry_id) assert check_entry - assert check_entry.state == ENTRY_STATE_SETUP_ERROR + assert check_entry.state is ConfigEntryState.SETUP_ERROR async def test_update_skip_unsubscribed(hass): @@ -147,7 +142,7 @@ async def test_fetch_failed(hass): async def test_unload_entry(hass, ev_entry): """Test that entry is unloaded.""" - assert ev_entry.state == ENTRY_STATE_LOADED + assert ev_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(ev_entry.entry_id) await hass.async_block_till_done() - assert ev_entry.state == ENTRY_STATE_NOT_LOADED + assert ev_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/totalconnect/test_init.py b/tests/components/totalconnect/test_init.py index b8024dbe70d..ba33d996a9b 100644 --- a/tests/components/totalconnect/test_init.py +++ b/tests/components/totalconnect/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant.components.totalconnect.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_ERROR +from homeassistant.config_entries import ConfigEntryState from homeassistant.setup import async_setup_component from .common import CONFIG_DATA @@ -26,4 +26,4 @@ async def test_reauth_started(hass): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - assert mock_entry.state == ENTRY_STATE_SETUP_ERROR + assert mock_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index c828ef55fcd..2008912bae2 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -11,7 +11,7 @@ from homeassistant.components.vera import ( CONF_LIGHTS, DOMAIN, ) -from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -148,7 +148,7 @@ async def test_unload( for config_entry in entries: assert await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_async_setup_entry_error( diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index fed967f9ffc..b4b3c8f24ed 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -242,7 +242,7 @@ async def test_discovery_updates_unique_id(hass): "name": "dummy", "id": TEST_DISCOVERY_RESULT["id"], }, - state=config_entries.ENTRY_STATE_SETUP_RETRY, + state=config_entries.ConfigEntryState.SETUP_RETRY, ) entry.add_to_hass(hass) diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py index 4f6654d3436..24efdaaa8e1 100644 --- a/tests/components/wilight/test_init.py +++ b/tests/components/wilight/test_init.py @@ -5,11 +5,7 @@ import pytest import pywilight from pywilight.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.wilight import ( @@ -47,7 +43,7 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None: """Test the WiLight configuration entry not ready.""" entry = await setup_integration(hass) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry(hass: HomeAssistant, dummy_device_from_host) -> None: @@ -55,11 +51,11 @@ async def test_unload_config_entry(hass: HomeAssistant, dummy_device_from_host) entry = await setup_integration(hass) assert entry.entry_id in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() if DOMAIN in hass.data: assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index edce49cfd80..8db7e266e80 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from wled import WLEDConnectionError from homeassistant.components.wled.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.wled import init_integration @@ -17,7 +17,7 @@ async def test_config_entry_not_ready( ) -> None: """Test the WLED configuration entry not ready.""" entry = await init_integration(hass, aioclient_mock) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 3c25852810d..8a37c2b283e 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -11,7 +11,7 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -110,7 +110,7 @@ async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_discovery(hass: HomeAssistant): diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 80b37301383..c4f35941a25 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -9,12 +9,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id -from homeassistant.config_entries import ( - DISABLED_USER, - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import DISABLED_USER, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -39,12 +34,12 @@ async def test_entry_setup_unload(hass, client, integration): entry = integration assert client.connect.call_count == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) assert client.disconnect.call_count == 1 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_home_assistant_stop(hass, client, integration): @@ -62,7 +57,7 @@ async def test_initialized_timeout(hass, client, connect_timeout): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_enabled_statistics(hass, client): @@ -130,7 +125,7 @@ async def test_listen_failure(hass, client, error): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_on_node_added_ready( @@ -590,7 +585,7 @@ async def test_start_addon( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert install_addon.call_count == 0 assert set_addon_options.call_count == 1 assert set_addon_options.call_args == call( @@ -621,7 +616,7 @@ async def test_install_addon( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert install_addon.call_count == 1 assert install_addon.call_args == call(hass, "core_zwave_js") assert set_addon_options.call_count == 1 @@ -654,7 +649,7 @@ async def test_addon_info_failure( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert install_addon.call_count == 0 assert start_addon.call_count == 0 @@ -708,7 +703,7 @@ async def test_update_addon( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert create_shapshot.call_count == snapshot_calls assert update_addon.call_count == update_calls @@ -716,8 +711,8 @@ async def test_update_addon( @pytest.mark.parametrize( "stop_addon_side_effect, entry_state", [ - (None, ENTRY_STATE_NOT_LOADED), - (HassioAPIError("Boom"), ENTRY_STATE_LOADED), + (None, ConfigEntryState.NOT_LOADED), + (HassioAPIError("Boom"), ConfigEntryState.LOADED), ], ) async def test_stop_addon( @@ -749,7 +744,7 @@ async def test_stop_addon( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_set_disabled_by(entry.entry_id, DISABLED_USER) await hass.async_block_till_done() @@ -770,12 +765,12 @@ async def test_remove_entry( data={"integration_created_addon": False}, ) entry.add_to_hass(hass) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 await hass.config_entries.async_remove(entry.entry_id) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 # test successful remove with created add-on @@ -799,7 +794,7 @@ async def test_remove_entry( ) assert uninstall_addon.call_count == 1 assert uninstall_addon.call_args == call(hass, "core_zwave_js") - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 stop_addon.reset_mock() create_shapshot.reset_mock() @@ -816,7 +811,7 @@ async def test_remove_entry( assert stop_addon.call_args == call(hass, "core_zwave_js") assert create_shapshot.call_count == 0 assert uninstall_addon.call_count == 0 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to stop the Z-Wave JS add-on" in caplog.text stop_addon.side_effect = None @@ -840,7 +835,7 @@ async def test_remove_entry( partial=True, ) assert uninstall_addon.call_count == 0 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to create a snapshot of the Z-Wave JS add-on" in caplog.text create_shapshot.side_effect = None @@ -865,7 +860,7 @@ async def test_remove_entry( ) assert uninstall_addon.call_count == 1 assert uninstall_addon.call_args == call(hass, "core_zwave_js") - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the Z-Wave JS add-on" in caplog.text diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 77f4fdd0361..042bec418ac 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -86,7 +86,7 @@ async def test_call_setup_entry(hass): assert result assert len(mock_migrate_entry.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.supports_unload @@ -115,7 +115,7 @@ async def test_call_setup_entry_without_reload_support(hass): assert result assert len(mock_migrate_entry.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert not entry.supports_unload @@ -145,7 +145,7 @@ async def test_call_async_migrate_entry(hass): assert result assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.supports_unload @@ -173,7 +173,7 @@ async def test_call_async_migrate_entry_failure_false(hass): assert result assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR assert not entry.supports_unload @@ -201,7 +201,7 @@ async def test_call_async_migrate_entry_failure_exception(hass): assert result assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR assert not entry.supports_unload @@ -229,7 +229,7 @@ async def test_call_async_migrate_entry_failure_not_bool(hass): assert result assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR assert not entry.supports_unload @@ -248,7 +248,7 @@ async def test_call_async_migrate_entry_failure_not_supported(hass): result = await async_setup_component(hass, "comp", {}) assert result assert len(mock_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR assert not entry.supports_unload @@ -380,7 +380,7 @@ async def test_remove_entry_raises(hass, manager): MockConfigEntry(domain="test", entry_id="test1").add_to_manager(manager) MockConfigEntry( - domain="comp", entry_id="test2", state=config_entries.ENTRY_STATE_LOADED + domain="comp", entry_id="test2", state=config_entries.ConfigEntryState.LOADED ).add_to_manager(manager) MockConfigEntry(domain="test", entry_id="test3").add_to_manager(manager) @@ -796,7 +796,7 @@ async def test_updating_entry_data(manager): entry = MockConfigEntry( domain="test", data={"first": True}, - state=config_entries.ENTRY_STATE_SETUP_ERROR, + state=config_entries.ConfigEntryState.SETUP_ERROR, ) entry.add_to_manager(manager) @@ -812,7 +812,7 @@ async def test_updating_entry_system_options(manager): entry = MockConfigEntry( domain="test", data={"first": True}, - state=config_entries.ENTRY_STATE_SETUP_ERROR, + state=config_entries.ConfigEntryState.SETUP_ERROR, system_options={"disable_new_entities": True}, ) entry.add_to_manager(manager) @@ -861,14 +861,14 @@ async def test_setup_raise_not_ready(hass, caplog): assert p_hass is hass assert p_wait_time == 5 - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY assert entry.reason == "The internet connection is offline" mock_setup_entry.side_effect = None mock_setup_entry.return_value = True await p_setup(None) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.reason is None @@ -905,12 +905,12 @@ async def test_setup_retrying_during_unload(hass): with patch("homeassistant.helpers.event.async_call_later") as mock_call: await entry.async_setup(hass) - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY assert len(mock_call.return_value.mock_calls) == 0 await entry.async_unload(hass) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(mock_call.return_value.mock_calls) == 1 @@ -927,7 +927,7 @@ async def test_setup_retrying_during_unload_before_started(hass): await entry.async_setup(hass) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY assert ( hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 1 ) @@ -935,7 +935,7 @@ async def test_setup_retrying_during_unload_before_started(hass): await entry.async_unload(hass) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert ( hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 0 ) @@ -1057,7 +1057,9 @@ async def test_entry_options_abort(hass, manager): async def test_entry_setup_succeed(hass, manager): """Test that we can setup an entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_NOT_LOADED) + entry = MockConfigEntry( + domain="comp", state=config_entries.ConfigEntryState.NOT_LOADED + ) entry.add_to_hass(hass) mock_setup = AsyncMock(return_value=True) @@ -1072,17 +1074,17 @@ async def test_entry_setup_succeed(hass, manager): assert await manager.async_setup(entry.entry_id) assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED @pytest.mark.parametrize( "state", ( - config_entries.ENTRY_STATE_LOADED, - config_entries.ENTRY_STATE_SETUP_ERROR, - config_entries.ENTRY_STATE_MIGRATION_ERROR, - config_entries.ENTRY_STATE_SETUP_RETRY, - config_entries.ENTRY_STATE_FAILED_UNLOAD, + config_entries.ConfigEntryState.LOADED, + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.MIGRATION_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, + config_entries.ConfigEntryState.FAILED_UNLOAD, ), ) async def test_entry_setup_invalid_state(hass, manager, state): @@ -1103,12 +1105,12 @@ async def test_entry_setup_invalid_state(hass, manager, state): assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 - assert entry.state == state + assert entry.state is state async def test_entry_unload_succeed(hass, manager): """Test that we can unload an entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) async_unload_entry = AsyncMock(return_value=True) @@ -1117,15 +1119,15 @@ async def test_entry_unload_succeed(hass, manager): assert await manager.async_unload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( "state", ( - config_entries.ENTRY_STATE_NOT_LOADED, - config_entries.ENTRY_STATE_SETUP_ERROR, - config_entries.ENTRY_STATE_SETUP_RETRY, + config_entries.ConfigEntryState.NOT_LOADED, + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, ), ) async def test_entry_unload_failed_to_load(hass, manager, state): @@ -1139,14 +1141,14 @@ async def test_entry_unload_failed_to_load(hass, manager, state): assert await manager.async_unload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( "state", ( - config_entries.ENTRY_STATE_MIGRATION_ERROR, - config_entries.ENTRY_STATE_FAILED_UNLOAD, + config_entries.ConfigEntryState.MIGRATION_ERROR, + config_entries.ConfigEntryState.FAILED_UNLOAD, ), ) async def test_entry_unload_invalid_state(hass, manager, state): @@ -1162,12 +1164,12 @@ async def test_entry_unload_invalid_state(hass, manager, state): assert await manager.async_unload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 - assert entry.state == state + assert entry.state is state async def test_entry_reload_succeed(hass, manager): """Test that we can reload an entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True) @@ -1189,15 +1191,15 @@ async def test_entry_reload_succeed(hass, manager): assert len(async_unload_entry.mock_calls) == 1 assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED @pytest.mark.parametrize( "state", ( - config_entries.ENTRY_STATE_NOT_LOADED, - config_entries.ENTRY_STATE_SETUP_ERROR, - config_entries.ENTRY_STATE_SETUP_RETRY, + config_entries.ConfigEntryState.NOT_LOADED, + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, ), ) async def test_entry_reload_not_loaded(hass, manager, state): @@ -1224,14 +1226,14 @@ async def test_entry_reload_not_loaded(hass, manager, state): assert len(async_unload_entry.mock_calls) == 0 assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED @pytest.mark.parametrize( "state", ( - config_entries.ENTRY_STATE_MIGRATION_ERROR, - config_entries.ENTRY_STATE_FAILED_UNLOAD, + config_entries.ConfigEntryState.MIGRATION_ERROR, + config_entries.ConfigEntryState.FAILED_UNLOAD, ), ) async def test_entry_reload_error(hass, manager, state): @@ -1265,7 +1267,7 @@ async def test_entry_reload_error(hass, manager, state): async def test_entry_disable_succeed(hass, manager): """Test that we can disable an entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True) @@ -1290,19 +1292,19 @@ async def test_entry_disable_succeed(hass, manager): assert len(async_unload_entry.mock_calls) == 1 assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED # Enable assert await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_unload_entry.mock_calls) == 1 assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED async def test_entry_disable_without_reload_support(hass, manager): """Test that we can disable an entry without reload support.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True) @@ -1324,14 +1326,14 @@ async def test_entry_disable_without_reload_support(hass, manager): ) assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD + assert entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD # Enable with pytest.raises(config_entries.OperationNotAllowed): await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD + assert entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD async def test_entry_enable_without_reload_support(hass, manager): @@ -1356,7 +1358,7 @@ async def test_entry_enable_without_reload_support(hass, manager): assert await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED # Disable assert not await manager.async_set_disabled_by( @@ -1364,7 +1366,7 @@ async def test_entry_enable_without_reload_support(hass, manager): ) assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD + assert entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD async def test_init_custom_integration(hass): @@ -1408,7 +1410,7 @@ async def test_reload_entry_entity_registry_works(hass): registry = mock_registry(hass) config_entry = MockConfigEntry( - domain="comp", state=config_entries.ENTRY_STATE_LOADED + domain="comp", state=config_entries.ConfigEntryState.LOADED ) config_entry.supports_unload = True config_entry.add_to_hass(hass) @@ -1488,7 +1490,7 @@ async def test_unique_id_existing_entry(hass, manager): hass.config.components.add("comp") MockConfigEntry( domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, unique_id="mock-unique-id", ).add_to_hass(hass) @@ -1543,7 +1545,7 @@ async def test_entry_id_existing_entry(hass, manager): MockConfigEntry( entry_id=collide_entry_id, domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, unique_id="mock-unique-id", ).add_to_hass(hass) @@ -1580,7 +1582,7 @@ async def test_unique_id_update_existing_entry_without_reload(hass, manager): domain="comp", data={"additional": "data", "host": "0.0.0.0"}, unique_id="mock-unique-id", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, ) entry.add_to_hass(hass) @@ -1624,7 +1626,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): domain="comp", data={"additional": "data", "host": "0.0.0.0"}, unique_id="mock-unique-id", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, ) entry.add_to_hass(hass) @@ -1663,7 +1665,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): # Test we don't reload if entry not started updates["host"] = "2.2.2.2" - entry.state = config_entries.ENTRY_STATE_NOT_LOADED + entry.state = config_entries.ConfigEntryState.NOT_LOADED with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( "homeassistant.config_entries.ConfigEntries.async_reload" ) as async_reload: @@ -1842,7 +1844,7 @@ async def test_manual_add_overrides_ignored_entry(hass, manager): domain="comp", data={"additional": "data", "host": "0.0.0.0"}, unique_id="mock-unique-id", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) @@ -1885,7 +1887,7 @@ async def test_manual_add_overrides_ignored_entry_singleton(hass, manager): hass.config.components.add("comp") entry = MockConfigEntry( domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) @@ -1924,7 +1926,7 @@ async def test__async_current_entries_does_not_skip_ignore_non_user(hass, manage hass.config.components.add("comp") entry = MockConfigEntry( domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) @@ -1959,7 +1961,7 @@ async def test__async_current_entries_explict_skip_ignore(hass, manager): hass.config.components.add("comp") entry = MockConfigEntry( domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) @@ -1998,7 +2000,7 @@ async def test__async_current_entries_explict_include_ignore(hass, manager): hass.config.components.add("comp") entry = MockConfigEntry( domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) @@ -2254,7 +2256,7 @@ async def test_async_setup_init_entry(hass): entries = hass.config_entries.async_entries("comp") assert len(entries) == 1 - assert entries[0].state == config_entries.ENTRY_STATE_LOADED + assert entries[0].state is config_entries.ConfigEntryState.LOADED async def test_async_setup_update_entry(hass): @@ -2309,7 +2311,7 @@ async def test_async_setup_update_entry(hass): entries = hass.config_entries.async_entries("comp") assert len(entries) == 1 - assert entries[0].state == config_entries.ENTRY_STATE_LOADED + assert entries[0].state is config_entries.ConfigEntryState.LOADED assert entries[0].data == {"value": "updated"} @@ -2501,7 +2503,7 @@ async def test_updating_entry_with_and_without_changes(manager): title="thetitle", options={"option": True}, unique_id="abc123", - state=config_entries.ENTRY_STATE_SETUP_ERROR, + state=config_entries.ConfigEntryState.SETUP_ERROR, ) entry.add_to_manager(manager) @@ -2552,7 +2554,7 @@ async def test_updating_entry_with_and_without_changes(manager): async def test_entry_reload_calls_on_unload_listeners(hass, manager): """Test reload calls the on unload listeners.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True) @@ -2578,7 +2580,7 @@ async def test_entry_reload_calls_on_unload_listeners(hass, manager): assert len(async_unload_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_unload_callback.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert await manager.async_reload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 2 @@ -2586,7 +2588,7 @@ async def test_entry_reload_calls_on_unload_listeners(hass, manager): # Since we did not register another async_on_unload it should # have only been called once assert len(mock_unload_callback.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED async def test_setup_raise_auth_failed(hass, caplog): @@ -2603,7 +2605,7 @@ async def test_setup_raise_auth_failed(hass, caplog): await hass.async_block_till_done() assert "could not authenticate: The password is no longer valid" in caplog.text - assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR assert entry.reason == "The password is no longer valid" flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2611,7 +2613,7 @@ async def test_setup_raise_auth_failed(hass, caplog): assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH caplog.clear() - entry.state = config_entries.ENTRY_STATE_NOT_LOADED + entry.state = config_entries.ConfigEntryState.NOT_LOADED entry.reason = None await entry.async_setup(hass) @@ -2619,7 +2621,7 @@ async def test_setup_raise_auth_failed(hass, caplog): assert "could not authenticate: The password is no longer valid" in caplog.text # Verify multiple ConfigEntryAuthFailed does not generate a second flow - assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2652,21 +2654,21 @@ async def test_setup_raise_auth_failed_from_first_coordinator_update(hass, caplo await hass.async_block_till_done() assert "could not authenticate: The password is no longer valid" in caplog.text - assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["context"]["entry_id"] == entry.entry_id assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH caplog.clear() - entry.state = config_entries.ENTRY_STATE_NOT_LOADED + entry.state = config_entries.ConfigEntryState.NOT_LOADED await entry.async_setup(hass) await hass.async_block_till_done() assert "could not authenticate: The password is no longer valid" in caplog.text # Verify multiple ConfigEntryAuthFailed does not generate a second flow - assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2700,14 +2702,14 @@ async def test_setup_raise_auth_failed_from_future_coordinator_update(hass, capl assert "Authentication failed while fetching" in caplog.text assert "The password is no longer valid" in caplog.text - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["context"]["entry_id"] == entry.entry_id assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH caplog.clear() - entry.state = config_entries.ENTRY_STATE_NOT_LOADED + entry.state = config_entries.ConfigEntryState.NOT_LOADED await entry.async_setup(hass) await hass.async_block_till_done() @@ -2715,7 +2717,7 @@ async def test_setup_raise_auth_failed_from_future_coordinator_update(hass, capl assert "The password is no longer valid" in caplog.text # Verify multiple ConfigEntryAuthFailed does not generate a second flow - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2743,7 +2745,7 @@ async def test_setup_retrying_during_shutdown(hass): with patch("homeassistant.helpers.event.async_call_later") as mock_call: await entry.async_setup(hass) - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY assert len(mock_call.return_value.mock_calls) == 0 hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) From 06236483096fa86b3bc3d1b611d7df438b7f1217 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 20 May 2021 19:28:21 +0200 Subject: [PATCH 611/852] Clean up Netatmo integration (#50904) --- homeassistant/components/netatmo/__init__.py | 16 +++++++++------- homeassistant/components/netatmo/camera.py | 16 ++++++++++++---- homeassistant/components/netatmo/const.py | 13 +++++++++++++ homeassistant/components/netatmo/data_handler.py | 16 ++++++++++++---- homeassistant/components/netatmo/light.py | 4 +++- homeassistant/components/netatmo/media_source.py | 2 +- 6 files changed, 50 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index fa4e63a21f8..354ce2cf942 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -44,6 +44,10 @@ from .const import ( DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN, + PLATFORMS, + WEBHOOK_ACTIVATION, + WEBHOOK_DEACTIVATION, + WEBHOOK_PUSH_TYPE, ) from .data_handler import NetatmoDataHandler from .webhook import async_handle_webhook @@ -62,8 +66,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["camera", "climate", "light", "sensor"] - async def async_setup(hass: HomeAssistant, config: dict): """Set up the Netatmo component.""" @@ -126,7 +128,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async_dispatcher_send( hass, f"signal-{DOMAIN}-webhook-None", - {"type": "None", "data": {"push_type": "webhook_deactivation"}}, + {"type": "None", "data": {WEBHOOK_PUSH_TYPE: WEBHOOK_DEACTIVATION}}, ) webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook() @@ -150,9 +152,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry.data[CONF_WEBHOOK_ID] ) - if entry.data["auth_implementation"] == "cloud" and not webhook_url.startswith( - "https://" - ): + if entry.data[ + "auth_implementation" + ] == cloud.DOMAIN and not webhook_url.startswith("https://"): _LOGGER.warning( "Webhook not registered - " "https and port 443 is required to register the webhook" @@ -170,7 +172,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def handle_event(event): """Handle webhook events.""" - if event["data"]["push_type"] == "webhook_activation": + if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: if activation_listener is not None: activation_listener() diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 60914860d3d..7004ef0c472 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -31,6 +31,9 @@ from .const import ( SERVICE_SET_PERSON_AWAY, SERVICE_SET_PERSONS_HOME, SIGNAL_NAME, + WEBHOOK_LIGHT_MODE, + WEBHOOK_NACAMERA_CONNECTION, + WEBHOOK_PUSH_TYPE, ) from .data_handler import CAMERA_DATA_CLASS_NAME from .netatmo_entity_base import NetatmoBase @@ -158,13 +161,16 @@ class NetatmoCamera(NetatmoBase, Camera): return if data["home_id"] == self._home_id and data["camera_id"] == self._id: - if data["push_type"] in ["NACamera-off", "NACamera-disconnection"]: + if data[WEBHOOK_PUSH_TYPE] in ["NACamera-off", "NACamera-disconnection"]: self.is_streaming = False self._status = "off" - elif data["push_type"] in ["NACamera-on", "NACamera-connection"]: + elif data[WEBHOOK_PUSH_TYPE] in [ + "NACamera-on", + WEBHOOK_NACAMERA_CONNECTION, + ]: self.is_streaming = True self._status = "on" - elif data["push_type"] == "NOC-light_mode": + elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE: self._light_state = data["sub_type"] self.async_write_ha_state() @@ -176,8 +182,10 @@ class NetatmoCamera(NetatmoBase, Camera): return await self._data.async_get_live_snapshot(camera_id=self._id) except ( aiohttp.ClientPayloadError, - pyatmo.exceptions.ApiError, aiohttp.ContentTypeError, + aiohttp.ServerDisconnectedError, + aiohttp.ClientConnectorError, + pyatmo.exceptions.ApiError, ) as err: _LOGGER.debug("Could not fetch live camera image (%s)", err) return None diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index b0a312fa1f3..2f840baa4c3 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -1,9 +1,16 @@ """Constants used by the Netatmo component.""" +from homeassistant.components.camera import DOMAIN as CAMERA_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 + API = "api" DOMAIN = "netatmo" MANUFACTURER = "Netatmo" +PLATFORMS = [CAMERA_DOMAIN, CLIMATE_DOMAIN, LIGHT_DOMAIN, SENSOR_DOMAIN] + MODEL_NAPLUG = "Relay" MODEL_NATHERM1 = "Smart Thermostat" MODEL_NRV = "Smart Radiator Valves" @@ -156,3 +163,9 @@ MODE_LIGHT_ON = "on" MODE_LIGHT_OFF = "off" MODE_LIGHT_AUTO = "auto" CAMERA_LIGHT_MODES = [MODE_LIGHT_ON, MODE_LIGHT_OFF, MODE_LIGHT_AUTO] + +WEBHOOK_ACTIVATION = "webhook_activation" +WEBHOOK_DEACTIVATION = "webhook_deactivation" +WEBHOOK_NACAMERA_CONNECTION = "NACamera-connection" +WEBHOOK_PUSH_TYPE = "push_type" +WEBHOOK_LIGHT_MODE = "NOC-light_mode" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index d3c2db95afa..83215bd3af5 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -15,7 +15,15 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval -from .const import AUTH, DOMAIN, MANUFACTURER +from .const import ( + AUTH, + DOMAIN, + MANUFACTURER, + WEBHOOK_ACTIVATION, + WEBHOOK_DEACTIVATION, + WEBHOOK_NACAMERA_CONNECTION, + WEBHOOK_PUSH_TYPE, +) _LOGGER = logging.getLogger(__name__) @@ -108,15 +116,15 @@ class NetatmoDataHandler: async def handle_event(self, event): """Handle webhook events.""" - if event["data"]["push_type"] == "webhook_activation": + if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: _LOGGER.info("%s webhook successfully registered", MANUFACTURER) self._webhook = True - elif event["data"]["push_type"] == "webhook_deactivation": + elif event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_DEACTIVATION: _LOGGER.info("%s webhook unregistered", MANUFACTURER) self._webhook = False - elif event["data"]["push_type"] == "NACamera-connection": + elif event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_NACAMERA_CONNECTION: _LOGGER.debug("%s camera reconnected", MANUFACTURER) self.async_force_update(CAMERA_DATA_CLASS_NAME) diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index f51b0fd9eaf..160fb00be6b 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -12,6 +12,8 @@ from .const import ( EVENT_TYPE_LIGHT_MODE, MANUFACTURER, SIGNAL_NAME, + WEBHOOK_LIGHT_MODE, + WEBHOOK_PUSH_TYPE, ) from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler from .netatmo_entity_base import NetatmoBase @@ -105,7 +107,7 @@ class NetatmoLight(NetatmoBase, LightEntity): if ( data["home_id"] == self._home_id and data["camera_id"] == self._id - and data["push_type"] == "NOC-light_mode" + and data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE ): self._is_on = bool(data["sub_type"] == "on") diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index ea023d1ef57..99f52d95ad4 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -159,7 +159,7 @@ def async_parse_identifier( item: MediaSourceItem, ) -> tuple[str, str, int | None]: """Parse identifier.""" - if "/" not in item.identifier: + if not item.identifier or "/" not in item.identifier: return "events", "", None source, path = item.identifier.lstrip("/").split("/", 1) From eddc1ab778a31f25500f3f1035288cc6d565fd61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 May 2021 18:06:37 -0500 Subject: [PATCH 612/852] Handle threads exiting unexpected during shutdown (#50907) If a thread exits right when we are trying to force an exception to shut it down, setting the exception will fail with SystemError. At this point in the shutdown process we want to move on as this will cause the shutdown to abort --- homeassistant/util/executor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/executor.py b/homeassistant/util/executor.py index 6765fc5d8ae..c25c6b9c13f 100644 --- a/homeassistant/util/executor.py +++ b/homeassistant/util/executor.py @@ -2,6 +2,7 @@ from __future__ import annotations from concurrent.futures import ThreadPoolExecutor +import contextlib import logging import queue import sys @@ -49,7 +50,11 @@ def join_or_interrupt_threads( if log: _log_thread_running_at_shutdown(thread.name, thread.ident) - async_raise(thread.ident, SystemExit) + with contextlib.suppress(SystemError): + # SystemError at this stage is usually a race condition + # where the thread happens to die right before we force + # it to raise the exception + async_raise(thread.ident, SystemExit) return joined From 3e1f51883ed01e90c2e98f3885cbcb99d4e61b56 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 21 May 2021 01:30:37 +0200 Subject: [PATCH 613/852] Create KNX weather entity directly from config (#49640) * create climate entities directly from config * deprecate create_sensors * move create staticmethod to module level * add comment for deprecation date --- homeassistant/components/knx/factory.py | 48 +-------------------- homeassistant/components/knx/schema.py | 42 ++++++++++--------- homeassistant/components/knx/weather.py | 56 ++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 7b5b19dcdd2..99885c8387a 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -2,17 +2,13 @@ from __future__ import annotations from xknx import XKNX -from xknx.devices import ( - Device as XknxDevice, - Sensor as XknxSensor, - Weather as XknxWeather, -) +from xknx.devices import Device as XknxDevice, Sensor as XknxSensor from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.helpers.typing import ConfigType from .const import SupportedPlatforms -from .schema import SensorSchema, WeatherSchema +from .schema import SensorSchema def create_knx_device( @@ -24,9 +20,6 @@ def create_knx_device( if platform is SupportedPlatforms.SENSOR: return _create_sensor(knx_module, config) - if platform is SupportedPlatforms.WEATHER: - return _create_weather(knx_module, config) - return None @@ -40,40 +33,3 @@ def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor: always_callback=config[SensorSchema.CONF_ALWAYS_CALLBACK], value_type=config[CONF_TYPE], ) - - -def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather: - """Return a KNX weather device to be used within XKNX.""" - return XknxWeather( - knx_module, - name=config[CONF_NAME], - sync_state=config[WeatherSchema.CONF_SYNC_STATE], - create_sensors=config[WeatherSchema.CONF_KNX_CREATE_SENSORS], - group_address_temperature=config[WeatherSchema.CONF_KNX_TEMPERATURE_ADDRESS], - group_address_brightness_south=config.get( - WeatherSchema.CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS - ), - group_address_brightness_east=config.get( - WeatherSchema.CONF_KNX_BRIGHTNESS_EAST_ADDRESS - ), - group_address_brightness_west=config.get( - WeatherSchema.CONF_KNX_BRIGHTNESS_WEST_ADDRESS - ), - group_address_brightness_north=config.get( - WeatherSchema.CONF_KNX_BRIGHTNESS_NORTH_ADDRESS - ), - group_address_wind_speed=config.get(WeatherSchema.CONF_KNX_WIND_SPEED_ADDRESS), - group_address_wind_bearing=config.get( - WeatherSchema.CONF_KNX_WIND_BEARING_ADDRESS - ), - group_address_rain_alarm=config.get(WeatherSchema.CONF_KNX_RAIN_ALARM_ADDRESS), - group_address_frost_alarm=config.get( - WeatherSchema.CONF_KNX_FROST_ALARM_ADDRESS - ), - group_address_wind_alarm=config.get(WeatherSchema.CONF_KNX_WIND_ALARM_ADDRESS), - group_address_day_night=config.get(WeatherSchema.CONF_KNX_DAY_NIGHT_ADDRESS), - group_address_air_pressure=config.get( - WeatherSchema.CONF_KNX_AIR_PRESSURE_ADDRESS - ), - group_address_humidity=config.get(WeatherSchema.CONF_KNX_HUMIDITY_ADDRESS), - ) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 2dde2fd7160..4fd0aaaa3b9 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -521,27 +521,29 @@ class WeatherSchema: CONF_KNX_DAY_NIGHT_ADDRESS = "address_day_night" CONF_KNX_AIR_PRESSURE_ADDRESS = "address_air_pressure" CONF_KNX_HUMIDITY_ADDRESS = "address_humidity" - CONF_KNX_CREATE_SENSORS = "create_sensors" DEFAULT_NAME = "KNX Weather Station" - SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, - vol.Optional(CONF_KNX_CREATE_SENSORS, default=False): cv.boolean, - vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator, - vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator, - } + SCHEMA = vol.All( + # deprecated since 2021.6 + cv.deprecated("create_sensors"), + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, + vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator, + } + ), ) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 18cb217105c..b396142e387 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -1,16 +1,18 @@ """Support for KNX/IP weather station.""" from __future__ import annotations +from xknx import XKNX from xknx.devices import Weather as XknxWeather from homeassistant.components.weather import WeatherEntity -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import CONF_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN from .knx_entity import KnxEntity +from .schema import WeatherSchema async def async_setup_platform( @@ -20,20 +22,62 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up weather entities for KNX platform.""" + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + entities = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxWeather): - entities.append(KNXWeather(device)) + for entity_config in platform_config: + entities.append(KNXWeather(xknx, entity_config)) + async_add_entities(entities) +def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: + """Return a KNX weather device to be used within XKNX.""" + return XknxWeather( + xknx, + name=config[CONF_NAME], + sync_state=config[WeatherSchema.CONF_SYNC_STATE], + group_address_temperature=config[WeatherSchema.CONF_KNX_TEMPERATURE_ADDRESS], + group_address_brightness_south=config.get( + WeatherSchema.CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS + ), + group_address_brightness_east=config.get( + WeatherSchema.CONF_KNX_BRIGHTNESS_EAST_ADDRESS + ), + group_address_brightness_west=config.get( + WeatherSchema.CONF_KNX_BRIGHTNESS_WEST_ADDRESS + ), + group_address_brightness_north=config.get( + WeatherSchema.CONF_KNX_BRIGHTNESS_NORTH_ADDRESS + ), + group_address_wind_speed=config.get(WeatherSchema.CONF_KNX_WIND_SPEED_ADDRESS), + group_address_wind_bearing=config.get( + WeatherSchema.CONF_KNX_WIND_BEARING_ADDRESS + ), + group_address_rain_alarm=config.get(WeatherSchema.CONF_KNX_RAIN_ALARM_ADDRESS), + group_address_frost_alarm=config.get( + WeatherSchema.CONF_KNX_FROST_ALARM_ADDRESS + ), + group_address_wind_alarm=config.get(WeatherSchema.CONF_KNX_WIND_ALARM_ADDRESS), + group_address_day_night=config.get(WeatherSchema.CONF_KNX_DAY_NIGHT_ADDRESS), + group_address_air_pressure=config.get( + WeatherSchema.CONF_KNX_AIR_PRESSURE_ADDRESS + ), + group_address_humidity=config.get(WeatherSchema.CONF_KNX_HUMIDITY_ADDRESS), + ) + + class KNXWeather(KnxEntity, WeatherEntity): """Representation of a KNX weather device.""" - def __init__(self, device: XknxWeather) -> None: + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" self._device: XknxWeather - super().__init__(device) + super().__init__(_create_weather(xknx, config)) self._unique_id = f"{self._device._temperature.group_address_state}" @property From 25bf88415661aaff03b96cfc5a80fa3593050ff8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 21 May 2021 00:12:09 +0000 Subject: [PATCH 614/852] [ci skip] Translation update --- homeassistant/components/fritz/translations/de.json | 6 ++++++ homeassistant/components/isy994/translations/af.json | 7 +++++++ homeassistant/components/isy994/translations/ca.json | 6 ++++++ homeassistant/components/isy994/translations/de.json | 5 +++++ homeassistant/components/isy994/translations/et.json | 8 ++++++++ homeassistant/components/isy994/translations/nl.json | 8 ++++++++ homeassistant/components/isy994/translations/no.json | 8 ++++++++ homeassistant/components/isy994/translations/ru.json | 8 ++++++++ 8 files changed, 56 insertions(+) create mode 100644 homeassistant/components/isy994/translations/af.json diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index 8fd24fa43dc..6a33938ba35 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -8,6 +8,7 @@ "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindungsfehler", "connection_error": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, @@ -38,6 +39,11 @@ }, "description": "Einrichten der FRITZ! Box Tools zur Steuerung Ihrer FRITZ! Box.\n Ben\u00f6tigt: Benutzername, Passwort.", "title": "Setup FRITZ! Box Tools - obligatorisch" + }, + "user": { + "data": { + "username": "Nutzername" + } } } } diff --git a/homeassistant/components/isy994/translations/af.json b/homeassistant/components/isy994/translations/af.json new file mode 100644 index 00000000000..7f3e90a9136 --- /dev/null +++ b/homeassistant/components/isy994/translations/af.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "host_reachable": "Afrikanisch" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ca.json b/homeassistant/components/isy994/translations/ca.json index 420214d3e16..16755c688c0 100644 --- a/homeassistant/components/isy994/translations/ca.json +++ b/homeassistant/components/isy994/translations/ca.json @@ -36,5 +36,11 @@ "title": "Opcions d'ISY994" } } + }, + "system_health": { + "info": { + "device_connected": "ISY connectat", + "host_reachable": "Amfitri\u00f3 accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index 0a4758e1156..00397ca5dd8 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -36,5 +36,10 @@ "title": "ISY994 Optionen" } } + }, + "system_health": { + "info": { + "host_reachable": "Deutsch" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/et.json b/homeassistant/components/isy994/translations/et.json index d7d4a12b2cb..233b3a2bfe8 100644 --- a/homeassistant/components/isy994/translations/et.json +++ b/homeassistant/components/isy994/translations/et.json @@ -36,5 +36,13 @@ "title": "ISY994 valikud" } } + }, + "system_health": { + "info": { + "device_connected": "ISY on \u00fchendatud", + "host_reachable": "Host on saadaval", + "last_heartbeat": "Viimatise suhtluse aeg", + "websocket_status": "S\u00fcndmuse sokli olek" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json index be274ad84a9..c75cdf86473 100644 --- a/homeassistant/components/isy994/translations/nl.json +++ b/homeassistant/components/isy994/translations/nl.json @@ -36,5 +36,13 @@ "title": "ISY994-opties" } } + }, + "system_health": { + "info": { + "device_connected": "ISY verbonden", + "host_reachable": "Host bereikbaar", + "last_heartbeat": "Laatste Heartbeat tijd", + "websocket_status": "Event Socket Status" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/no.json b/homeassistant/components/isy994/translations/no.json index f15baed6657..7da3fa4c6fe 100644 --- a/homeassistant/components/isy994/translations/no.json +++ b/homeassistant/components/isy994/translations/no.json @@ -36,5 +36,13 @@ "title": "ISY994 Alternativer" } } + }, + "system_health": { + "info": { + "device_connected": "ISY tilkoblet", + "host_reachable": "Verten kan n\u00e5s", + "last_heartbeat": "Tidspunkt for siste hjerteslag", + "websocket_status": "Event Socket Status" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index fe4c161c1a9..a5dccc79ddd 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -36,5 +36,13 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ISY994" } } + }, + "system_health": { + "info": { + "device_connected": "ISY \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d", + "host_reachable": "\u0425\u043e\u0441\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d", + "last_heartbeat": "\u0412\u0440\u0435\u043c\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e Heartbeat-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f", + "websocket_status": "\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0441\u043e\u043a\u0435\u0442\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f" + } } } \ No newline at end of file From d9769900bb2d0fe7a5f36b28e099898e73a84039 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 May 2021 00:27:52 -0500 Subject: [PATCH 615/852] Bump aiodiscover to 1.4.2 (#50917) --- homeassistant/components/dhcp/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/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 58082265006..3cf03c09d3f 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.0"], + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.2"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ebfdd4bbfe5..078582387dc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiodiscover==1.4.0 +aiodiscover==1.4.2 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 diff --git a/requirements_all.txt b/requirements_all.txt index c56b47aa2f3..656a4f7da8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ aioazuredevops==1.3.5 aiobotocore==1.2.2 # homeassistant.components.dhcp -aiodiscover==1.4.0 +aiodiscover==1.4.2 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a0073112de..2688ffabed1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -87,7 +87,7 @@ aioazuredevops==1.3.5 aiobotocore==1.2.2 # homeassistant.components.dhcp -aiodiscover==1.4.0 +aiodiscover==1.4.2 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From 19aee19efd3fb5cae0e81d0b4fbce34ab8a485ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 May 2021 00:40:55 -0500 Subject: [PATCH 616/852] Resolve race condition in powerview when discovered by zeroconf and dhcp (#50908) Set the host in the context before checking to ensure that the second discovery aborts. Seen when testing on a very fast system only --- .../components/hunterdouglas_powerview/config_flow.py | 3 ++- homeassistant/components/hunterdouglas_powerview/strings.json | 1 + .../components/hunterdouglas_powerview/translations/en.json | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 3ae60d5e62e..8dbe21eb10e 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -113,6 +113,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Confirm dhcp or homekit discovery.""" # If we already have the host configured do # not open connections to it if we can avoid it. + self.context[CONF_HOST] = self.discovered_ip for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == self.discovered_ip: return self.async_abort(reason="already_in_progress") @@ -140,8 +141,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data={CONF_HOST: self.powerview_config[CONF_HOST]}, ) - self.context[CONF_HOST] = self.discovered_ip self._set_confirm_only() + self.context["title_placeholders"] = self.powerview_config return self.async_show_form( step_id="link", description_placeholders=self.powerview_config ) diff --git a/homeassistant/components/hunterdouglas_powerview/strings.json b/homeassistant/components/hunterdouglas_powerview/strings.json index a1c91cd2ed4..e78cc105db6 100644 --- a/homeassistant/components/hunterdouglas_powerview/strings.json +++ b/homeassistant/components/hunterdouglas_powerview/strings.json @@ -12,6 +12,7 @@ "description": "Do you want to setup {name} ({host})?" } }, + "flow_title": "{name} ({host})", "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/hunterdouglas_powerview/translations/en.json b/homeassistant/components/hunterdouglas_powerview/translations/en.json index b2d09aac12c..d95d7451b1a 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/en.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/en.json @@ -7,6 +7,7 @@ "cannot_connect": "Failed to connect", "unknown": "Unexpected error" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Do you want to setup {name} ({host})?", From 80d172140f1b9ce3307670a1a16504c2ac677508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Fri, 21 May 2021 08:57:17 +0200 Subject: [PATCH 617/852] Add Modbus light integration (#42120) * Add Modbus Light and add unit tests * Update to original PR. * Review comments. * Review 2. Co-authored-by: jan Iversen --- homeassistant/components/modbus/__init__.py | 28 ++ homeassistant/components/modbus/const.py | 5 +- homeassistant/components/modbus/light.py | 158 +++++++++++ tests/components/modbus/test_light.py | 289 ++++++++++++++++++++ 4 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/modbus/light.py create mode 100644 tests/components/modbus/test_light.py diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index ff70ccd6ad5..e6178ba405c 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONF_DELAY, CONF_DEVICE_CLASS, CONF_HOST, + CONF_LIGHTS, CONF_METHOD, CONF_NAME, CONF_OFFSET, @@ -239,6 +240,32 @@ SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( } ) +LIGHT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( + { + vol.Required(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( + [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_COIL] + ), + vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int, + vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int, + vol.Optional(CONF_VERIFY): vol.Maybe( + { + vol.Optional(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_INPUT_TYPE): vol.In( + [ + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_COIL, + ] + ), + vol.Optional(CONF_STATE_OFF): cv.positive_int, + vol.Optional(CONF_STATE_ON): cv.positive_int, + } + ), + } +) + SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( { vol.Required(CONF_ADDRESS): cv.positive_int, @@ -289,6 +316,7 @@ MODBUS_SCHEMA = vol.Schema( ), vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATE_SCHEMA]), vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), + vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHT_SCHEMA]), vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]), } diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index ac32897e857..31ffb68cc5c 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -3,11 +3,13 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate.const import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_COVERS, + CONF_LIGHTS, CONF_SENSORS, CONF_SWITCHES, ) @@ -99,9 +101,10 @@ DEFAULT_TEMP_UNIT = "C" MODBUS_DOMAIN = "modbus" PLATFORMS = ( + (BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS), (CLIMATE_DOMAIN, CONF_CLIMATES), (COVER_DOMAIN, CONF_COVERS), - (BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS), + (LIGHT_DOMAIN, CONF_LIGHTS), (SENSOR_DOMAIN, CONF_SENSORS), (SWITCH_DOMAIN, CONF_SWITCHES), ) diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py new file mode 100644 index 00000000000..d253ea4df7e --- /dev/null +++ b/homeassistant/components/modbus/light.py @@ -0,0 +1,158 @@ +"""Support for Modbus lights.""" +from __future__ import annotations + +import logging + +from homeassistant.components.light import LightEntity +from homeassistant.const import ( + CONF_ADDRESS, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_LIGHTS, + CONF_NAME, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType + +from .base_platform import BasePlatform +from .const import ( + CALL_TYPE_COIL, + CALL_TYPE_WRITE_COIL, + CALL_TYPE_WRITE_REGISTER, + CONF_INPUT_TYPE, + CONF_STATE_OFF, + CONF_STATE_ON, + CONF_VERIFY, + CONF_WRITE_TYPE, + MODBUS_DOMAIN, +) +from .modbus import ModbusHub + +PARALLEL_UPDATES = 1 +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None +): + """Read configuration and create Modbus lights.""" + if discovery_info is None: + return + lights = [] + for entry in discovery_info[CONF_LIGHTS]: + hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + lights.append(ModbusLight(hub, entry)) + async_add_entities(lights) + + +class ModbusLight(BasePlatform, LightEntity, RestoreEntity): + """Base class representing a Modbus light.""" + + def __init__(self, hub: ModbusHub, config: dict) -> None: + """Initialize the light.""" + config[CONF_INPUT_TYPE] = "" + super().__init__(hub, config) + self._is_on = None + if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: + self._write_type = CALL_TYPE_WRITE_COIL + else: + self._write_type = CALL_TYPE_WRITE_REGISTER + self._command_on = config[CONF_COMMAND_ON] + self._command_off = config[CONF_COMMAND_OFF] + if CONF_VERIFY in config: + if config[CONF_VERIFY] is None: + config[CONF_VERIFY] = {} + self._verify_active = True + self._verify_address = config[CONF_VERIFY].get( + CONF_ADDRESS, config[CONF_ADDRESS] + ) + self._verify_type = config[CONF_VERIFY].get( + CONF_INPUT_TYPE, config[CONF_WRITE_TYPE] + ) + self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self._command_on) + self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) + else: + self._verify_active = False + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await self.async_base_added_to_hass() + state = await self.async_get_last_state() + if state: + self._is_on = state.state == STATE_ON + + @property + def is_on(self): + """Return true if light is on.""" + return self._is_on + + async def async_turn_on(self, **kwargs): + """Set light on.""" + + result = await self._hub.async_pymodbus_call( + self._slave, self._address, self._command_on, self._write_type + ) + if result is None: + self._available = False + self.async_write_ha_state() + else: + self._available = True + if self._verify_active: + await self.async_update() + else: + self._is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Set light off.""" + result = await self._hub.async_pymodbus_call( + self._slave, self._address, self._command_off, self._write_type + ) + if result is None: + self._available = False + self.async_write_ha_state() + else: + self._available = True + if self._verify_active: + await self.async_update() + else: + self._is_on = False + self.async_write_ha_state() + + async def async_update(self, now=None): + """Update the entity state.""" + # remark "now" is a dummy parameter to avoid problems with + # async_track_time_interval + if not self._verify_active: + self._available = True + self.async_write_ha_state() + return + + result = await self._hub.async_pymodbus_call( + self._slave, self._verify_address, 1, self._verify_type + ) + if result is None: + self._available = False + self.async_write_ha_state() + return + + self._available = True + if self._verify_type == CALL_TYPE_COIL: + self._is_on = bool(result.bits[0] & 1) + else: + value = int(result.registers[0]) + if value == self._state_on: + self._is_on = True + elif value == self._state_off: + self._is_on = False + elif value is not None: + _LOGGER.error( + "Unexpected response from hub %s, slave %s register %s, got 0x%2x", + self._hub.name, + self._slave, + self._verify_address, + value, + ) + self.async_write_ha_state() diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py new file mode 100644 index 00000000000..12e72e54155 --- /dev/null +++ b/tests/components/modbus/test_light.py @@ -0,0 +1,289 @@ +"""The tests for the Modbus light component.""" +from pymodbus.exceptions import ModbusException +import pytest + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.modbus.const import ( + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CONF_INPUT_TYPE, + CONF_STATE_OFF, + CONF_STATE_ON, + CONF_VERIFY, + CONF_WRITE_TYPE, + MODBUS_DOMAIN, +) +from homeassistant.const import ( + CONF_ADDRESS, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_HOST, + CONF_LIGHTS, + CONF_NAME, + CONF_PORT, + CONF_SLAVE, + CONF_TYPE, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update + +from tests.common import mock_restore_cache + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 1234, + }, + { + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: None, + }, + ], +) +async def test_config_light(hass, do_config): + """Run test for light.""" + device_name = "test_light" + + device_config = { + CONF_NAME: device_name, + **do_config, + } + + await base_config_test( + hass, + device_config, + device_name, + LIGHT_DOMAIN, + CONF_LIGHTS, + None, + method_discovery=True, + ) + + +@pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) +@pytest.mark.parametrize( + "regs,verify,expected", + [ + ( + [0x00], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + [0x01], + {CONF_VERIFY: {}}, + STATE_ON, + ), + ( + [0xFE], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + None, + {CONF_VERIFY: {}}, + STATE_UNAVAILABLE, + ), + ( + None, + {}, + STATE_OFF, + ), + ], +) +async def test_all_light(hass, call_type, regs, verify, expected): + """Run test for given config.""" + light_name = "modbus_test_light" + state = await base_test( + hass, + { + CONF_NAME: light_name, + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_WRITE_TYPE: call_type, + **verify, + }, + light_name, + LIGHT_DOMAIN, + CONF_LIGHTS, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected + + +async def test_restore_state_light(hass): + """Run test for sensor restore state.""" + + light_name = "test_light" + entity_id = f"{LIGHT_DOMAIN}.{light_name}" + test_value = STATE_ON + config_light = {CONF_NAME: light_name, CONF_ADDRESS: 17} + mock_restore_cache( + hass, + (State(f"{entity_id}", test_value),), + ) + await base_config_test( + hass, + config_light, + light_name, + LIGHT_DOMAIN, + CONF_LIGHTS, + None, + method_discovery=True, + ) + assert hass.states.get(entity_id).state == test_value + + +async def test_light_service_turn(hass, caplog, mock_pymodbus): + """Run test for service turn_on/turn_off.""" + + entity_id1 = f"{LIGHT_DOMAIN}.light1" + entity_id2 = f"{LIGHT_DOMAIN}.light2" + config = { + MODBUS_DOMAIN: { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_LIGHTS: [ + { + CONF_NAME: "light1", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + { + CONF_NAME: "light2", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_VERIFY: {}, + }, + ], + }, + } + assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True + await hass.async_block_till_done() + assert MODBUS_DOMAIN in hass.config.components + + assert hass.states.get(entity_id1).state == STATE_OFF + await hass.services.async_call( + "light", "turn_on", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_ON + await hass.services.async_call( + "light", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_OFF + + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + assert hass.states.get(entity_id2).state == STATE_OFF + await hass.services.async_call( + "light", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_ON + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + await hass.services.async_call( + "light", "turn_off", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_OFF + + mock_pymodbus.write_register.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "light", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE + mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "light", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE + + +async def test_service_light_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "light.test" + config = { + CONF_LIGHTS: [ + { + CONF_NAME: "test", + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + } + mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + mock_pymodbus.read_coils.return_value = ReadResult([0x00]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF From c979101a02034f78c89f3f986534f6157f827710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Fri, 21 May 2021 09:56:47 +0200 Subject: [PATCH 618/852] Add Modbus fan integration (#48558) * Add Modbus fan entity * Update to PR. * Pylint. Co-authored-by: jan Iversen --- homeassistant/components/modbus/__init__.py | 28 ++ homeassistant/components/modbus/const.py | 3 + homeassistant/components/modbus/fan.py | 167 +++++++++++ tests/components/modbus/test_fan.py | 289 ++++++++++++++++++++ 4 files changed, 487 insertions(+) create mode 100644 homeassistant/components/modbus/fan.py create mode 100644 tests/components/modbus/test_fan.py diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index e6178ba405c..c6336739bf2 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -63,6 +63,7 @@ from .const import ( CONF_CURRENT_TEMP_REGISTER_TYPE, CONF_DATA_COUNT, CONF_DATA_TYPE, + CONF_FANS, CONF_INPUT_TYPE, CONF_MAX_TEMP, CONF_MIN_TEMP, @@ -266,6 +267,32 @@ LIGHT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( } ) +FAN_SCHEMA = BASE_COMPONENT_SCHEMA.extend( + { + vol.Required(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( + [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_COIL] + ), + vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int, + vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int, + vol.Optional(CONF_VERIFY): vol.Maybe( + { + vol.Optional(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_INPUT_TYPE): vol.In( + [ + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_COIL, + ] + ), + vol.Optional(CONF_STATE_OFF): cv.positive_int, + vol.Optional(CONF_STATE_ON): cv.positive_int, + } + ), + } +) + SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( { vol.Required(CONF_ADDRESS): cv.positive_int, @@ -319,6 +346,7 @@ MODBUS_SCHEMA = vol.Schema( vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHT_SCHEMA]), vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]), + vol.Optional(CONF_FANS): vol.All(cv.ensure_list, [FAN_SCHEMA]), } ) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 31ffb68cc5c..dfec0dbb50a 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -3,6 +3,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate.const import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -24,6 +25,7 @@ CONF_CURRENT_TEMP = "current_temp_register" CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" CONF_DATA_COUNT = "data_count" CONF_DATA_TYPE = "data_type" +CONF_FANS = "fans" CONF_HUB = "hub" CONF_INPUTS = "inputs" CONF_INPUT_TYPE = "input_type" @@ -105,6 +107,7 @@ PLATFORMS = ( (CLIMATE_DOMAIN, CONF_CLIMATES), (COVER_DOMAIN, CONF_COVERS), (LIGHT_DOMAIN, CONF_LIGHTS), + (FAN_DOMAIN, CONF_FANS), (SENSOR_DOMAIN, CONF_SENSORS), (SWITCH_DOMAIN, CONF_SWITCHES), ) diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py new file mode 100644 index 00000000000..0f75748472b --- /dev/null +++ b/homeassistant/components/modbus/fan.py @@ -0,0 +1,167 @@ +"""Support for Modbus fans.""" +from __future__ import annotations + +import logging + +from homeassistant.components.fan import FanEntity +from homeassistant.const import ( + CONF_ADDRESS, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_NAME, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType + +from .base_platform import BasePlatform +from .const import ( + CALL_TYPE_COIL, + CALL_TYPE_WRITE_COIL, + CALL_TYPE_WRITE_REGISTER, + CONF_FANS, + CONF_INPUT_TYPE, + CONF_STATE_OFF, + CONF_STATE_ON, + CONF_VERIFY, + CONF_WRITE_TYPE, + MODBUS_DOMAIN, +) +from .modbus import ModbusHub + +PARALLEL_UPDATES = 1 +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None +): + """Read configuration and create Modbus fans.""" + if discovery_info is None: + return + fans = [] + + for entry in discovery_info[CONF_FANS]: + hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + fans.append(ModbusFan(hub, entry)) + async_add_entities(fans) + + +class ModbusFan(BasePlatform, FanEntity, RestoreEntity): + """Base class representing a Modbus fan.""" + + def __init__(self, hub: ModbusHub, config: dict) -> None: + """Initialize the fan.""" + config[CONF_INPUT_TYPE] = "" + super().__init__(hub, config) + self._is_on: bool = False + if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: + self._write_type = CALL_TYPE_WRITE_COIL + else: + self._write_type = CALL_TYPE_WRITE_REGISTER + self._command_on = config[CONF_COMMAND_ON] + self._command_off = config[CONF_COMMAND_OFF] + if CONF_VERIFY in config: + if config[CONF_VERIFY] is None: + config[CONF_VERIFY] = {} + self._verify_active = True + self._verify_address = config[CONF_VERIFY].get( + CONF_ADDRESS, config[CONF_ADDRESS] + ) + self._verify_type = config[CONF_VERIFY].get( + CONF_INPUT_TYPE, config[CONF_WRITE_TYPE] + ) + self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self._command_on) + self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) + else: + self._verify_active = False + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await self.async_base_added_to_hass() + state = await self.async_get_last_state() + if state: + self._is_on = state.state == STATE_ON + + @property + def is_on(self): + """Return true if fan is on.""" + return self._is_on + + async def async_turn_on( + self, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs, + ) -> None: + """Set fan on.""" + + result = await self._hub.async_pymodbus_call( + self._slave, self._address, self._command_on, self._write_type + ) + if result is None: + self._available = False + self.async_write_ha_state() + return + + self._available = True + if self._verify_active: + await self.async_update() + return + + self._is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Set fan off.""" + result = await self._hub.async_pymodbus_call( + self._slave, self._address, self._command_off, self._write_type + ) + if result is None: + self._available = False + self.async_write_ha_state() + else: + self._available = True + if self._verify_active: + await self.async_update() + else: + self._is_on = False + self.async_write_ha_state() + + async def async_update(self, now=None): + """Update the entity state.""" + # remark "now" is a dummy parameter to avoid problems with + # async_track_time_interval + if not self._verify_active: + self._available = True + self.async_write_ha_state() + return + + result = await self._hub.async_pymodbus_call( + self._slave, self._verify_address, 1, self._verify_type + ) + if result is None: + self._available = False + self.async_write_ha_state() + return + + self._available = True + if self._verify_type == CALL_TYPE_COIL: + self._is_on = bool(result.bits[0] & 1) + else: + value = int(result.registers[0]) + if value == self._state_on: + self._is_on = True + elif value == self._state_off: + self._is_on = False + elif value is not None: + _LOGGER.error( + "Unexpected response from hub %s, slave %s register %s, got 0x%2x", + self._hub.name, + self._slave, + self._verify_address, + value, + ) + self.async_write_ha_state() diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py new file mode 100644 index 00000000000..2a9414d2277 --- /dev/null +++ b/tests/components/modbus/test_fan.py @@ -0,0 +1,289 @@ +"""The tests for the Modbus fan component.""" +from pymodbus.exceptions import ModbusException +import pytest + +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN +from homeassistant.components.modbus.const import ( + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CONF_FANS, + CONF_INPUT_TYPE, + CONF_STATE_OFF, + CONF_STATE_ON, + CONF_VERIFY, + CONF_WRITE_TYPE, + MODBUS_DOMAIN, +) +from homeassistant.const import ( + CONF_ADDRESS, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_SLAVE, + CONF_TYPE, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update + +from tests.common import mock_restore_cache + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 1234, + }, + { + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: None, + }, + ], +) +async def test_config_fan(hass, do_config): + """Run test for fan.""" + device_name = "test_fan" + + device_config = { + CONF_NAME: device_name, + **do_config, + } + + await base_config_test( + hass, + device_config, + device_name, + FAN_DOMAIN, + CONF_FANS, + None, + method_discovery=True, + ) + + +@pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) +@pytest.mark.parametrize( + "regs,verify,expected", + [ + ( + [0x00], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + [0x01], + {CONF_VERIFY: {}}, + STATE_ON, + ), + ( + [0xFE], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + None, + {CONF_VERIFY: {}}, + STATE_UNAVAILABLE, + ), + ( + None, + {}, + STATE_OFF, + ), + ], +) +async def test_all_fan(hass, call_type, regs, verify, expected): + """Run test for given config.""" + fan_name = "modbus_test_fan" + state = await base_test( + hass, + { + CONF_NAME: fan_name, + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_WRITE_TYPE: call_type, + **verify, + }, + fan_name, + FAN_DOMAIN, + CONF_FANS, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected + + +async def test_restore_state_fan(hass): + """Run test for fan restore state.""" + + fan_name = "test_fan" + entity_id = f"{FAN_DOMAIN}.{fan_name}" + test_value = STATE_ON + config_fan = {CONF_NAME: fan_name, CONF_ADDRESS: 17} + mock_restore_cache( + hass, + (State(f"{entity_id}", test_value),), + ) + await base_config_test( + hass, + config_fan, + fan_name, + FAN_DOMAIN, + CONF_FANS, + None, + method_discovery=True, + ) + assert hass.states.get(entity_id).state == test_value + + +async def test_fan_service_turn(hass, caplog, mock_pymodbus): + """Run test for service turn_on/turn_off.""" + + entity_id1 = f"{FAN_DOMAIN}.fan1" + entity_id2 = f"{FAN_DOMAIN}.fan2" + config = { + MODBUS_DOMAIN: { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_FANS: [ + { + CONF_NAME: "fan1", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + { + CONF_NAME: "fan2", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_VERIFY: {}, + }, + ], + }, + } + assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True + await hass.async_block_till_done() + assert MODBUS_DOMAIN in hass.config.components + + assert hass.states.get(entity_id1).state == STATE_OFF + await hass.services.async_call( + "fan", "turn_on", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_ON + await hass.services.async_call( + "fan", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_OFF + + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + assert hass.states.get(entity_id2).state == STATE_OFF + await hass.services.async_call( + "fan", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_ON + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + await hass.services.async_call( + "fan", "turn_off", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_OFF + + mock_pymodbus.write_register.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "fan", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE + mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "fan", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE + + +async def test_service_fan_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "fan.test" + config = { + CONF_FANS: [ + { + CONF_NAME: "test", + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + } + mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + mock_pymodbus.read_coils.return_value = ReadResult([0x00]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF From 6f26687aa7f5c77773e65d83ea45f5da0b48fc87 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 21 May 2021 10:48:11 +0200 Subject: [PATCH 619/852] Compile statistics for battery, humidity and pressure sensors (#50920) --- homeassistant/components/sensor/recorder.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index e3da50d9738..a75aa6298bc 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -8,7 +8,10 @@ from statistics import fmean from homeassistant.components.recorder import history, statistics from homeassistant.components.sensor import ( ATTR_STATE_CLASS, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, ) @@ -19,8 +22,11 @@ import homeassistant.util.dt as dt_util from . import DOMAIN DEVICE_CLASS_STATISTICS = { - DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, + DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, DEVICE_CLASS_ENERGY: {"sum"}, + DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, + DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, + DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, } From 73d7a754e846eff872ccb9b82284d317de2c71bd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 21 May 2021 11:44:34 +0200 Subject: [PATCH 620/852] Mark temperature sensors as STATE_CLASS_MEASUREMENT (#50889) * Mark temperature sensors as STATE_CLASS_MEASUREMENT * Fix broadlink tests * Tweak Hue changes --- homeassistant/components/broadlink/sensor.py | 21 ++++++++++++++----- homeassistant/components/deconz/sensor.py | 20 +++++++++++++++++- homeassistant/components/hue/sensor.py | 7 ++++++- homeassistant/components/shelly/entity.py | 1 + homeassistant/components/shelly/sensor.py | 6 ++++++ homeassistant/components/tasmota/sensor.py | 16 ++++++++++++-- .../components/xiaomi_miio/sensor.py | 17 +++++++++++++-- homeassistant/components/zha/sensor.py | 8 +++++++ homeassistant/components/zwave_js/sensor.py | 20 ++++++++++++++++++ tests/components/broadlink/test_sensors.py | 16 +++++++------- 10 files changed, 113 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 3f4a1e861b3..aa0aa6c5f0b 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.const import CONF_HOST, PERCENTAGE, TEMP_CELSIUS @@ -20,11 +21,16 @@ from .helpers import import_device _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "temperature": ("Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE), - "air_quality": ("Air Quality", None, None), - "humidity": ("Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY), - "light": ("Light", None, DEVICE_CLASS_ILLUMINANCE), - "noise": ("Noise", None, None), + "temperature": ( + "Temperature", + TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + ), + "air_quality": ("Air Quality", None, None, None), + "humidity": ("Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY, None), + "light": ("Light", None, DEVICE_CLASS_ILLUMINANCE, None), + "noise": ("Noise", None, None, None), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -101,6 +107,11 @@ class BroadlinkSensor(SensorEntity): """Return device class.""" return SENSOR_TYPES[self._monitored_condition][2] + @property + def state_class(self): + """Return state class.""" + return SENSOR_TYPES[self._monitored_condition][3] + @property def device_info(self): """Return device info.""" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index ba3be37da42..96963302139 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -14,7 +14,11 @@ from pydeconz.sensor import ( Thermostat, ) -from homeassistant.components.sensor import DOMAIN, SensorEntity +from homeassistant.components.sensor import ( + DOMAIN, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, @@ -60,6 +64,10 @@ ICON = { Temperature: "mdi:thermometer", } +STATE_CLASS = { + Temperature: STATE_CLASS_MEASUREMENT, +} + UNIT_OF_MEASUREMENT = { Consumption: ENERGY_KILO_WATT_HOUR, Humidity: PERCENTAGE, @@ -161,6 +169,11 @@ class DeconzSensor(DeconzDevice, SensorEntity): """Return the icon to use in the frontend.""" return ICON.get(type(self._device)) + @property + def state_class(self): + """Return the state class of the sensor.""" + return STATE_CLASS.get(type(self._device)) + @property def unit_of_measurement(self): """Return the unit of measurement of this sensor.""" @@ -233,6 +246,11 @@ class DeconzTemperature(DeconzDevice, SensorEntity): """Return the class of the sensor.""" return DEVICE_CLASS_TEMPERATURE + @property + def state_class(self): + """Return the state class of the sensor.""" + return STATE_CLASS_MEASUREMENT + @property def unit_of_measurement(self): """Return the unit of measurement of this sensor.""" diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 988dfc31b35..75d31fb61ce 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -6,7 +6,7 @@ from aiohue.sensors import ( TYPE_ZLL_TEMPERATURE, ) -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, @@ -87,6 +87,11 @@ class HueTemperature(GenericHueGaugeSensorEntity): return self.sensor.temperature / 100 + @property + def state_class(self): + """Return the state class of the sensor.""" + return STATE_CLASS_MEASUREMENT + class HueBattery(GenericHueSensor, SensorEntity): """Battery class for when a batt-powered device is only represented as an event.""" diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 9445c792c7b..bcaa6385100 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -151,6 +151,7 @@ class BlockAttributeDescription: unit: None | str | Callable[[dict], str] = None value: Callable[[Any], Any] = lambda val: val device_class: str | None = None + state_class: str | None = None default_enabled: bool = True available: Callable[[aioshelly.Block], bool] | None = None # Callable (settings, block), return true if entity should be removed diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 9337011ba16..006ee166423 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -135,6 +135,7 @@ SENSORS = { unit=temperature_unit, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_TEMPERATURE, + state_class=sensor.STATE_CLASS_MEASUREMENT, available=lambda block: block.extTemp != 999, ), ("sensor", "humidity"): BlockAttributeDescription( @@ -231,6 +232,11 @@ class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity): """Return value of sensor.""" return self.attribute_value + @property + def state_class(self): + """State class of sensor.""" + return self.description.state_class + @property def unit_of_measurement(self): """Return unit of sensor.""" diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 02a04467194..5faea128594 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from hatasmota import const as hc, status_sensor from homeassistant.components import sensor -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, @@ -46,6 +46,7 @@ from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate DEVICE_CLASS = "device_class" +STATE_CLASS = "state_class" ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, not both @@ -89,7 +90,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_STATUS_SIGNAL: {DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH}, hc.SENSOR_STATUS_RSSI: {ICON: "mdi:access-point"}, hc.SENSOR_STATUS_SSID: {ICON: "mdi:access-point-network"}, - hc.SENSOR_TEMPERATURE: {DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE}, + hc.SENSOR_TEMPERATURE: { + DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_POWER}, hc.SENSOR_TOTAL: {DEVICE_CLASS: DEVICE_CLASS_POWER}, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, @@ -172,6 +176,14 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): ) return class_or_icon.get(DEVICE_CLASS) + @property + def state_class(self) -> str | None: + """Return the state class of the sensor.""" + class_or_icon = SENSOR_DEVICE_CLASS_ICON_MAP.get( + self._tasmota_entity.quantity, {} + ) + return class_or_icon.get(STATE_CLASS) + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index ac9a7ab4543..ed551a6dd49 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -12,7 +12,11 @@ from miio.gateway.gateway import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -66,11 +70,15 @@ class SensorType: unit: str = None icon: str = None device_class: str = None + state_class: str = None GATEWAY_SENSOR_TYPES = { "temperature": SensorType( - unit=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE + unit=TEMP_CELSIUS, + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), "humidity": SensorType( unit=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_HUMIDITY @@ -245,6 +253,11 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): """Return the device class of this entity.""" return GATEWAY_SENSOR_TYPES[self._data_key].device_class + @property + def state_class(self): + """Return the state class of this entity.""" + return GATEWAY_SENSOR_TYPES[self._data_key].state_class + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 616e0345828..a6d7ae4ce9e 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -15,6 +15,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DOMAIN, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -101,6 +102,7 @@ class Sensor(ZhaEntity, SensorEntity): _device_class: str | None = None _divisor: int = 1 _multiplier: int = 1 + _state_class: str | None = None _unit: str | None = None def __init__( @@ -126,6 +128,11 @@ class Sensor(ZhaEntity, SensorEntity): """Return device class from component DEVICE_CLASSES.""" return self._device_class + @property + def state_class(self) -> str | None: + """Return the state class of this entity, from STATE_CLASSES, if any.""" + return self._state_class + @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" @@ -285,6 +292,7 @@ class Temperature(Sensor): SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_TEMPERATURE _divisor = 100 + _state_class = STATE_CLASS_MEASUREMENT _unit = TEMP_CELSIUS diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index b23b11f3424..dcc21b236e8 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -87,6 +88,7 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): super().__init__(config_entry, client, info) self._name = self.generate_name(include_value_name=True) self._device_class = self._get_device_class() + self._state_class = self._get_state_class() def _get_device_class(self) -> str | None: """ @@ -113,11 +115,29 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): return DEVICE_CLASS_ILLUMINANCE return None + def _get_state_class(self) -> str | None: + """ + Get the state class of the sensor. + + This should be run once during initialization so we don't have to calculate + this value on every state update. + """ + if isinstance(self.info.primary_value.property_, str): + property_lower = self.info.primary_value.property_.lower() + if "temperature" in property_lower: + return STATE_CLASS_MEASUREMENT + return None + @property def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._device_class + @property + def state_class(self) -> str | None: + """Return the state class of the sensor.""" + return self._state_class + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index e5d31705a4f..5cc75c28a73 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -27,7 +27,7 @@ async def test_a1_sensor_setup(hass): assert mock_api.check_sensors_raw.call_count == 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 5 sensors_and_states = { @@ -62,7 +62,7 @@ async def test_a1_sensor_update(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 5 mock_api.check_sensors_raw.return_value = { @@ -104,7 +104,7 @@ async def test_rm_pro_sensor_setup(hass): assert mock_api.check_sensors.call_count == 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 sensors_and_states = { @@ -127,7 +127,7 @@ async def test_rm_pro_sensor_update(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 mock_api.check_sensors.return_value = {"temperature": 25.8} @@ -159,7 +159,7 @@ async def test_rm_pro_filter_crazy_temperature(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 mock_api.check_sensors.return_value = {"temperature": -7} @@ -189,7 +189,7 @@ async def test_rm_mini3_no_sensor(hass): assert mock_api.check_sensors.call_count <= 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 0 @@ -207,7 +207,7 @@ async def test_rm4_pro_hts2_sensor_setup(hass): assert mock_api.check_sensors.call_count == 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 2 sensors_and_states = { @@ -233,7 +233,7 @@ async def test_rm4_pro_hts2_sensor_update(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 2 mock_api.check_sensors.return_value = {"temperature": 16.8, "humidity": 34.0} From 00208ff0d869e289955c9ff636a5ea21587db8b7 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Fri, 21 May 2021 12:08:40 +0100 Subject: [PATCH 621/852] Use type safe import for device_tracker.PLATFORM_SCHEMA (#50860) --- homeassistant/components/aprs/device_tracker.py | 6 ++++-- homeassistant/components/arris_tg2492lg/device_tracker.py | 4 ++-- homeassistant/components/aruba/device_tracker.py | 4 ++-- homeassistant/components/bbox/device_tracker.py | 4 ++-- .../components/bluetooth_le_tracker/device_tracker.py | 6 ++++-- homeassistant/components/bt_home_hub_5/device_tracker.py | 4 ++-- homeassistant/components/bt_smarthub/device_tracker.py | 4 ++-- homeassistant/components/cisco_ios/device_tracker.py | 4 ++-- .../components/cisco_mobility_express/device_tracker.py | 4 ++-- homeassistant/components/cppm_tracker/device_tracker.py | 4 ++-- homeassistant/components/ddwrt/device_tracker.py | 4 ++-- homeassistant/components/device_tracker/legacy.py | 4 ++-- homeassistant/components/ee_brightbox/device_tracker.py | 4 ++-- homeassistant/components/fleetgo/device_tracker.py | 6 ++++-- homeassistant/components/fritz/device_tracker.py | 4 ++-- homeassistant/components/hitron_coda/device_tracker.py | 4 ++-- homeassistant/components/huawei_router/device_tracker.py | 4 ++-- homeassistant/components/linksys_smart/device_tracker.py | 4 ++-- homeassistant/components/luci/device_tracker.py | 4 ++-- homeassistant/components/meraki/device_tracker.py | 7 +++++-- homeassistant/components/mqtt_json/device_tracker.py | 6 ++++-- homeassistant/components/netgear/device_tracker.py | 4 ++-- homeassistant/components/quantum_gateway/device_tracker.py | 4 ++-- homeassistant/components/sky_hub/device_tracker.py | 4 ++-- homeassistant/components/snmp/device_tracker.py | 4 ++-- homeassistant/components/swisscom/device_tracker.py | 4 ++-- homeassistant/components/thomson/device_tracker.py | 4 ++-- homeassistant/components/tomato/device_tracker.py | 4 ++-- homeassistant/components/traccar/device_tracker.py | 7 +++++-- homeassistant/components/trackr/device_tracker.py | 6 ++++-- homeassistant/components/ubus/device_tracker.py | 4 ++-- homeassistant/components/unifi_direct/device_tracker.py | 4 ++-- homeassistant/components/upc_connect/device_tracker.py | 4 ++-- homeassistant/components/xiaomi/device_tracker.py | 4 ++-- 34 files changed, 84 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 1ce34c8a751..ef3686fcd00 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -8,7 +8,9 @@ from aprslib import ConnectionError as AprsConnectionError, LoginError import geopy.distance import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, +) from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, @@ -44,7 +46,7 @@ FILTER_PORT = 14580 MSG_FORMATS = ["compressed", "uncompressed", "mic-e"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_CALLSIGNS): cv.ensure_list, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index 09ddf40e063..d0f15499d20 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv DEFAULT_HOST = "192.168.178.1" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index 355bcad3aaf..49074dba3be 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -21,7 +21,7 @@ _DEVICES_REGEX = re.compile( + r"(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))\s+" ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 9dac635dd2f..68f24d03ed0 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -24,7 +24,7 @@ DEFAULT_HOST = "192.168.1.254" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string} ) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 6fb6f2109f1..499e6923bd8 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -7,7 +7,9 @@ from uuid import UUID import pygatt import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, +) from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, @@ -36,7 +38,7 @@ DATA_BLE_ADAPTER = "ADAPTER" BLE_PREFIX = "BLE_" MIN_SEEN_NEW = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TRACK_BATTERY, default=False): cv.boolean, vol.Optional( diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 32b8e2aa050..ec852f0c31b 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_IP = "192.168.1.254" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string} ) diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 107eb5598d9..ef4e89bd4bb 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_IP = "192.168.1.254" CONF_SMARTHUB_MODEL = "smarthub_model" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, vol.Optional(CONF_SMARTHUB_MODEL): vol.In([1, 2]), diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index 0c77fc6fd7e..b9eadec7d18 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend( + PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index b032ca30fc3..5854f4a72a1 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index a784fbd2f89..7bd2be96030 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_API_KEY, CONF_CLIENT_ID, CONF_HOST @@ -17,7 +17,7 @@ SCAN_INTERVAL = timedelta(seconds=120) GRANT_TYPE = "client_credentials" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_CLIENT_ID): cv.string, diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index c324d6a5b64..bc52e7712b6 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -31,7 +31,7 @@ DEFAULT_VERIFY_SSL = True CONF_WIRELESS_ONLY = "wireless_only" DEFAULT_WIRELESS_ONLY = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 3427bf9595e..bdef9b83c94 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -6,7 +6,7 @@ from collections.abc import Sequence from datetime import timedelta import hashlib from types import ModuleType -from typing import Any, Callable, final +from typing import Any, Callable, Final, final import attr import voluptuous as vol @@ -82,7 +82,7 @@ NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( None, vol.Schema({vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean}), ) -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA.extend( { vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_TRACK_NEW): cv.boolean, diff --git a/homeassistant/components/ee_brightbox/device_tracker.py b/homeassistant/components/ee_brightbox/device_tracker.py index 845d557e029..64f81023796 100644 --- a/homeassistant/components/ee_brightbox/device_tracker.py +++ b/homeassistant/components/ee_brightbox/device_tracker.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -20,7 +20,7 @@ CONF_DEFAULT_IP = "192.168.1.1" CONF_DEFAULT_USERNAME = "admin" CONF_DEFAULT_VERSION = 2 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_VERSION, default=CONF_DEFAULT_VERSION): cv.positive_int, vol.Required(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index 1f4c0d0ddfc..688531c11f1 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -5,7 +5,9 @@ import requests from ritassist import API import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, +) from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -18,7 +20,7 @@ from homeassistant.helpers.event import track_utc_time_change _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index c32afcdfc26..743918fa33c 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER, ) from homeassistant.components.device_tracker.config_entry import ScannerEntity @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_HOST), cv.deprecated(CONF_USERNAME), cv.deprecated(CONF_PASSWORD), - PLATFORM_SCHEMA.extend( + PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=YAML_DEFAULT_HOST): cv.string, vol.Optional(CONF_USERNAME, default=YAML_DEFAULT_USERNAME): cv.string, diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index cbd6b7eeff8..666f6796d4c 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_TYPE = "rogers" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py index 69278ed6574..d4882f0a499 100644 --- a/homeassistant/components/huawei_router/device_tracker.py +++ b/homeassistant/components/huawei_router/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index 1f31ddc03a1..3761c047997 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, HTTP_OK @@ -16,7 +16,7 @@ DEFAULT_TIMEOUT = 10 _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) def get_scanner(hass, config): diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index d4fb1d5f7bc..7f70e793e60 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 13644c1d341..159083ecd23 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -4,7 +4,10 @@ import logging import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SOURCE_TYPE_ROUTER, +) from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_BAD_REQUEST, HTTP_UNPROCESSABLE_ENTITY from homeassistant.core import callback @@ -19,7 +22,7 @@ VERSION = "2.0" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_VALIDATOR): cv.string, vol.Required(CONF_SECRET): cv.string} ) diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index 8f64636b817..2d14001b61b 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -5,7 +5,9 @@ import logging import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, +) from homeassistant.components.mqtt import CONF_QOS from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -29,7 +31,7 @@ GPS_JSON_PAYLOAD_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}} ) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index f30277086de..504faef70eb 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) CONF_APS = "accesspoints" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=""): cv.string, vol.Optional(CONF_SSL, default=False): cv.boolean, diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index 08f8a5191c9..228f6a5eab0 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SSL @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_HOST = "myfiosgateway.com" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_SSL, default=True): cv.boolean, diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index 75241d78ed3..fda0b5e3774 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) async def async_get_scanner(hass, config): diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index eafae9537e5..30ec5cd41a3 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -24,7 +24,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BASEOID): cv.string, vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 1332aa46189..b50f0b31083 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_IP = "192.168.1.1" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string} ) diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index 05e7a49b625..4922117b64c 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -25,7 +25,7 @@ _DEVICES_REGEX = re.compile( r"(?P([^\s]+))" ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index ce660a60280..4be3ae2470d 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -27,7 +27,7 @@ CONF_HTTP_ID = "http_id" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT): cv.port, diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index aebbb8b3b6c..b4d1f919238 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -6,7 +6,10 @@ from pytraccar.api import API from stringcase import camelcase import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SOURCE_TYPE_GPS, +) from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.const import ( CONF_EVENT, @@ -70,7 +73,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/trackr/device_tracker.py b/homeassistant/components/trackr/device_tracker.py index 07d3c60e256..c08a990ea16 100644 --- a/homeassistant/components/trackr/device_tracker.py +++ b/homeassistant/components/trackr/device_tracker.py @@ -4,7 +4,9 @@ import logging from pytrackr.api import trackrApiInterface import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_utc_time_change @@ -12,7 +14,7 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index 12c986d57bb..dc5cd8857f8 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -20,7 +20,7 @@ CONF_DHCP_SOFTWARE = "dhcp_software" DEFAULT_DHCP_SOFTWARE = "dnsmasq" DHCP_SOFTWARES = ["dnsmasq", "odhcpd", "none"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index 0594adb1a3c..55a9026d443 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -20,7 +20,7 @@ UNIFI_COMMAND = 'mca-dump | tr -d "\n"' UNIFI_SSID_TABLE = "vap_table" UNIFI_CLIENT_TABLE = "sta_table" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index 8a03b283236..f95f3276852 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_IP = "192.168.0.1" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string, diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index 4fe485b193b..27c9aae89c9 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, HTTP_OK @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME, default="admin"): cv.string, From 0c40f3733608691e1fe83a1604c0b239e49b402a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 21 May 2021 13:23:20 +0200 Subject: [PATCH 622/852] Set device_class and state_class for utility_meter (#50921) * Set device_class and state_class for utility_meter * Update test * Tweak tests according to review comments --- .../components/utility_meter/sensor.py | 24 ++++++- tests/components/utility_meter/test_sensor.py | 72 +++++++++++++++++-- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 1d244c970ff..94a9e0d9175 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -5,10 +5,17 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import ATTR_LAST_RESET, SensorEntity +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -53,6 +60,11 @@ ATTR_PERIOD = "meter_period" ATTR_LAST_PERIOD = "last_period" ATTR_TARIFF = "tariff" +DEVICE_CLASS_MAP = { + ENERGY_WATT_HOUR: DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR: DEVICE_CLASS_ENERGY, +} + ICON = "mdi:counter" PRECISION = 3 @@ -313,6 +325,16 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): """Return the state of the sensor.""" return self._state + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_MAP.get(self.unit_of_measurement) + + @property + def state_class(self): + """Return the device class of the sensor.""" + return STATE_CLASS_MEASUREMENT + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 54854ac9668..c5075aa322b 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -3,7 +3,7 @@ from contextlib import contextmanager from datetime import timedelta from unittest.mock import patch -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.components.utility_meter.const import ( ATTR_TARIFF, ATTR_VALUE, @@ -18,6 +18,7 @@ from homeassistant.components.utility_meter.sensor import ( PAUSED, ) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, @@ -52,7 +53,6 @@ async def test_state(hass): } assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -159,6 +159,70 @@ async def test_state(hass): assert state.state == "0.123" +async def test_device_class(hass): + """Test utility device_class.""" + config = { + "utility_meter": { + "energy_meter": { + "source": "sensor.energy", + }, + "gas_meter": { + "source": "sensor.gas", + }, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id_energy = config[DOMAIN]["energy_meter"]["source"] + hass.states.async_set( + entity_id_energy, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + ) + entity_id_gas = config[DOMAIN]["gas_meter"]["source"] + hass.states.async_set( + entity_id_gas, 2, {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"} + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_meter") + assert state is not None + assert state.state == "0" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + + state = hass.states.get("sensor.gas_meter") + assert state is not None + assert state.state == "0" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + + hass.states.async_set( + entity_id_energy, 3, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + ) + hass.states.async_set( + entity_id_gas, 3, {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"} + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_meter") + assert state is not None + assert state.state == "1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == "energy" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + + state = hass.states.get("sensor.gas_meter") + assert state is not None + assert state.state == "1" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "some_archaic_unit" + + async def test_restore_state(hass): """Test utility sensor restore state.""" last_reset = "2020-12-21T00:00:00.013073+00:00" @@ -193,7 +257,6 @@ async def test_restore_state(hass): ) assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() # restore from cache @@ -230,7 +293,6 @@ async def test_net_consumption(hass): } assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -265,7 +327,6 @@ async def test_non_net_consumption(hass): } assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -310,7 +371,6 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): now = dt_util.parse_datetime(start_time) with alter_time(now): assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) From 07e2f53b375b47b6fb3823a2b4131a8ff1b53f87 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 21 May 2021 13:47:37 +0200 Subject: [PATCH 623/852] Add zwave_js add-on info dataclass (#50776) --- homeassistant/components/zwave_js/__init__.py | 12 ++-- homeassistant/components/zwave_js/addon.py | 70 +++++++++++++------ .../components/zwave_js/config_flow.py | 34 +++------ tests/components/zwave_js/conftest.py | 8 ++- 4 files changed, 71 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 8e980e19765..bac6433a5e2 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send -from .addon import AddonError, AddonManager, get_addon_manager +from .addon import AddonError, AddonManager, AddonState, get_addon_manager from .api import async_register_api from .const import ( ATTR_COMMAND_CLASS, @@ -559,22 +559,22 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> if addon_manager.task_in_progress(): raise ConfigEntryNotReady try: - addon_is_installed = await addon_manager.async_is_addon_installed() - addon_is_running = await addon_manager.async_is_addon_running() + addon_info = await addon_manager.async_get_addon_info() except AddonError as err: - LOGGER.error("Failed to get the Z-Wave JS add-on info") + LOGGER.error(err) raise ConfigEntryNotReady from err usb_path: str = entry.data[CONF_USB_PATH] network_key: str = entry.data[CONF_NETWORK_KEY] + addon_state = addon_info.state - if not addon_is_installed: + if addon_state == AddonState.NOT_INSTALLED: addon_manager.async_schedule_install_setup_addon( usb_path, network_key, catch_error=True ) raise ConfigEntryNotReady - if not addon_is_running: + if addon_state == AddonState.NOT_RUNNING: addon_manager.async_schedule_setup_addon( usb_path, network_key, catch_error=True ) diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 1413ea06de1..ff74b5d5a44 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass +from enum import Enum from functools import partial from typing import Any, Callable, TypeVar, cast @@ -55,6 +57,26 @@ def api_error(error_message: str) -> Callable[[F], F]: return handle_hassio_api_error +@dataclass +class AddonInfo: + """Represent the current add-on info state.""" + + options: dict[str, Any] + state: AddonState + update_available: bool + version: str | None + + +class AddonState(Enum): + """Represent the current state of the add-on.""" + + NOT_INSTALLED = "not_installed" + INSTALLING = "installing" + UPDATING = "updating" + NOT_RUNNING = "not_running" + RUNNING = "running" + + class AddonManager: """Manage the add-on. @@ -93,25 +115,32 @@ class AddonManager: return discovery_info_config @api_error("Failed to get the Z-Wave JS add-on info") - async def async_get_addon_info(self) -> dict: + async def async_get_addon_info(self) -> AddonInfo: """Return and cache Z-Wave JS add-on info.""" addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG) - return addon_info + addon_state = self.async_get_addon_state(addon_info) + return AddonInfo( + options=addon_info["options"], + state=addon_state, + update_available=addon_info["update_available"], + version=addon_info["version"], + ) - async def async_is_addon_running(self) -> bool: - """Return True if Z-Wave JS add-on is running.""" - addon_info = await self.async_get_addon_info() - return bool(addon_info["state"] == "started") + @callback + def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState: + """Return the current state of the Z-Wave JS add-on.""" + addon_state = AddonState.NOT_INSTALLED - async def async_is_addon_installed(self) -> bool: - """Return True if Z-Wave JS add-on is installed.""" - addon_info = await self.async_get_addon_info() - return addon_info["version"] is not None + if addon_info["version"] is not None: + addon_state = AddonState.NOT_RUNNING + if addon_info["state"] == "started": + addon_state = AddonState.RUNNING + if self._install_task and not self._install_task.done(): + addon_state = AddonState.INSTALLING + if self._update_task and not self._update_task.done(): + addon_state = AddonState.UPDATING - async def async_get_addon_options(self) -> dict: - """Get Z-Wave JS add-on options.""" - addon_info = await self.async_get_addon_info() - return cast(dict, addon_info["options"]) + return addon_state @api_error("Failed to set the Z-Wave JS add-on options") async def async_set_addon_options(self, config: dict) -> None: @@ -164,13 +193,11 @@ class AddonManager: async def async_update_addon(self) -> None: """Update the Z-Wave JS add-on if needed.""" addon_info = await self.async_get_addon_info() - addon_version = addon_info["version"] - update_available = addon_info["update_available"] - if addon_version is None: + if addon_info.version is None: raise AddonError("Z-Wave JS add-on is not installed") - if not update_available: + if not addon_info.update_available: return await self.async_create_snapshot() @@ -215,14 +242,14 @@ class AddonManager: async def async_configure_addon(self, usb_path: str, network_key: str) -> None: """Configure and start Z-Wave JS add-on.""" - addon_options = await self.async_get_addon_options() + addon_info = await self.async_get_addon_info() new_addon_options = { CONF_ADDON_DEVICE: usb_path, CONF_ADDON_NETWORK_KEY: network_key, } - if new_addon_options != addon_options: + if new_addon_options != addon_info.options: await self.async_set_addon_options(new_addon_options) @callback @@ -246,8 +273,7 @@ class AddonManager: async def async_create_snapshot(self) -> None: """Create a partial snapshot of the Z-Wave JS add-on.""" addon_info = await self.async_get_addon_info() - addon_version = addon_info["version"] - name = f"addon_{ADDON_SLUG}_{addon_version}" + name = f"addon_{ADDON_SLUG}_{addon_info.version}" LOGGER.debug("Creating snapshot: %s", name) await async_create_snapshot( diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 0b54494654b..ef39a043b0e 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import logging -from typing import Any, cast +from typing import Any import aiohttp from async_timeout import timeout @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .addon import AddonError, AddonManager, get_addon_manager +from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager from .const import ( CONF_ADDON_DEVICE, CONF_ADDON_NETWORK_KEY, @@ -187,13 +187,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.use_addon = True - if await self._async_is_addon_running(): - addon_config = await self._async_get_addon_config() + addon_info = await self._async_get_addon_info() + + if addon_info.state == AddonState.RUNNING: + addon_config = addon_info.options self.usb_path = addon_config[CONF_ADDON_DEVICE] self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") return await self.async_step_finish_addon_setup() - if await self._async_is_addon_installed(): + if addon_info.state == AddonState.NOT_RUNNING: return await self.async_step_configure_addon() return await self.async_step_install_addon() @@ -228,7 +230,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Ask for config for Z-Wave JS add-on.""" - addon_config = await self._async_get_addon_config() + addon_info = await self._async_get_addon_info() + addon_config = addon_info.options errors: dict[str, str] = {} @@ -345,32 +348,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self._async_create_entry_from_vars() - async def _async_get_addon_info(self) -> dict: + async def _async_get_addon_info(self) -> AddonInfo: """Return and cache Z-Wave JS add-on info.""" addon_manager: AddonManager = get_addon_manager(self.hass) try: - addon_info: dict = await addon_manager.async_get_addon_info() + addon_info: AddonInfo = await addon_manager.async_get_addon_info() except AddonError as err: _LOGGER.error(err) raise AbortFlow("addon_info_failed") from err return addon_info - async def _async_is_addon_running(self) -> bool: - """Return True if Z-Wave JS add-on is running.""" - addon_info = await self._async_get_addon_info() - return bool(addon_info["state"] == "started") - - async def _async_is_addon_installed(self) -> bool: - """Return True if Z-Wave JS add-on is installed.""" - addon_info = await self._async_get_addon_info() - return addon_info["version"] is not None - - async def _async_get_addon_config(self) -> dict: - """Get Z-Wave JS add-on config.""" - addon_info = await self._async_get_addon_info() - return cast(dict, addon_info["options"]) - async def _async_set_addon_config(self, config: dict) -> None: """Set Z-Wave JS add-on config.""" addon_manager: AddonManager = get_addon_manager(self.hass) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 5935f5da29e..ffb9f13698f 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -30,7 +30,12 @@ def mock_addon_info(addon_info_side_effect): "homeassistant.components.zwave_js.addon.async_get_addon_info", side_effect=addon_info_side_effect, ) as addon_info: - addon_info.return_value = {} + addon_info.return_value = { + "options": {}, + "state": None, + "update_available": False, + "version": None, + } yield addon_info @@ -52,7 +57,6 @@ def mock_addon_installed(addon_info): @pytest.fixture(name="addon_options") def mock_addon_options(addon_info): """Mock add-on options.""" - addon_info.return_value["options"] = {} return addon_info.return_value["options"] From b4bb7c38ceffbfff53fa2da590e3a946b9063c71 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 21 May 2021 14:20:58 +0200 Subject: [PATCH 624/852] Fix zwave_js api typing (#50923) --- homeassistant/components/zwave_js/api.py | 64 ++++++++++++------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 1c2b345faec..91fa4589074 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -142,14 +142,14 @@ def async_register_api(hass: HomeAssistant) -> None: hass, websocket_update_data_collection_preference ) websocket_api.async_register_command(hass, websocket_data_collection_status) - hass.http.register_view(DumpView) # type: ignore + hass.http.register_view(DumpView()) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( {vol.Required(TYPE): "zwave_js/network_status", vol.Required(ENTRY_ID): str} ) +@websocket_api.async_response @async_get_entry async def websocket_network_status( hass: HomeAssistant, @@ -177,7 +177,6 @@ async def websocket_network_status( ) -@websocket_api.async_response # type: ignore @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/node_status", @@ -185,6 +184,7 @@ async def websocket_network_status( vol.Required(NODE_ID): int, } ) +@websocket_api.async_response @async_get_node async def websocket_node_status( hass: HomeAssistant, @@ -206,8 +206,7 @@ async def websocket_node_status( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/add_node", @@ -215,6 +214,7 @@ async def websocket_node_status( vol.Optional("secure", default=False): bool, } ) +@websocket_api.async_response @async_get_entry async def websocket_add_node( hass: HomeAssistant, @@ -300,14 +300,14 @@ async def websocket_add_node( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/stop_inclusion", vol.Required(ENTRY_ID): str, } ) +@websocket_api.async_response @async_get_entry async def websocket_stop_inclusion( hass: HomeAssistant, @@ -325,14 +325,14 @@ async def websocket_stop_inclusion( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/stop_exclusion", vol.Required(ENTRY_ID): str, } ) +@websocket_api.async_response @async_get_entry async def websocket_stop_exclusion( hass: HomeAssistant, @@ -350,14 +350,14 @@ async def websocket_stop_exclusion( ) -@websocket_api.require_admin # type:ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/remove_node", vol.Required(ENTRY_ID): str, } ) +@websocket_api.async_response @async_get_entry async def websocket_remove_node( hass: HomeAssistant, @@ -409,8 +409,7 @@ async def websocket_remove_node( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/refresh_node_info", @@ -418,6 +417,7 @@ async def websocket_remove_node( vol.Required(NODE_ID): int, }, ) +@websocket_api.async_response @async_get_node async def websocket_refresh_node_info( hass: HomeAssistant, @@ -459,8 +459,7 @@ async def websocket_refresh_node_info( connection.send_result(msg[ID], result) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/refresh_node_values", @@ -468,6 +467,7 @@ async def websocket_refresh_node_info( vol.Required(NODE_ID): int, }, ) +@websocket_api.async_response @async_get_node async def websocket_refresh_node_values( hass: HomeAssistant, @@ -480,8 +480,7 @@ async def websocket_refresh_node_values( connection.send_result(msg[ID]) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/refresh_node_cc_values", @@ -490,6 +489,7 @@ async def websocket_refresh_node_values( vol.Required(COMMAND_CLASS_ID): int, }, ) +@websocket_api.async_response @async_get_node async def websocket_refresh_node_cc_values( hass: HomeAssistant, @@ -512,8 +512,7 @@ async def websocket_refresh_node_cc_values( connection.send_result(msg[ID]) -@websocket_api.require_admin # type:ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/set_config_parameter", @@ -524,6 +523,7 @@ async def websocket_refresh_node_cc_values( vol.Required(VALUE): int, } ) +@websocket_api.async_response @async_get_node async def websocket_set_config_parameter( hass: HomeAssistant, @@ -563,8 +563,7 @@ async def websocket_set_config_parameter( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/get_config_parameters", @@ -572,6 +571,7 @@ async def websocket_set_config_parameter( vol.Required(NODE_ID): int, } ) +@websocket_api.async_response @async_get_node async def websocket_get_config_parameters( hass: HomeAssistant, connection: ActiveConnection, msg: dict, node: Node @@ -613,14 +613,14 @@ def filename_is_present_if_logging_to_file(obj: dict) -> dict: return obj -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/subscribe_logs", vol.Required(ENTRY_ID): str, } ) +@websocket_api.async_response @async_get_entry async def websocket_subscribe_logs( hass: HomeAssistant, @@ -660,8 +660,7 @@ async def websocket_subscribe_logs( connection.send_result(msg[ID]) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/update_log_config", @@ -688,6 +687,7 @@ async def websocket_subscribe_logs( ), }, ) +@websocket_api.async_response @async_get_entry async def websocket_update_log_config( hass: HomeAssistant, @@ -703,14 +703,14 @@ async def websocket_update_log_config( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/get_log_config", vol.Required(ENTRY_ID): str, }, ) +@websocket_api.async_response @async_get_entry async def websocket_get_log_config( hass: HomeAssistant, @@ -727,8 +727,7 @@ async def websocket_get_log_config( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/update_data_collection_preference", @@ -736,6 +735,7 @@ async def websocket_get_log_config( vol.Required(OPTED_IN): bool, }, ) +@websocket_api.async_response @async_get_entry async def websocket_update_data_collection_preference( hass: HomeAssistant, @@ -758,14 +758,14 @@ async def websocket_update_data_collection_preference( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/data_collection_status", vol.Required(ENTRY_ID): str, }, ) +@websocket_api.async_response @async_get_entry async def websocket_data_collection_status( hass: HomeAssistant, From 8c5c8ed1536f427c3f5ec6928bc205cb0a9a633b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 21 May 2021 14:33:54 +0200 Subject: [PATCH 625/852] Add strict type annotations to fitbit (#50740) * add strict type annotations * cast json_load() * apply suggestions * move SCAN_INTERVAL back to platform file * apply suggestion * apply suggestion * apply suggestions * rename to PARENT_PLATFORM_SCHEMA --- .coveragerc | 2 +- .strict-typing | 1 + homeassistant/components/fitbit/const.py | 148 ++++++++++++ homeassistant/components/fitbit/sensor.py | 267 +++++++++------------- mypy.ini | 14 +- script/hassfest/mypy_config.py | 1 - 6 files changed, 264 insertions(+), 169 deletions(-) create mode 100644 homeassistant/components/fitbit/const.py diff --git a/.coveragerc b/.coveragerc index d4116e3ab46..227d932643c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -309,7 +309,7 @@ omit = homeassistant/components/firmata/pin.py homeassistant/components/firmata/sensor.py homeassistant/components/firmata/switch.py - homeassistant/components/fitbit/sensor.py + homeassistant/components/fitbit/* homeassistant/components/fixer/sensor.py homeassistant/components/fleetgo/device_tracker.py homeassistant/components/flexit/climate.py diff --git a/.strict-typing b/.strict-typing index 1fbaaa39c30..4087f970e47 100644 --- a/.strict-typing +++ b/.strict-typing @@ -21,6 +21,7 @@ homeassistant.components.camera.* homeassistant.components.cover.* homeassistant.components.device_automation.* homeassistant.components.elgato.* +homeassistant.components.fitbit.* homeassistant.components.fritzbox.* homeassistant.components.frontend.* homeassistant.components.geo_location.* diff --git a/homeassistant/components/fitbit/const.py b/homeassistant/components/fitbit/const.py new file mode 100644 index 00000000000..e5891758f60 --- /dev/null +++ b/homeassistant/components/fitbit/const.py @@ -0,0 +1,148 @@ +"""Constants for the Fitbit platform.""" +from __future__ import annotations + +from typing import Final + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + LENGTH_FEET, + MASS_KILOGRAMS, + MASS_MILLIGRAMS, + PERCENTAGE, + TIME_MILLISECONDS, + TIME_MINUTES, +) + +ATTR_ACCESS_TOKEN: Final = "access_token" +ATTR_REFRESH_TOKEN: Final = "refresh_token" +ATTR_LAST_SAVED_AT: Final = "last_saved_at" + +ATTR_DURATION: Final = "duration" +ATTR_DISTANCE: Final = "distance" +ATTR_ELEVATION: Final = "elevation" +ATTR_HEIGHT: Final = "height" +ATTR_WEIGHT: Final = "weight" +ATTR_BODY: Final = "body" +ATTR_LIQUIDS: Final = "liquids" +ATTR_BLOOD_GLUCOSE: Final = "blood glucose" +ATTR_BATTERY: Final = "battery" + +CONF_MONITORED_RESOURCES: Final = "monitored_resources" +CONF_CLOCK_FORMAT: Final = "clock_format" +ATTRIBUTION: Final = "Data provided by Fitbit.com" + +FITBIT_AUTH_CALLBACK_PATH: Final = "/api/fitbit/callback" +FITBIT_AUTH_START: Final = "/api/fitbit" +FITBIT_CONFIG_FILE: Final = "fitbit.conf" +FITBIT_DEFAULT_RESOURCES: Final[list[str]] = ["activities/steps"] + +DEFAULT_CONFIG: Final[dict[str, str]] = { + CONF_CLIENT_ID: "CLIENT_ID_HERE", + CONF_CLIENT_SECRET: "CLIENT_SECRET_HERE", +} +DEFAULT_CLOCK_FORMAT: Final = "24H" + +FITBIT_RESOURCES_LIST: Final[dict[str, tuple[str, str | None, str]]] = { + "activities/activityCalories": ("Activity Calories", "cal", "fire"), + "activities/calories": ("Calories", "cal", "fire"), + "activities/caloriesBMR": ("Calories BMR", "cal", "fire"), + "activities/distance": ("Distance", "", "map-marker"), + "activities/elevation": ("Elevation", "", "walk"), + "activities/floors": ("Floors", "floors", "walk"), + "activities/heart": ("Resting Heart Rate", "bpm", "heart-pulse"), + "activities/minutesFairlyActive": ("Minutes Fairly Active", TIME_MINUTES, "walk"), + "activities/minutesLightlyActive": ("Minutes Lightly Active", TIME_MINUTES, "walk"), + "activities/minutesSedentary": ( + "Minutes Sedentary", + TIME_MINUTES, + "seat-recline-normal", + ), + "activities/minutesVeryActive": ("Minutes Very Active", TIME_MINUTES, "run"), + "activities/steps": ("Steps", "steps", "walk"), + "activities/tracker/activityCalories": ("Tracker Activity Calories", "cal", "fire"), + "activities/tracker/calories": ("Tracker Calories", "cal", "fire"), + "activities/tracker/distance": ("Tracker Distance", "", "map-marker"), + "activities/tracker/elevation": ("Tracker Elevation", "", "walk"), + "activities/tracker/floors": ("Tracker Floors", "floors", "walk"), + "activities/tracker/minutesFairlyActive": ( + "Tracker Minutes Fairly Active", + TIME_MINUTES, + "walk", + ), + "activities/tracker/minutesLightlyActive": ( + "Tracker Minutes Lightly Active", + TIME_MINUTES, + "walk", + ), + "activities/tracker/minutesSedentary": ( + "Tracker Minutes Sedentary", + TIME_MINUTES, + "seat-recline-normal", + ), + "activities/tracker/minutesVeryActive": ( + "Tracker Minutes Very Active", + TIME_MINUTES, + "run", + ), + "activities/tracker/steps": ("Tracker Steps", "steps", "walk"), + "body/bmi": ("BMI", "BMI", "human"), + "body/fat": ("Body Fat", PERCENTAGE, "human"), + "body/weight": ("Weight", "", "human"), + "devices/battery": ("Battery", None, "battery"), + "sleep/awakeningsCount": ("Awakenings Count", "times awaken", "sleep"), + "sleep/efficiency": ("Sleep Efficiency", PERCENTAGE, "sleep"), + "sleep/minutesAfterWakeup": ("Minutes After Wakeup", TIME_MINUTES, "sleep"), + "sleep/minutesAsleep": ("Sleep Minutes Asleep", TIME_MINUTES, "sleep"), + "sleep/minutesAwake": ("Sleep Minutes Awake", TIME_MINUTES, "sleep"), + "sleep/minutesToFallAsleep": ( + "Sleep Minutes to Fall Asleep", + TIME_MINUTES, + "sleep", + ), + "sleep/startTime": ("Sleep Start Time", None, "clock"), + "sleep/timeInBed": ("Sleep Time in Bed", TIME_MINUTES, "hotel"), +} + +FITBIT_MEASUREMENTS: Final[dict[str, dict[str, str]]] = { + "en_US": { + ATTR_DURATION: TIME_MILLISECONDS, + ATTR_DISTANCE: "mi", + ATTR_ELEVATION: LENGTH_FEET, + ATTR_HEIGHT: "in", + ATTR_WEIGHT: "lbs", + ATTR_BODY: "in", + ATTR_LIQUIDS: "fl. oz.", + ATTR_BLOOD_GLUCOSE: f"{MASS_MILLIGRAMS}/dL", + ATTR_BATTERY: "", + }, + "en_GB": { + ATTR_DURATION: TIME_MILLISECONDS, + ATTR_DISTANCE: "kilometers", + ATTR_ELEVATION: "meters", + ATTR_HEIGHT: "centimeters", + ATTR_WEIGHT: "stone", + ATTR_BODY: "centimeters", + ATTR_LIQUIDS: "milliliters", + ATTR_BLOOD_GLUCOSE: "mmol/L", + ATTR_BATTERY: "", + }, + "metric": { + ATTR_DURATION: TIME_MILLISECONDS, + ATTR_DISTANCE: "kilometers", + ATTR_ELEVATION: "meters", + ATTR_HEIGHT: "centimeters", + ATTR_WEIGHT: MASS_KILOGRAMS, + ATTR_BODY: "centimeters", + ATTR_LIQUIDS: "milliliters", + ATTR_BLOOD_GLUCOSE: "mmol/L", + ATTR_BATTERY: "", + }, +} + +BATTERY_LEVELS: Final[dict[str, int]] = { + "High": 100, + "Medium": 50, + "Low": 20, + "Empty": 0, +} diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 263ae24ff34..9f99b3d0bb0 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -1,162 +1,70 @@ """Support for the Fitbit API.""" + +from __future__ import annotations + import datetime import logging import os import time +from typing import Any, Final, cast +from aiohttp.web import Request from fitbit import Fitbit from fitbit.api import FitbitOauth2Client from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_UNIT_SYSTEM, - LENGTH_FEET, - MASS_KILOGRAMS, - MASS_MILLIGRAMS, - PERCENTAGE, - TIME_MILLISECONDS, - TIME_MINUTES, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.network import get_url +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.json import load_json, save_json -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) +from .const import ( + ATTR_ACCESS_TOKEN, + ATTR_LAST_SAVED_AT, + ATTR_REFRESH_TOKEN, + ATTRIBUTION, + BATTERY_LEVELS, + CONF_CLOCK_FORMAT, + CONF_MONITORED_RESOURCES, + DEFAULT_CLOCK_FORMAT, + DEFAULT_CONFIG, + FITBIT_AUTH_CALLBACK_PATH, + FITBIT_AUTH_START, + FITBIT_CONFIG_FILE, + FITBIT_DEFAULT_RESOURCES, + FITBIT_MEASUREMENTS, + FITBIT_RESOURCES_LIST, +) -ATTR_ACCESS_TOKEN = "access_token" -ATTR_REFRESH_TOKEN = "refresh_token" -ATTR_LAST_SAVED_AT = "last_saved_at" +_LOGGER: Final = logging.getLogger(__name__) -CONF_MONITORED_RESOURCES = "monitored_resources" -CONF_CLOCK_FORMAT = "clock_format" -ATTRIBUTION = "Data provided by Fitbit.com" +_CONFIGURING: dict[str, str] = {} -FITBIT_AUTH_CALLBACK_PATH = "/api/fitbit/callback" -FITBIT_AUTH_START = "/api/fitbit" -FITBIT_CONFIG_FILE = "fitbit.conf" -FITBIT_DEFAULT_RESOURCES = ["activities/steps"] +SCAN_INTERVAL: Final = datetime.timedelta(minutes=30) -SCAN_INTERVAL = datetime.timedelta(minutes=30) - -DEFAULT_CONFIG = { - CONF_CLIENT_ID: "CLIENT_ID_HERE", - CONF_CLIENT_SECRET: "CLIENT_SECRET_HERE", -} - -FITBIT_RESOURCES_LIST = { - "activities/activityCalories": ["Activity Calories", "cal", "fire"], - "activities/calories": ["Calories", "cal", "fire"], - "activities/caloriesBMR": ["Calories BMR", "cal", "fire"], - "activities/distance": ["Distance", "", "map-marker"], - "activities/elevation": ["Elevation", "", "walk"], - "activities/floors": ["Floors", "floors", "walk"], - "activities/heart": ["Resting Heart Rate", "bpm", "heart-pulse"], - "activities/minutesFairlyActive": ["Minutes Fairly Active", TIME_MINUTES, "walk"], - "activities/minutesLightlyActive": ["Minutes Lightly Active", TIME_MINUTES, "walk"], - "activities/minutesSedentary": [ - "Minutes Sedentary", - TIME_MINUTES, - "seat-recline-normal", - ], - "activities/minutesVeryActive": ["Minutes Very Active", TIME_MINUTES, "run"], - "activities/steps": ["Steps", "steps", "walk"], - "activities/tracker/activityCalories": ["Tracker Activity Calories", "cal", "fire"], - "activities/tracker/calories": ["Tracker Calories", "cal", "fire"], - "activities/tracker/distance": ["Tracker Distance", "", "map-marker"], - "activities/tracker/elevation": ["Tracker Elevation", "", "walk"], - "activities/tracker/floors": ["Tracker Floors", "floors", "walk"], - "activities/tracker/minutesFairlyActive": [ - "Tracker Minutes Fairly Active", - TIME_MINUTES, - "walk", - ], - "activities/tracker/minutesLightlyActive": [ - "Tracker Minutes Lightly Active", - TIME_MINUTES, - "walk", - ], - "activities/tracker/minutesSedentary": [ - "Tracker Minutes Sedentary", - TIME_MINUTES, - "seat-recline-normal", - ], - "activities/tracker/minutesVeryActive": [ - "Tracker Minutes Very Active", - TIME_MINUTES, - "run", - ], - "activities/tracker/steps": ["Tracker Steps", "steps", "walk"], - "body/bmi": ["BMI", "BMI", "human"], - "body/fat": ["Body Fat", PERCENTAGE, "human"], - "body/weight": ["Weight", "", "human"], - "devices/battery": ["Battery", None, None], - "sleep/awakeningsCount": ["Awakenings Count", "times awaken", "sleep"], - "sleep/efficiency": ["Sleep Efficiency", PERCENTAGE, "sleep"], - "sleep/minutesAfterWakeup": ["Minutes After Wakeup", TIME_MINUTES, "sleep"], - "sleep/minutesAsleep": ["Sleep Minutes Asleep", TIME_MINUTES, "sleep"], - "sleep/minutesAwake": ["Sleep Minutes Awake", TIME_MINUTES, "sleep"], - "sleep/minutesToFallAsleep": [ - "Sleep Minutes to Fall Asleep", - TIME_MINUTES, - "sleep", - ], - "sleep/startTime": ["Sleep Start Time", None, "clock"], - "sleep/timeInBed": ["Sleep Time in Bed", TIME_MINUTES, "hotel"], -} - -FITBIT_MEASUREMENTS = { - "en_US": { - "duration": TIME_MILLISECONDS, - "distance": "mi", - "elevation": LENGTH_FEET, - "height": "in", - "weight": "lbs", - "body": "in", - "liquids": "fl. oz.", - "blood glucose": f"{MASS_MILLIGRAMS}/dL", - "battery": "", - }, - "en_GB": { - "duration": TIME_MILLISECONDS, - "distance": "kilometers", - "elevation": "meters", - "height": "centimeters", - "weight": "stone", - "body": "centimeters", - "liquids": "milliliters", - "blood glucose": "mmol/L", - "battery": "", - }, - "metric": { - "duration": TIME_MILLISECONDS, - "distance": "kilometers", - "elevation": "meters", - "height": "centimeters", - "weight": MASS_KILOGRAMS, - "body": "centimeters", - "liquids": "milliliters", - "blood glucose": "mmol/L", - "battery": "", - }, -} - -BATTERY_LEVELS = {"High": 100, "Medium": 50, "Low": 20, "Empty": 0} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_MONITORED_RESOURCES, default=FITBIT_DEFAULT_RESOURCES ): vol.All(cv.ensure_list, [vol.In(FITBIT_RESOURCES_LIST)]), - vol.Optional(CONF_CLOCK_FORMAT, default="24H"): vol.In(["12H", "24H"]), + vol.Optional(CONF_CLOCK_FORMAT, default=DEFAULT_CLOCK_FORMAT): vol.In( + ["12H", "24H"] + ), vol.Optional(CONF_UNIT_SYSTEM, default="default"): vol.In( ["en_GB", "en_US", "metric", "default"] ), @@ -164,11 +72,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def request_app_setup(hass, config, add_entities, config_path, discovery_info=None): +def request_app_setup( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + config_path: str, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Assist user with configuring the Fitbit dev application.""" configurator = hass.components.configurator - def fitbit_configuration_callback(callback_data): + def fitbit_configuration_callback(fields: list[dict[str, str]]) -> None: """Handle configuration updates.""" config_path = hass.config.path(FITBIT_CONFIG_FILE) if os.path.isfile(config_path): @@ -206,7 +120,7 @@ def request_app_setup(hass, config, add_entities, config_path, discovery_info=No ) -def request_oauth_completion(hass): +def request_oauth_completion(hass: HomeAssistant) -> None: """Request user complete Fitbit OAuth2 flow.""" configurator = hass.components.configurator if "fitbit" in _CONFIGURING: @@ -216,7 +130,7 @@ def request_oauth_completion(hass): return - def fitbit_configuration_callback(callback_data): + def fitbit_configuration_callback(fields: list[dict[str, str]]) -> None: """Handle configuration updates.""" start_url = f"{get_url(hass)}{FITBIT_AUTH_START}" @@ -231,28 +145,37 @@ def request_oauth_completion(hass): ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the Fitbit sensor.""" config_path = hass.config.path(FITBIT_CONFIG_FILE) if os.path.isfile(config_path): - config_file = load_json(config_path) + config_file: ConfigType = cast(ConfigType, load_json(config_path)) if config_file == DEFAULT_CONFIG: request_app_setup( hass, config, add_entities, config_path, discovery_info=None ) - return False + return else: save_json(config_path, DEFAULT_CONFIG) request_app_setup(hass, config, add_entities, config_path, discovery_info=None) - return False + return if "fitbit" in _CONFIGURING: hass.components.configurator.request_done(_CONFIGURING.pop("fitbit")) - access_token = config_file.get(ATTR_ACCESS_TOKEN) - refresh_token = config_file.get(ATTR_REFRESH_TOKEN) - expires_at = config_file.get(ATTR_LAST_SAVED_AT) - if None not in (access_token, refresh_token): + access_token: str | None = config_file.get(ATTR_ACCESS_TOKEN) + refresh_token: str | None = config_file.get(ATTR_REFRESH_TOKEN) + expires_at: int | None = config_file.get(ATTR_LAST_SAVED_AT) + if ( + access_token is not None + and refresh_token is not None + and expires_at is not None + ): authd_client = Fitbit( config_file.get(CONF_CLIENT_ID), config_file.get(CONF_CLIENT_SECRET), @@ -278,8 +201,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] registered_devs = authd_client.get_devices() - clock_format = config.get(CONF_CLOCK_FORMAT) - for resource in config.get(CONF_MONITORED_RESOURCES): + clock_format = config.get(CONF_CLOCK_FORMAT, DEFAULT_CLOCK_FORMAT) + for resource in config.get(CONF_MONITORED_RESOURCES, FITBIT_DEFAULT_RESOURCES): # monitor battery for all linked FitBit devices if resource == "devices/battery": @@ -339,16 +262,21 @@ class FitbitAuthCallbackView(HomeAssistantView): url = FITBIT_AUTH_CALLBACK_PATH name = "api:fitbit:callback" - def __init__(self, config, add_entities, oauth): + def __init__( + self, + config: ConfigType, + add_entities: AddEntitiesCallback, + oauth: FitbitOauth2Client, + ) -> None: """Initialize the OAuth callback view.""" self.config = config self.add_entities = add_entities self.oauth = oauth @callback - async def get(self, request): + async def get(self, request: Request) -> str: """Finish OAuth callback request.""" - hass = request.app["hass"] + hass: HomeAssistant = request.app["hass"] data = request.query response_message = """Fitbit has been successfully authorized! @@ -408,8 +336,14 @@ class FitbitSensor(SensorEntity): """Implementation of a Fitbit sensor.""" def __init__( - self, client, config_path, resource_type, is_metric, clock_format, extra=None - ): + self, + client: Fitbit, + config_path: str, + resource_type: str, + is_metric: bool, + clock_format: str, + extra: dict[str, str] | None = None, + ) -> None: """Initialize the Fitbit sensor.""" self.client = client self.config_path = config_path @@ -418,7 +352,7 @@ class FitbitSensor(SensorEntity): self.clock_format = clock_format self.extra = extra self._name = FITBIT_RESOURCES_LIST[self.resource_type][0] - if self.extra: + if self.extra is not None: self._name = f"{self.extra.get('deviceVersion')} Battery" unit_type = FITBIT_RESOURCES_LIST[self.resource_type][1] if unit_type == "": @@ -432,48 +366,53 @@ class FitbitSensor(SensorEntity): measurement_system = FITBIT_MEASUREMENTS["en_US"] unit_type = measurement_system[split_resource[-1]] self._unit_of_measurement = unit_type - self._state = 0 + self._state: str | None = None @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def state(self): + def state(self) -> str | None: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend, if any.""" - if self.resource_type == "devices/battery" and self.extra: - battery_level = BATTERY_LEVELS[self.extra.get("battery")] - return icon_for_battery_level(battery_level=battery_level, charging=None) - return f"mdi:{FITBIT_RESOURCES_LIST[self.resource_type][2]}" + if self.resource_type == "devices/battery" and self.extra is not None: + extra_battery = self.extra.get("battery") + if extra_battery is not None: + battery_level = BATTERY_LEVELS.get(extra_battery) + if battery_level is not None: + return icon_for_battery_level(battery_level=battery_level) + fitbit_ressource = FITBIT_RESOURCES_LIST[self.resource_type] + return f"mdi:{fitbit_ressource[2]}" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str | None]: """Return the state attributes.""" - attrs = {} + attrs: dict[str, str | None] = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - if self.extra: + if self.extra is not None: attrs["model"] = self.extra.get("deviceVersion") - attrs["type"] = self.extra.get("type").lower() + extra_type = self.extra.get("type") + attrs["type"] = extra_type.lower() if extra_type is not None else None return attrs - def update(self): + def update(self) -> None: """Get the latest data from the Fitbit API and update the states.""" - if self.resource_type == "devices/battery" and self.extra: - registered_devs = self.client.get_devices() + if self.resource_type == "devices/battery" and self.extra is not None: + registered_devs: list[dict[str, Any]] = self.client.get_devices() device_id = self.extra.get("id") self.extra = list( filter(lambda device: device.get("id") == device_id, registered_devs) diff --git a/mypy.ini b/mypy.ini index 0ca5b618fc6..c2af1b1f643 100644 --- a/mypy.ini +++ b/mypy.ini @@ -242,6 +242,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.fitbit.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.fritzbox.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -884,9 +895,6 @@ ignore_errors = true [mypy-homeassistant.components.firmata.*] ignore_errors = true -[mypy-homeassistant.components.fitbit.*] -ignore_errors = true - [mypy-homeassistant.components.flo.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 94f5bc81a0d..3372750b507 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -63,7 +63,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.fints.*", "homeassistant.components.fireservicerota.*", "homeassistant.components.firmata.*", - "homeassistant.components.fitbit.*", "homeassistant.components.flo.*", "homeassistant.components.fortios.*", "homeassistant.components.foscam.*", From 6e087039f47f5eac3bddee31301ea30d384fba01 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 21 May 2021 18:35:27 +0300 Subject: [PATCH 626/852] Add min/max/step to MQTT number (#50869) --- .../components/mqtt/abbreviations.py | 3 + homeassistant/components/mqtt/number.py | 80 ++++++++++++-- tests/components/mqtt/test_number.py | 104 ++++++++++++++++++ 3 files changed, 176 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 5d34c92c1b1..4eef2d372ae 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -75,6 +75,8 @@ ABBREVIATIONS = { "json_attr": "json_attributes", "json_attr_t": "json_attributes_topic", "json_attr_tpl": "json_attributes_template", + "max": "max", + "min": "min", "max_mirs": "max_mireds", "min_mirs": "min_mireds", "max_temp": "max_temp", @@ -170,6 +172,7 @@ ABBREVIATIONS = { "stat_t": "state_topic", "stat_tpl": "state_template", "stat_val_tpl": "state_value_template", + "step": "step", "stype": "subtype", "sup_feat": "supported_features", "sup_clrm": "supported_color_modes", diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index dd4cfb47acb..95409924fa4 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -5,7 +5,12 @@ import logging import voluptuous as vol from homeassistant.components import number -from homeassistant.components.number import NumberEntity +from homeassistant.components.number import ( + DEFAULT_MAX_VALUE, + DEFAULT_MIN_VALUE, + DEFAULT_STEP, + NumberEntity, +) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -28,15 +33,36 @@ from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_hel _LOGGER = logging.getLogger(__name__) +CONF_MIN = "min" +CONF_MAX = "max" +CONF_STEP = "step" + DEFAULT_NAME = "MQTT Number" DEFAULT_OPTIMISTIC = False -PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - } -).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + +def validate_config(config): + """Validate that the configuration is valid, throws if it isn't.""" + if config.get(CONF_MIN) >= config.get(CONF_MAX): + raise vol.Invalid(f"'{CONF_MAX}'' must be > '{CONF_MIN}'") + + return config + + +PLATFORM_SCHEMA = vol.All( + mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), + vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( + vol.Coerce(float), vol.Range(min=1e-3) + ), + }, + ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + validate_config, +) async def async_setup_platform( @@ -67,6 +93,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): def __init__(self, config, config_entry, discovery_data): """Initialize the MQTT Number.""" + self._config = config self._sub_state = None self._current_number = None @@ -89,12 +116,28 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """Handle new MQTT messages.""" try: if msg.payload.decode("utf-8").isnumeric(): - self._current_number = int(msg.payload) + num_value = int(msg.payload) else: - self._current_number = float(msg.payload) - self.async_write_ha_state() + num_value = float(msg.payload) except ValueError: - _LOGGER.warning("We received <%s> which is not a Number", msg.payload) + _LOGGER.warning( + "Payload '%s' is not a Number", + msg.payload.decode("utf-8", errors="ignore"), + ) + return + + if num_value < self.min_value or num_value > self.max_value: + _LOGGER.error( + "Invalid value for %s: %s (range %s - %s)", + self.entity_id, + num_value, + self.min_value, + self.max_value, + ) + return + + self._current_number = num_value + self.async_write_ha_state() if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. @@ -118,6 +161,21 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): if last_state: self._current_number = last_state.state + @property + def min_value(self) -> float: + """Return the minimum value.""" + return self._config[CONF_MIN] + + @property + def max_value(self) -> float: + """Return the maximum value.""" + return self._config[CONF_MAX] + + @property + def step(self) -> float: + """Return the increment/decrement step.""" + return self._config[CONF_STEP] + @property def value(self): """Return the current value.""" diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index ac5285e9855..d93b0483865 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -5,7 +5,11 @@ from unittest.mock import patch import pytest from homeassistant.components import number +from homeassistant.components.mqtt.number import CONF_MAX, CONF_MIN from homeassistant.components.number import ( + ATTR_MAX, + ATTR_MIN, + ATTR_STEP, ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -357,3 +361,103 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload=b"1" ) + + +async def test_min_max_step_attributes(hass, mqtt_mock): + """Test min/max/step attributes.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 5, + "max": 110, + "step": 20, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.attributes.get(ATTR_MIN) == 5 + assert state.attributes.get(ATTR_MAX) == 110 + assert state.attributes.get(ATTR_STEP) == 20 + + +async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock): + """Test invalid min/max attributes.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 35, + "max": 10, + } + }, + ) + await hass.async_block_till_done() + + assert f"'{CONF_MAX}'' must be > '{CONF_MIN}'" in caplog.text + + +async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): + """Test warning for MQTT payload which is not a number.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, "not_a_number") + + await hass.async_block_till_done() + + assert "Payload 'not_a_number' is not a Number" in caplog.text + + +async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): + """Test error when MQTT payload is out of min/max range.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 5, + "max": 110, + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, "115.5") + + await hass.async_block_till_done() + + assert ( + "Invalid value for number.test_number: 115.5 (range 5.0 - 110.0)" in caplog.text + ) From dc65f279a7379d75fb49abdd1feb06f918c3666e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 21 May 2021 17:37:26 +0200 Subject: [PATCH 627/852] Add support for state_class to MQTT sensor (#50927) --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/sensor.py | 13 +++++- tests/components/mqtt/test_sensor.py | 45 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 4eef2d372ae..23c94ada4c0 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -160,6 +160,7 @@ ABBREVIATIONS = { "spd_val_tpl": "speed_value_template", "spds": "speeds", "src_type": "source_type", + "stat_cla": "state_class", "stat_clsd": "state_closed", "stat_closing": "state_closing", "stat_off": "state_off", diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index ca399161b25..145af55daa8 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -7,7 +7,11 @@ import functools import voluptuous as vol from homeassistant.components import sensor -from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA, + STATE_CLASSES_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, @@ -33,6 +37,7 @@ from .mixins import ( ) CONF_EXPIRE_AFTER = "expire_after" +CONF_STATE_CLASS = "state_class" DEFAULT_NAME = "MQTT Sensor" DEFAULT_FORCE_UPDATE = False @@ -42,6 +47,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( 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_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) @@ -173,6 +179,11 @@ class MqttSensor(MqttEntity, SensorEntity): """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) + @property + def state_class(self) -> str | None: + """Return the state class of the sensor.""" + return self._config.get(CONF_STATE_CLASS) + @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index c6ebbe98dc4..fe97bdfbfde 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -381,6 +381,51 @@ async def test_valid_device_class(hass, mqtt_mock): assert "device_class" not in state.attributes +async def test_invalid_state_class(hass, mqtt_mock): + """Test state_class option with invalid value.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "state_class": "foobarnotreal", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state is None + + +async def test_valid_state_class(hass, mqtt_mock): + """Test state_class option with valid values.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "state_class": "measurement", + }, + {"platform": "mqtt", "name": "Test 2", "state_topic": "test-topic"}, + ] + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_1") + assert state.attributes["state_class"] == "measurement" + state = hass.states.get("sensor.test_2") + assert "state_class" not in state.attributes + + async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( From 42ff687c32ea9f0f1453dcb21404ff55da977047 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Fri, 21 May 2021 17:39:18 +0100 Subject: [PATCH 628/852] Add missing type hints to websocket_api (#50915) --- .../components/websocket_api/__init__.py | 17 ++- .../components/websocket_api/auth.py | 39 ++++--- .../components/websocket_api/commands.py | 102 +++++++++++++----- .../components/websocket_api/connection.py | 38 +++---- .../components/websocket_api/const.py | 53 +++++---- .../components/websocket_api/decorators.py | 56 +++++----- .../components/websocket_api/http.py | 43 ++++---- .../components/websocket_api/messages.py | 21 ++-- .../components/websocket_api/permissions.py | 6 +- .../components/websocket_api/sensor.py | 28 +++-- .../websocket_api/test_connection.py | 7 +- 11 files changed, 251 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index e7b10e18889..52158d3f1ad 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -1,11 +1,12 @@ """WebSocket based API for Home Assistant.""" from __future__ import annotations -from typing import cast +from typing import Final, cast import voluptuous as vol from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import commands, connection, const, decorators, http, messages # noqa: F401 @@ -34,11 +35,9 @@ from .messages import ( # noqa: F401 result_message, ) -# mypy: allow-untyped-calls, allow-untyped-defs +DOMAIN: Final = const.DOMAIN -DOMAIN = const.DOMAIN - -DEPENDENCIES = ("http",) +DEPENDENCIES: Final[tuple[str]] = ("http",) @bind_hass @@ -53,8 +52,8 @@ def async_register_command( # pylint: disable=protected-access if handler is None: handler = cast(const.WebSocketCommandHandler, command_or_handler) - command = handler._ws_command # type: ignore - schema = handler._ws_schema # type: ignore + command = handler._ws_command # type: ignore[attr-defined] + schema = handler._ws_schema # type: ignore[attr-defined] else: command = command_or_handler handlers = hass.data.get(DOMAIN) @@ -63,8 +62,8 @@ def async_register_command( handlers[command] = (handler, schema) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize the websocket API.""" - hass.http.register_view(http.WebsocketAPIView) + hass.http.register_view(http.WebsocketAPIView()) commands.async_register_commands(hass, async_register_command) return True diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 3c795902900..130ffe82840 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -1,22 +1,31 @@ """Handle the auth of a connection.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Final + +from aiohttp.web import Request import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.auth.models import RefreshToken, User from homeassistant.components.http.ban import process_success_login, process_wrong_login from homeassistant.const import __version__ +from homeassistant.core import HomeAssistant from .connection import ActiveConnection from .error import Disconnect -# mypy: allow-untyped-calls, allow-untyped-defs +if TYPE_CHECKING: + from .http import WebSocketAdapter -TYPE_AUTH = "auth" -TYPE_AUTH_INVALID = "auth_invalid" -TYPE_AUTH_OK = "auth_ok" -TYPE_AUTH_REQUIRED = "auth_required" -AUTH_MESSAGE_SCHEMA = vol.Schema( +TYPE_AUTH: Final = "auth" +TYPE_AUTH_INVALID: Final = "auth_invalid" +TYPE_AUTH_OK: Final = "auth_ok" +TYPE_AUTH_REQUIRED: Final = "auth_required" + +AUTH_MESSAGE_SCHEMA: Final = vol.Schema( { vol.Required("type"): TYPE_AUTH, vol.Exclusive("api_password", "auth"): str, @@ -25,17 +34,17 @@ AUTH_MESSAGE_SCHEMA = vol.Schema( ) -def auth_ok_message(): +def auth_ok_message() -> dict[str, str]: """Return an auth_ok message.""" return {"type": TYPE_AUTH_OK, "ha_version": __version__} -def auth_required_message(): +def auth_required_message() -> dict[str, str]: """Return an auth_required message.""" return {"type": TYPE_AUTH_REQUIRED, "ha_version": __version__} -def auth_invalid_message(message): +def auth_invalid_message(message: str) -> dict[str, str]: """Return an auth_invalid message.""" return {"type": TYPE_AUTH_INVALID, "message": message} @@ -43,16 +52,20 @@ def auth_invalid_message(message): class AuthPhase: """Connection that requires client to authenticate first.""" - def __init__(self, logger, hass, send_message, request): + def __init__( + self, + logger: WebSocketAdapter, + hass: HomeAssistant, + send_message: Callable[[str | dict[str, Any]], None], + request: Request, + ) -> None: """Initialize the authentiated connection.""" self._hass = hass self._send_message = send_message self._logger = logger self._request = request - self._authenticated = False - self._connection = None - async def async_handle(self, msg): + async def async_handle(self, msg: dict[str, str]) -> ActiveConnection: """Handle authentication.""" try: msg = AUTH_MESSAGE_SCHEMA(msg) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 53ff6d1da26..179fbcd1a30 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1,6 +1,10 @@ """Commands part of Websocket API.""" +from __future__ import annotations + import asyncio +from collections.abc import Callable import json +from typing import Any import voluptuous as vol @@ -8,7 +12,7 @@ from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_READ from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.const import EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL -from homeassistant.core import callback +from homeassistant.core import Context, Event, HomeAssistant, callback from homeassistant.exceptions import ( HomeAssistantError, ServiceNotFound, @@ -17,19 +21,25 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import TrackTemplate, async_track_template_result +from homeassistant.helpers.event import ( + TrackTemplate, + TrackTemplateResult, + async_track_template_result, +) from homeassistant.helpers.json import ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations from . import const, decorators, messages - -# mypy: allow-untyped-calls, allow-untyped-defs +from .connection import ActiveConnection @callback -def async_register_commands(hass, async_reg): +def async_register_commands( + hass: HomeAssistant, + async_reg: Callable[[HomeAssistant, const.WebSocketCommandHandler], None], +) -> None: """Register commands.""" async_reg(hass, handle_call_service) async_reg(hass, handle_entity_source) @@ -49,7 +59,7 @@ def async_register_commands(hass, async_reg): async_reg(hass, handle_unsubscribe_events) -def pong_message(iden): +def pong_message(iden: int) -> dict[str, Any]: """Return a pong message.""" return {"id": iden, "type": "pong"} @@ -61,7 +71,9 @@ def pong_message(iden): vol.Optional("event_type", default=MATCH_ALL): str, } ) -def handle_subscribe_events(hass, connection, msg): +def handle_subscribe_events( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle subscribe events command.""" # Circular dep # pylint: disable=import-outside-toplevel @@ -75,7 +87,7 @@ def handle_subscribe_events(hass, connection, msg): if event_type == EVENT_STATE_CHANGED: @callback - def forward_events(event): + def forward_events(event: Event) -> None: """Forward state changed events to websocket.""" if not connection.user.permissions.check_entity( event.data["entity_id"], POLICY_READ @@ -87,7 +99,7 @@ def handle_subscribe_events(hass, connection, msg): else: @callback - def forward_events(event): + def forward_events(event: Event) -> None: """Forward events to websocket.""" if event.event_type == EVENT_TIME_CHANGED: return @@ -107,11 +119,13 @@ def handle_subscribe_events(hass, connection, msg): vol.Required("type"): "subscribe_bootstrap_integrations", } ) -def handle_subscribe_bootstrap_integrations(hass, connection, msg): +def handle_subscribe_bootstrap_integrations( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle subscribe bootstrap integrations command.""" @callback - def forward_bootstrap_integrations(message): + def forward_bootstrap_integrations(message: dict[str, Any]) -> None: """Forward bootstrap integrations to websocket.""" connection.send_message(messages.event_message(msg["id"], message)) @@ -129,7 +143,9 @@ def handle_subscribe_bootstrap_integrations(hass, connection, msg): vol.Required("subscription"): cv.positive_int, } ) -def handle_unsubscribe_events(hass, connection, msg): +def handle_unsubscribe_events( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle unsubscribe events command.""" subscription = msg["subscription"] @@ -154,7 +170,9 @@ def handle_unsubscribe_events(hass, connection, msg): } ) @decorators.async_response -async def handle_call_service(hass, connection, msg): +async def handle_call_service( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle call service command.""" blocking = True # We do not support templates. @@ -206,7 +224,9 @@ async def handle_call_service(hass, connection, msg): @callback @decorators.websocket_command({vol.Required("type"): "get_states"}) -def handle_get_states(hass, connection, msg): +def handle_get_states( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle get states command.""" if connection.user.permissions.access_all_entities("read"): states = hass.states.async_all() @@ -223,7 +243,9 @@ def handle_get_states(hass, connection, msg): @decorators.websocket_command({vol.Required("type"): "get_services"}) @decorators.async_response -async def handle_get_services(hass, connection, msg): +async def handle_get_services( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle get services command.""" descriptions = await async_get_all_descriptions(hass) connection.send_message(messages.result_message(msg["id"], descriptions)) @@ -231,14 +253,18 @@ async def handle_get_services(hass, connection, msg): @callback @decorators.websocket_command({vol.Required("type"): "get_config"}) -def handle_get_config(hass, connection, msg): +def handle_get_config( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle get config command.""" connection.send_message(messages.result_message(msg["id"], hass.config.as_dict())) @decorators.websocket_command({vol.Required("type"): "manifest/list"}) @decorators.async_response -async def handle_manifest_list(hass, connection, msg): +async def handle_manifest_list( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle integrations command.""" loaded_integrations = async_get_loaded_integrations(hass) integrations = await asyncio.gather( @@ -253,7 +279,9 @@ async def handle_manifest_list(hass, connection, msg): {vol.Required("type"): "manifest/get", vol.Required("integration"): str} ) @decorators.async_response -async def handle_manifest_get(hass, connection, msg): +async def handle_manifest_get( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle integrations command.""" try: integration = await async_get_integration(hass, msg["integration"]) @@ -264,7 +292,9 @@ async def handle_manifest_get(hass, connection, msg): @decorators.websocket_command({vol.Required("type"): "integration/setup_info"}) @decorators.async_response -async def handle_integration_setup_info(hass, connection, msg): +async def handle_integration_setup_info( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle integrations command.""" connection.send_result( msg["id"], @@ -277,7 +307,9 @@ async def handle_integration_setup_info(hass, connection, msg): @callback @decorators.websocket_command({vol.Required("type"): "ping"}) -def handle_ping(hass, connection, msg): +def handle_ping( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle ping command.""" connection.send_message(pong_message(msg["id"])) @@ -293,10 +325,12 @@ def handle_ping(hass, connection, msg): } ) @decorators.async_response -async def handle_render_template(hass, connection, msg): +async def handle_render_template( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle render_template command.""" template_str = msg["template"] - template_obj = template.Template(template_str, hass) + template_obj = template.Template(template_str, hass) # type: ignore[no-untyped-call] variables = msg.get("variables") timeout = msg.get("timeout") info = None @@ -319,7 +353,7 @@ async def handle_render_template(hass, connection, msg): return @callback - def _template_listener(event, updates): + def _template_listener(event: Event, updates: list[TrackTemplateResult]) -> None: nonlocal info track_template_result = updates.pop() result = track_template_result.result @@ -329,7 +363,7 @@ async def handle_render_template(hass, connection, msg): connection.send_message( messages.event_message( - msg["id"], {"result": result, "listeners": info.listeners} # type: ignore + msg["id"], {"result": result, "listeners": info.listeners} # type: ignore[attr-defined] ) ) @@ -356,7 +390,9 @@ async def handle_render_template(hass, connection, msg): @decorators.websocket_command( {vol.Required("type"): "entity/source", vol.Optional("entity_id"): [cv.entity_id]} ) -def handle_entity_source(hass, connection, msg): +def handle_entity_source( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle entity source command.""" raw_sources = entity.entity_sources(hass) entity_perm = connection.user.permissions.check_entity @@ -404,7 +440,9 @@ def handle_entity_source(hass, connection, msg): ) @decorators.require_admin @decorators.async_response -async def handle_subscribe_trigger(hass, connection, msg): +async def handle_subscribe_trigger( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle subscribe trigger command.""" # Circular dep # pylint: disable=import-outside-toplevel @@ -413,7 +451,9 @@ async def handle_subscribe_trigger(hass, connection, msg): trigger_config = await trigger.async_validate_trigger_config(hass, msg["trigger"]) @callback - def forward_triggers(variables, context=None): + def forward_triggers( + variables: dict[str, Any], context: Context | None = None + ) -> None: """Forward events to websocket.""" message = messages.event_message( msg["id"], {"variables": variables, "context": context} @@ -449,7 +489,9 @@ async def handle_subscribe_trigger(hass, connection, msg): ) @decorators.require_admin @decorators.async_response -async def handle_test_condition(hass, connection, msg): +async def handle_test_condition( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle test condition command.""" # Circular dep # pylint: disable=import-outside-toplevel @@ -470,7 +512,9 @@ async def handle_test_condition(hass, connection, msg): ) @decorators.require_admin @decorators.async_response -async def handle_execute_script(hass, connection, msg): +async def handle_execute_script( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle execute script command.""" # Circular dep # pylint: disable=import-outside-toplevel diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 4e0ba257d59..62c21ef5894 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -3,48 +3,50 @@ from __future__ import annotations import asyncio from collections.abc import Hashable -from typing import Any, Callable +from typing import TYPE_CHECKING, Any, Callable import voluptuous as vol -from homeassistant.core import Context, callback +from homeassistant.auth.models import RefreshToken, User +from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from . import const, messages -# mypy: allow-untyped-calls, allow-untyped-defs +if TYPE_CHECKING: + from .http import WebSocketAdapter class ActiveConnection: """Handle an active websocket client connection.""" - def __init__(self, logger, hass, send_message, user, refresh_token): + def __init__( + self, + logger: WebSocketAdapter, + hass: HomeAssistant, + send_message: Callable[[str | dict[str, Any]], None], + user: User, + refresh_token: RefreshToken, + ) -> None: """Initialize an active connection.""" self.logger = logger self.hass = hass self.send_message = send_message self.user = user - if refresh_token: - self.refresh_token_id = refresh_token.id - else: - self.refresh_token_id = None - + self.refresh_token_id = refresh_token.id self.subscriptions: dict[Hashable, Callable[[], Any]] = {} self.last_id = 0 - def context(self, msg): + def context(self, msg: dict[str, Any]) -> Context: """Return a context.""" - user = self.user - if user is None: - return Context() - return Context(user_id=user.id) + return Context(user_id=self.user.id) @callback def send_result(self, msg_id: int, result: Any | None = None) -> None: """Send a result message.""" self.send_message(messages.result_message(msg_id, result)) - async def send_big_result(self, msg_id, result): + async def send_big_result(self, msg_id: int, result: Any) -> None: """Send a result message that would be expensive to JSON serialize.""" content = await self.hass.async_add_executor_job( const.JSON_DUMP, messages.result_message(msg_id, result) @@ -57,7 +59,7 @@ class ActiveConnection: self.send_message(messages.error_message(msg_id, code, message)) @callback - def async_handle(self, msg): + def async_handle(self, msg: dict[str, Any]) -> None: """Handle a single incoming message.""" handlers = self.hass.data[const.DOMAIN] @@ -102,13 +104,13 @@ class ActiveConnection: self.last_id = cur_id @callback - def async_close(self): + def async_close(self) -> None: """Close down connection.""" for unsub in self.subscriptions.values(): unsub() @callback - def async_handle_exception(self, msg, err): + def async_handle_exception(self, msg: dict[str, Any], err: Exception) -> None: """Handle an exception while processing a handler.""" log_handler = self.logger.error diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 7c3f18f856c..69716b97076 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -1,9 +1,11 @@ """Websocket constants.""" +from __future__ import annotations + import asyncio from concurrent import futures from functools import partial import json -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Final from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder @@ -12,37 +14,42 @@ if TYPE_CHECKING: from .connection import ActiveConnection -WebSocketCommandHandler = Callable[[HomeAssistant, "ActiveConnection", dict], None] +WebSocketCommandHandler = Callable[ + [HomeAssistant, "ActiveConnection", Dict[str, Any]], None +] +AsyncWebSocketCommandHandler = Callable[ + [HomeAssistant, "ActiveConnection", Dict[str, Any]], Awaitable[None] +] -DOMAIN = "websocket_api" -URL = "/api/websocket" -PENDING_MSG_PEAK = 512 -PENDING_MSG_PEAK_TIME = 5 -MAX_PENDING_MSG = 2048 +DOMAIN: Final = "websocket_api" +URL: Final = "/api/websocket" +PENDING_MSG_PEAK: Final = 512 +PENDING_MSG_PEAK_TIME: Final = 5 +MAX_PENDING_MSG: Final = 2048 -ERR_ID_REUSE = "id_reuse" -ERR_INVALID_FORMAT = "invalid_format" -ERR_NOT_FOUND = "not_found" -ERR_NOT_SUPPORTED = "not_supported" -ERR_HOME_ASSISTANT_ERROR = "home_assistant_error" -ERR_UNKNOWN_COMMAND = "unknown_command" -ERR_UNKNOWN_ERROR = "unknown_error" -ERR_UNAUTHORIZED = "unauthorized" -ERR_TIMEOUT = "timeout" -ERR_TEMPLATE_ERROR = "template_error" +ERR_ID_REUSE: Final = "id_reuse" +ERR_INVALID_FORMAT: Final = "invalid_format" +ERR_NOT_FOUND: Final = "not_found" +ERR_NOT_SUPPORTED: Final = "not_supported" +ERR_HOME_ASSISTANT_ERROR: Final = "home_assistant_error" +ERR_UNKNOWN_COMMAND: Final = "unknown_command" +ERR_UNKNOWN_ERROR: Final = "unknown_error" +ERR_UNAUTHORIZED: Final = "unauthorized" +ERR_TIMEOUT: Final = "timeout" +ERR_TEMPLATE_ERROR: Final = "template_error" -TYPE_RESULT = "result" +TYPE_RESULT: Final = "result" # Define the possible errors that occur when connections are cancelled. # Originally, this was just asyncio.CancelledError, but issue #9546 showed # that futures.CancelledErrors can also occur in some situations. -CANCELLATION_ERRORS = (asyncio.CancelledError, futures.CancelledError) +CANCELLATION_ERRORS: Final = (asyncio.CancelledError, futures.CancelledError) # Event types -SIGNAL_WEBSOCKET_CONNECTED = "websocket_connected" -SIGNAL_WEBSOCKET_DISCONNECTED = "websocket_disconnected" +SIGNAL_WEBSOCKET_CONNECTED: Final = "websocket_connected" +SIGNAL_WEBSOCKET_DISCONNECTED: Final = "websocket_disconnected" # Data used to store the current connection list -DATA_CONNECTIONS = f"{DOMAIN}.connections" +DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" -JSON_DUMP = partial(json.dumps, cls=JSONEncoder, allow_nan=False) +JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, allow_nan=False) diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index cbb0e8563c5..af762cf2d46 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable from functools import wraps -from typing import Callable +from typing import Any, Callable + +import voluptuous as vol from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized @@ -12,10 +13,13 @@ from homeassistant.exceptions import Unauthorized from . import const, messages from .connection import ActiveConnection -# mypy: allow-untyped-calls, allow-untyped-defs - -async def _handle_async_response(func, hass, connection, msg): +async def _handle_async_response( + func: const.AsyncWebSocketCommandHandler, + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict[str, Any], +) -> None: """Create a response and handle exception.""" try: await func(hass, connection, msg) @@ -24,13 +28,15 @@ async def _handle_async_response(func, hass, connection, msg): def async_response( - func: Callable[[HomeAssistant, ActiveConnection, dict], Awaitable[None]] + func: const.AsyncWebSocketCommandHandler, ) -> const.WebSocketCommandHandler: """Decorate an async function to handle WebSocket API messages.""" @callback @wraps(func) - def schedule_handler(hass, connection, msg): + def schedule_handler( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] + ) -> None: """Schedule the handler.""" # As the webserver is now started before the start # event we do not want to block for websocket responders @@ -43,7 +49,9 @@ def require_admin(func: const.WebSocketCommandHandler) -> const.WebSocketCommand """Websocket decorator to require user to be an admin.""" @wraps(func) - def with_admin(hass, connection, msg): + def with_admin( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] + ) -> None: """Check admin and call function.""" user = connection.user @@ -56,34 +64,32 @@ def require_admin(func: const.WebSocketCommandHandler) -> const.WebSocketCommand def ws_require_user( - only_owner=False, - only_system_user=False, - allow_system_user=True, - only_active_user=True, - only_inactive_user=False, -): + only_owner: bool = False, + only_system_user: bool = False, + allow_system_user: bool = True, + only_active_user: bool = True, + only_inactive_user: bool = False, +) -> Callable[[const.WebSocketCommandHandler], const.WebSocketCommandHandler]: """Decorate function validating login user exist in current WS connection. Will write out error message if not authenticated. """ - def validator(func): + def validator(func: const.WebSocketCommandHandler) -> const.WebSocketCommandHandler: """Decorate func.""" @wraps(func) - def check_current_user(hass, connection, msg): + def check_current_user( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] + ) -> None: """Check current user.""" - def output_error(message_id, message): + def output_error(message_id: str, message: str) -> None: """Output error message.""" connection.send_message( messages.error_message(msg["id"], message_id, message) ) - if connection.user is None: - output_error("no_user", "Not authenticated as a user") - return - if only_owner and not connection.user.is_owner: output_error("only_owner", "Only allowed as owner") return @@ -112,16 +118,16 @@ def ws_require_user( def websocket_command( - schema: dict, + schema: dict[vol.Marker, Any], ) -> Callable[[const.WebSocketCommandHandler], const.WebSocketCommandHandler]: """Tag a function as a websocket command.""" command = schema["type"] - def decorate(func): + def decorate(func: const.WebSocketCommandHandler) -> const.WebSocketCommandHandler: """Decorate ws command function.""" # pylint: disable=protected-access - func._ws_schema = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend(schema) - func._ws_command = command + func._ws_schema = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend(schema) # type: ignore[attr-defined] + func._ws_command = command # type: ignore[attr-defined] return func return decorate diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index a84db598fdc..a80ff111f0d 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -2,15 +2,18 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from contextlib import suppress +import datetime as dt import logging +from typing import Any, Final from aiohttp import WSMsgType, web import async_timeout from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from .auth import AuthPhase, auth_required_message @@ -27,16 +30,15 @@ from .const import ( from .error import Disconnect from .messages import message_to_json -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs -_WS_LOGGER = logging.getLogger(f"{__name__}.connection") +_WS_LOGGER: Final = logging.getLogger(f"{__name__}.connection") class WebsocketAPIView(HomeAssistantView): """View to serve a websockets endpoint.""" - name = "websocketapi" - url = URL - requires_auth = False + name: str = "websocketapi" + url: str = URL + requires_auth: bool = False async def get(self, request: web.Request) -> web.WebSocketResponse: """Handle an incoming websocket connection.""" @@ -46,7 +48,7 @@ class WebsocketAPIView(HomeAssistantView): class WebSocketAdapter(logging.LoggerAdapter): """Add connection id to websocket messages.""" - def process(self, msg, kwargs): + def process(self, msg: str, kwargs: Any) -> tuple[str, Any]: """Add connid to websocket log messages.""" return f'[{self.extra["connid"]}] {msg}', kwargs @@ -54,20 +56,21 @@ class WebSocketAdapter(logging.LoggerAdapter): class WebSocketHandler: """Handle an active websocket client connection.""" - def __init__(self, hass, request): + def __init__(self, hass: HomeAssistant, request: web.Request) -> None: """Initialize an active connection.""" self.hass = hass self.request = request self.wsock: web.WebSocketResponse | None = None self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) - self._handle_task = None - self._writer_task = None + self._handle_task: asyncio.Task | None = None + self._writer_task: asyncio.Task | None = None self._logger = WebSocketAdapter(_WS_LOGGER, {"connid": id(self)}) - self._peak_checker_unsub = None + self._peak_checker_unsub: Callable[[], None] | None = None - async def _writer(self): + async def _writer(self) -> None: """Write outgoing messages.""" # Exceptions if Socket disconnected or cancelled by connection handler + assert self.wsock is not None with suppress(RuntimeError, ConnectionResetError, *CANCELLATION_ERRORS): while not self.wsock.closed: message = await self._to_write.get() @@ -78,12 +81,12 @@ class WebSocketHandler: await self.wsock.send_str(message) # Clean up the peaker checker when we shut down the writer - if self._peak_checker_unsub: + if self._peak_checker_unsub is not None: self._peak_checker_unsub() self._peak_checker_unsub = None @callback - def _send_message(self, message): + def _send_message(self, message: str | dict[str, Any]) -> None: """Send a message to the client. Closes connection if the client is not reading the messages. @@ -114,7 +117,7 @@ class WebSocketHandler: ) @callback - def _check_write_peak(self, _): + def _check_write_peak(self, _utc_time: dt.datetime) -> None: """Check that we are no longer above the write peak.""" self._peak_checker_unsub = None @@ -129,10 +132,12 @@ class WebSocketHandler: self._cancel() @callback - def _cancel(self): + def _cancel(self) -> None: """Cancel the connection.""" - self._handle_task.cancel() - self._writer_task.cancel() + if self._handle_task is not None: + self._handle_task.cancel() + if self._writer_task is not None: + self._writer_task.cancel() async def async_handle(self) -> web.WebSocketResponse: """Handle a websocket response.""" @@ -143,7 +148,7 @@ class WebSocketHandler: self._handle_task = asyncio.current_task() @callback - def handle_hass_stop(event): + def handle_hass_stop(event: Event) -> None: """Cancel this connection.""" self._cancel() diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 736a7ad59f0..8cdda3f8fa3 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -3,7 +3,7 @@ from __future__ import annotations from functools import lru_cache import logging -from typing import Any +from typing import Any, Final import voluptuous as vol @@ -17,28 +17,27 @@ from homeassistant.util.yaml.loader import JSON_TYPE from . import const -_LOGGER = logging.getLogger(__name__) -# mypy: allow-untyped-defs +_LOGGER: Final = logging.getLogger(__name__) # Minimal requirements of a message -MINIMAL_MESSAGE_SCHEMA = vol.Schema( +MINIMAL_MESSAGE_SCHEMA: Final = vol.Schema( {vol.Required("id"): cv.positive_int, vol.Required("type"): cv.string}, extra=vol.ALLOW_EXTRA, ) # Base schema to extend by message handlers -BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({vol.Required("id"): cv.positive_int}) +BASE_COMMAND_MESSAGE_SCHEMA: Final = vol.Schema({vol.Required("id"): cv.positive_int}) -IDEN_TEMPLATE = "__IDEN__" -IDEN_JSON_TEMPLATE = '"__IDEN__"' +IDEN_TEMPLATE: Final = "__IDEN__" +IDEN_JSON_TEMPLATE: Final = '"__IDEN__"' -def result_message(iden: int, result: Any = None) -> dict: +def result_message(iden: int, result: Any = None) -> dict[str, Any]: """Return a success result message.""" return {"id": iden, "type": const.TYPE_RESULT, "success": True, "result": result} -def error_message(iden: int, code: str, message: str) -> dict: +def error_message(iden: int | None, code: str, message: str) -> dict[str, Any]: """Return an error result message.""" return { "id": iden, @@ -48,7 +47,7 @@ def error_message(iden: int, code: str, message: str) -> dict: } -def event_message(iden: JSON_TYPE, event: Any) -> dict: +def event_message(iden: JSON_TYPE, event: Any) -> dict[str, Any]: """Return an event message.""" return {"id": iden, "type": "event", "event": event} @@ -75,7 +74,7 @@ def _cached_event_message(event: Event) -> str: return message_to_json(event_message(IDEN_TEMPLATE, event)) -def message_to_json(message: Any) -> str: +def message_to_json(message: dict[str, Any]) -> str: """Serialize a websocket message to json.""" try: return const.JSON_DUMP(message) diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 010a18f972c..5dade8eeb2a 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -2,6 +2,10 @@ Separate file to avoid circular imports. """ +from __future__ import annotations + +from typing import Final + from homeassistant.components.frontend import EVENT_PANELS_UPDATED from homeassistant.components.lovelace.const import EVENT_LOVELACE_UPDATED from homeassistant.components.persistent_notification import ( @@ -22,7 +26,7 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. -SUBSCRIBE_ALLOWLIST = { +SUBSCRIBE_ALLOWLIST: Final[set[str]] = { EVENT_AREA_REGISTRY_UPDATED, EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index dfcdc57842e..60d42e97604 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -1,7 +1,12 @@ """Entity to track connections to websocket API.""" +from __future__ import annotations + +from typing import Any from homeassistant.components.sensor import SensorEntity -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType from .const import ( DATA_CONNECTIONS, @@ -9,10 +14,13 @@ from .const import ( SIGNAL_WEBSOCKET_DISCONNECTED, ) -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, +) -> None: """Set up the API streams platform.""" entity = APICount() @@ -22,11 +30,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class APICount(SensorEntity): """Entity to represent how many people are connected to the stream API.""" - def __init__(self): + def __init__(self) -> None: """Initialize the API count.""" self.count = 0 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Added to hass.""" self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( @@ -40,21 +48,21 @@ class APICount(SensorEntity): ) @property - def name(self): + def name(self) -> str: """Return name of entity.""" return "Connected clients" @property - def state(self): + def state(self) -> int: """Return current API count.""" return self.count @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement.""" return "clients" @callback - def _update_count(self): + def _update_count(self) -> None: self.count = self.hass.data.get(DATA_CONNECTIONS, 0) self.async_write_ha_state() diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 55126ff1333..1d6bf5f2f6b 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -1,6 +1,7 @@ """Test WebSocket Connection class.""" import asyncio import logging +from unittest.mock import Mock import voluptuous as vol @@ -8,6 +9,8 @@ from homeassistant import exceptions from homeassistant.components import websocket_api from homeassistant.components.websocket_api import const +from tests.common import MockUser + async def test_send_big_result(hass, websocket_client): """Test sending big results over the WS.""" @@ -31,8 +34,10 @@ async def test_send_big_result(hass, websocket_client): async def test_exception_handling(): """Test handling of exceptions.""" send_messages = [] + user = MockUser() + refresh_token = Mock() conn = websocket_api.ActiveConnection( - logging.getLogger(__name__), None, send_messages.append, None, None + logging.getLogger(__name__), None, send_messages.append, user, refresh_token ) for (exc, code, err) in ( From 752a4b9d2ca7dacfc3df3f431e3e3f2e425042d2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 21 May 2021 19:31:04 +0200 Subject: [PATCH 629/852] Fix version bump script (#50932) --- script/version_bump.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/script/version_bump.py b/script/version_bump.py index f3ed5e99c55..5f1988f3c26 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -106,9 +106,15 @@ def write_version(version): major, minor, patch = str(version).split(".", 2) - content = re.sub("MAJOR_VERSION = .*\n", f"MAJOR_VERSION = {major}\n", content) - content = re.sub("MINOR_VERSION = .*\n", f"MINOR_VERSION = {minor}\n", content) - content = re.sub("PATCH_VERSION = .*\n", f'PATCH_VERSION = "{patch}"\n', content) + content = re.sub( + "MAJOR_VERSION: Final = .*\n", f"MAJOR_VERSION: Final = {major}\n", content + ) + content = re.sub( + "MINOR_VERSION: Final = .*\n", f"MINOR_VERSION: Final = {minor}\n", content + ) + content = re.sub( + "PATCH_VERSION: Final = .*\n", f'PATCH_VERSION: Final = "{patch}"\n', content + ) with open("homeassistant/const.py", "wt") as fil: content = fil.write(content) From 5491040693e3801ad2bbcb6076e3aad62479b4f7 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Fri, 21 May 2021 21:21:26 +0200 Subject: [PATCH 630/852] Fix missing link in scaffold comment (#50936) --- script/scaffold/templates/config_flow_oauth2/integration/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py index 4f15099c8e1..9ae65bb4d85 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/api.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/api.py @@ -9,7 +9,7 @@ from homeassistant.helpers import config_entry_oauth2_flow # TODO the following two API examples are based on our suggested best practices # for libraries using OAuth2 with requests or aiohttp. Delete the one you won't use. -# For more info see the docs at . +# For more info see the docs at https://developers.home-assistant.io/docs/api_lib_auth/#oauth2. class ConfigEntryAuth(my_pypi_package.AbstractAuth): From 78be237447cc9d6367986692ea5f7fc31d155056 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 22 May 2021 00:12:02 +0000 Subject: [PATCH 631/852] [ci skip] Translation update --- .../hunterdouglas_powerview/translations/ca.json | 1 + .../hunterdouglas_powerview/translations/et.json | 1 + .../hunterdouglas_powerview/translations/no.json | 1 + .../hunterdouglas_powerview/translations/ru.json | 1 + .../hunterdouglas_powerview/translations/zh-Hant.json | 1 + homeassistant/components/isy994/translations/ca.json | 4 +++- .../components/isy994/translations/zh-Hant.json | 8 ++++++++ homeassistant/components/kraken/translations/de.json | 11 +++++++++++ homeassistant/components/upnp/translations/fr.json | 1 + 9 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/kraken/translations/de.json diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ca.json b/homeassistant/components/hunterdouglas_powerview/translations/ca.json index 9029b4bcff5..2c2efd76a48 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/ca.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/ca.json @@ -7,6 +7,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Vols configurar {name} ({host})?", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/et.json b/homeassistant/components/hunterdouglas_powerview/translations/et.json index 78b9fdba10b..638aa287a76 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/et.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/et.json @@ -7,6 +7,7 @@ "cannot_connect": "\u00dchendamine nurjus", "unknown": "Ootamatu t\u00f5rge" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Kas soovid seadistada {name}({host})?", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/no.json b/homeassistant/components/hunterdouglas_powerview/translations/no.json index 14695bd815e..36c1702fde8 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/no.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/no.json @@ -7,6 +7,7 @@ "cannot_connect": "Tilkobling mislyktes", "unknown": "Uventet feil" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Vil du konfigurere {name} ({host})?", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ru.json b/homeassistant/components/hunterdouglas_powerview/translations/ru.json index ec20a2c71b1..6c2bf1a3196 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/ru.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/ru.json @@ -7,6 +7,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json index 1e02677fa44..7f1caaac7bb 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json @@ -7,6 +7,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", diff --git a/homeassistant/components/isy994/translations/ca.json b/homeassistant/components/isy994/translations/ca.json index 16755c688c0..cd0a2d2a1de 100644 --- a/homeassistant/components/isy994/translations/ca.json +++ b/homeassistant/components/isy994/translations/ca.json @@ -40,7 +40,9 @@ "system_health": { "info": { "device_connected": "ISY connectat", - "host_reachable": "Amfitri\u00f3 accessible" + "host_reachable": "Amfitri\u00f3 accessible", + "last_heartbeat": "Hora de l'\u00faltim senyal", + "websocket_status": "Estat del Socket d'esdeveniments" } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index edec9f514ce..2f955dd25e6 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -36,5 +36,13 @@ "title": "ISY994 \u9078\u9805" } } + }, + "system_health": { + "info": { + "device_connected": "ISY \u5df2\u9023\u7dda", + "host_reachable": "\u4e3b\u6a5f\u53ef\u9023", + "last_heartbeat": "\u4e0a\u6b21 Heartbeat \u6642\u9593", + "websocket_status": "\u4e8b\u4ef6 Socket \u72c0\u614b" + } } } \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/de.json b/homeassistant/components/kraken/translations/de.json new file mode 100644 index 00000000000..0a1e5f79414 --- /dev/null +++ b/homeassistant/components/kraken/translations/de.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "tracked_asset_pairs": "Verfolgte Asset-Paare" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/fr.json b/homeassistant/components/upnp/translations/fr.json index 7ef9ae20c82..fe1f1366d39 100644 --- a/homeassistant/components/upnp/translations/fr.json +++ b/homeassistant/components/upnp/translations/fr.json @@ -21,6 +21,7 @@ "user": { "data": { "scan_interval": "Intervalle de mise \u00e0 jour (secondes, minimum 30)", + "unique_id": "Appareil", "usn": "Appareil" } } From 92d1871de5e6da1e66e63cea13aecc6ea446c501 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 22 May 2021 02:57:30 -0400 Subject: [PATCH 632/852] Fix flaky vizio test and add comments to explain logic (#50948) --- homeassistant/components/vizio/__init__.py | 4 ++++ tests/components/vizio/test_init.py | 13 +++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index fb3a399327d..7c1ed7e8fa7 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -113,6 +113,9 @@ class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator): """Update data via library.""" data = await gen_apps_list_from_url(session=async_get_clientsession(self.hass)) if not data: + # For every failure, increase the fail count until we reach the threshold. + # We then log a warning, increase the threshold, and reset the fail count. + # This is here to prevent silent failures but to reduce repeat logs. if self.fail_count == self.fail_threshold: _LOGGER.warning( ( @@ -126,6 +129,7 @@ class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator): else: self.fail_count += 1 return self.data + # Reset the fail count and threshold when the data is successfully retrieved self.fail_count = 0 self.fail_threshold = 10 return sorted(data, key=lambda app: app["name"]) diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index c3e8afe49e6..ccda9253ec7 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -94,14 +94,11 @@ async def test_coordinator_update_failure( assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 assert DOMAIN in hass.data - for days in range(1, 10): + # Failing 25 days in a row should result in a single log message + # (first one after 10 days, next one would be at 30 days) + for days in range(1, 25): async_fire_time_changed(hass, now + timedelta(days=days)) await hass.async_block_till_done() - assert ( - "Unable to retrieve the apps list from the external server" - not in caplog.text - ) - async_fire_time_changed(hass, now + timedelta(days=10)) - await hass.async_block_till_done() - assert "Unable to retrieve the apps list from the external server" in caplog.text + err_msg = "Unable to retrieve the apps list from the external server" + assert len([record for record in caplog.records if err_msg in record.msg]) == 1 From 15e2c6d7dce94cb253ac5819d1f2b499ed7d27a6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 22 May 2021 09:34:49 +0200 Subject: [PATCH 633/852] Fix typing for dt_util as_timestamp (#50886) * Fix typing for dt_util::as_timestamp * Apply suggestions from code review --- homeassistant/util/dt.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 656f77b3289..a9a6ca4e3a3 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -79,10 +79,11 @@ def as_utc(dattim: dt.datetime) -> dt.datetime: return dattim.astimezone(UTC) -def as_timestamp(dt_value: dt.datetime) -> float: +def as_timestamp(dt_value: dt.datetime | str) -> float: """Convert a date/time into a unix time (seconds since 1970).""" - if hasattr(dt_value, "timestamp"): - parsed_dt: dt.datetime | None = dt_value + parsed_dt: dt.datetime | None + if isinstance(dt_value, dt.datetime): + parsed_dt = dt_value else: parsed_dt = parse_datetime(str(dt_value)) if parsed_dt is None: From 2e316f6fd54cc833071ee76db76ba5323510ec6a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 22 May 2021 10:14:59 +0200 Subject: [PATCH 634/852] Add strict type annotations to canary (#50943) * Add strict type annotations * Add missing futur import * Apply suggestions * Apply suggestions --- .strict-typing | 1 + homeassistant/components/canary/__init__.py | 16 +++-- .../components/canary/alarm_control_panel.py | 37 ++++++---- homeassistant/components/canary/camera.py | 68 ++++++++++++------- .../components/canary/config_flow.py | 16 ++--- homeassistant/components/canary/const.py | 16 +++-- .../components/canary/coordinator.py | 16 +++-- homeassistant/components/canary/model.py | 18 +++++ homeassistant/components/canary/sensor.py | 67 ++++++++++-------- mypy.ini | 14 +++- script/hassfest/mypy_config.py | 1 - 11 files changed, 175 insertions(+), 95 deletions(-) create mode 100644 homeassistant/components/canary/model.py diff --git a/.strict-typing b/.strict-typing index 4087f970e47..062eee858d5 100644 --- a/.strict-typing +++ b/.strict-typing @@ -18,6 +18,7 @@ homeassistant.components.bond.* homeassistant.components.brother.* homeassistant.components.calendar.* homeassistant.components.camera.* +homeassistant.components.canary.* homeassistant.components.cover.* homeassistant.components.device_automation.* homeassistant.components.elgato.* diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index c29dfeb1a71..b276fc4ed34 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -1,9 +1,12 @@ """Support for Canary devices.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Final from canary.api import Api -from requests import ConnectTimeout, HTTPError +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN @@ -12,6 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -23,11 +27,11 @@ from .const import ( ) from .coordinator import CanaryDataUpdateCoordinator -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) +MIN_TIME_BETWEEN_UPDATES: Final = timedelta(seconds=30) -CONFIG_SCHEMA = vol.Schema( +CONFIG_SCHEMA: Final = vol.Schema( vol.All( cv.deprecated(DOMAIN), { @@ -45,10 +49,10 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["alarm_control_panel", "camera", "sensor"] +PLATFORMS: Final[list[str]] = ["alarm_control_panel", "camera", "sensor"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Canary integration.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index f0d6deb477b..4e29c40f49f 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -1,7 +1,14 @@ """Support for Canary alarm.""" from __future__ import annotations -from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT +from typing import Any + +from canary.api import ( + LOCATION_MODE_AWAY, + LOCATION_MODE_HOME, + LOCATION_MODE_NIGHT, + Location, +) from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( @@ -44,29 +51,33 @@ async def async_setup_entry( class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): """Representation of a Canary alarm control panel.""" - def __init__(self, coordinator, location): + coordinator: CanaryDataUpdateCoordinator + + def __init__( + self, coordinator: CanaryDataUpdateCoordinator, location: Location + ) -> None: """Initialize a Canary security camera.""" super().__init__(coordinator) - self._location_id = location.location_id - self._location_name = location.name + self._location_id: str = location.location_id + self._location_name: str = location.name @property - def location(self): + def location(self) -> Location: """Return information about the location.""" return self.coordinator.data["locations"][self._location_id] @property - def name(self): + def name(self) -> str: """Return the name of the alarm.""" return self._location_name @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the alarm.""" return str(self._location_id) @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" if self.location.is_private: return STATE_ALARM_DISARMED @@ -87,25 +98,25 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {"private": self.location.is_private} - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" self.coordinator.canary.set_location_mode( self._location_id, self.location.mode.name, True ) - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_HOME) - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_AWAY) - def alarm_arm_night(self, code=None): + def alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" self.coordinator.canary.set_location_mode( self._location_id, LOCATION_MODE_NIGHT diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index ada9d168942..b1725945db2 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -3,17 +3,25 @@ from __future__ import annotations import asyncio from datetime import timedelta +from typing import Final +from aiohttp.web import Request, StreamResponse +from canary.api import Device, Location +from canary.live_stream_api import LiveStreamSession from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + Camera, +) +from homeassistant.components.ffmpeg import DATA_FFMPEG, FFmpegManager from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import Throttle @@ -28,11 +36,11 @@ from .const import ( ) from .coordinator import CanaryDataUpdateCoordinator -MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) +MIN_TIME_BETWEEN_SESSION_RENEW: Final = timedelta(seconds=90) -PLATFORM_SCHEMA = vol.All( +PLATFORM_SCHEMA: Final = vol.All( cv.deprecated(CONF_FFMPEG_ARGUMENTS), - PLATFORM_SCHEMA.extend( + PARENT_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS @@ -51,10 +59,10 @@ async def async_setup_entry( coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - ffmpeg_arguments = entry.options.get( + ffmpeg_arguments: str = entry.options.get( CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS ) - cameras = [] + cameras: list[CanaryCamera] = [] for location_id, location in coordinator.data["locations"].items(): for device in location.devices: @@ -76,37 +84,47 @@ async def async_setup_entry( class CanaryCamera(CoordinatorEntity, Camera): """An implementation of a Canary security camera.""" - def __init__(self, hass, coordinator, location_id, device, timeout, ffmpeg_args): + coordinator: CanaryDataUpdateCoordinator + + def __init__( + self, + hass: HomeAssistant, + coordinator: CanaryDataUpdateCoordinator, + location_id: str, + device: Device, + timeout: int, + ffmpeg_args: str, + ) -> None: """Initialize a Canary security camera.""" super().__init__(coordinator) Camera.__init__(self) - self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg: FFmpegManager = hass.data[DATA_FFMPEG] self._ffmpeg_arguments = ffmpeg_args self._location_id = location_id self._device = device - self._device_id = device.device_id - self._device_name = device.name + self._device_id: str = device.device_id + self._device_name: str = device.name self._device_type_name = device.device_type["name"] self._timeout = timeout - self._live_stream_session = None + self._live_stream_session: LiveStreamSession | None = None @property - def location(self): + def location(self) -> Location: """Return information about the location.""" return self.coordinator.data["locations"][self._location_id] @property - def name(self): + def name(self) -> str: """Return the name of this device.""" return self._device_name @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of this camera.""" return str(self._device_id) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" return { "identifiers": {(DOMAIN, str(self._device_id))}, @@ -116,16 +134,16 @@ class CanaryCamera(CoordinatorEntity, Camera): } @property - def is_recording(self): + def is_recording(self) -> bool: """Return true if the device is recording.""" - return self.location.is_recording + return self.location.is_recording # type: ignore[no-any-return] @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" return not self.location.is_recording - async def async_camera_image(self): + async def async_camera_image(self) -> bytes | None: """Return a still image response from the camera.""" await self.hass.async_add_executor_job(self.renew_live_stream_session) live_stream_url = await self.hass.async_add_executor_job( @@ -133,7 +151,7 @@ class CanaryCamera(CoordinatorEntity, Camera): ) ffmpeg = ImageFrame(self._ffmpeg.binary) - image = await asyncio.shield( + image: bytes | None = await asyncio.shield( ffmpeg.get_image( live_stream_url, output_format=IMAGE_JPEG, @@ -142,10 +160,12 @@ class CanaryCamera(CoordinatorEntity, Camera): ) return image - async def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream( + self, request: Request + ) -> StreamResponse | None: """Generate an HTTP MJPEG stream from the camera.""" if self._live_stream_session is None: - return + return None stream = CameraMjpeg(self._ffmpeg.binary) await stream.open_camera( @@ -164,7 +184,7 @@ class CanaryCamera(CoordinatorEntity, Camera): await stream.close() @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) - def renew_live_stream_session(self): + def renew_live_stream_session(self) -> None: """Renew live stream session.""" self._live_stream_session = self.coordinator.canary.get_live_stream_session( self._device diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py index f54ae3c308e..ac779a9cb69 100644 --- a/homeassistant/components/canary/config_flow.py +++ b/homeassistant/components/canary/config_flow.py @@ -2,13 +2,13 @@ from __future__ import annotations import logging -from typing import Any +from typing import Final from canary.api import Api -from requests import ConnectTimeout, HTTPError +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, OptionsFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult @@ -21,10 +21,10 @@ from .const import ( DOMAIN, ) -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: +def validate_input(hass: HomeAssistant, data: ConfigType) -> bool: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -46,7 +46,7 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" return CanaryOptionsFlowHandler(config_entry) @@ -100,11 +100,11 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN): class CanaryOptionsFlowHandler(OptionsFlow): """Handle Canary client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: ConfigType | None = None): + async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: """Manage Canary options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/canary/const.py b/homeassistant/components/canary/const.py index 8219a485ef9..210da35c7c1 100644 --- a/homeassistant/components/canary/const.py +++ b/homeassistant/components/canary/const.py @@ -1,16 +1,18 @@ """Constants for the Canary integration.""" -DOMAIN = "canary" +from typing import Final -MANUFACTURER = "Canary Connect, Inc" +DOMAIN: Final = "canary" + +MANUFACTURER: Final = "Canary Connect, Inc" # Configuration -CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" +CONF_FFMPEG_ARGUMENTS: Final = "ffmpeg_arguments" # Data -DATA_COORDINATOR = "coordinator" -DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" +DATA_COORDINATOR: Final = "coordinator" +DATA_UNDO_UPDATE_LISTENER: Final = "undo_update_listener" # Defaults -DEFAULT_FFMPEG_ARGUMENTS = "-pred 1" -DEFAULT_TIMEOUT = 10 +DEFAULT_FFMPEG_ARGUMENTS: Final = "-pred 1" +DEFAULT_TIMEOUT: Final = 10 diff --git a/homeassistant/components/canary/coordinator.py b/homeassistant/components/canary/coordinator.py index d1bdac5eee9..4c6c9ce5777 100644 --- a/homeassistant/components/canary/coordinator.py +++ b/homeassistant/components/canary/coordinator.py @@ -1,15 +1,19 @@ """Provides the Canary DataUpdateCoordinator.""" +from __future__ import annotations + +from collections.abc import ValuesView from datetime import timedelta import logging from async_timeout import timeout -from canary.api import Api -from requests import ConnectTimeout, HTTPError +from canary.api import Api, Location +from requests.exceptions import ConnectTimeout, HTTPError from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN +from .model import CanaryData _LOGGER = logging.getLogger(__name__) @@ -29,10 +33,10 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator): update_interval=update_interval, ) - def _update_data(self) -> dict: + def _update_data(self) -> CanaryData: """Fetch data from Canary via sync functions.""" - locations_by_id = {} - readings_by_device_id = {} + locations_by_id: dict[str, Location] = {} + readings_by_device_id: dict[str, ValuesView] = {} for location in self.canary.get_locations(): location_id = location.location_id @@ -49,7 +53,7 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator): "readings": readings_by_device_id, } - async def _async_update_data(self) -> dict: + async def _async_update_data(self) -> CanaryData: """Fetch data from Canary.""" try: diff --git a/homeassistant/components/canary/model.py b/homeassistant/components/canary/model.py new file mode 100644 index 00000000000..35c6a61a835 --- /dev/null +++ b/homeassistant/components/canary/model.py @@ -0,0 +1,18 @@ +"""Constants for the Canary integration.""" + +from __future__ import annotations + +from collections.abc import ValuesView +from typing import List, Optional, Tuple, TypedDict + +from canary.api import Location + + +class CanaryData(TypedDict): + """TypedDict for Canary Coordinator Data.""" + + locations: dict[str, Location] + readings: dict[str, ValuesView] + + +SensorTypeItem = Tuple[str, Optional[str], Optional[str], Optional[str], List[str]] diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 0378d34a989..91dc3bad5eb 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,7 +1,9 @@ """Support for Canary sensors.""" from __future__ import annotations -from canary.api import SensorType +from typing import Final + +from canary.api import Device, Location, SensorType from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -15,41 +17,43 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER from .coordinator import CanaryDataUpdateCoordinator +from .model import SensorTypeItem -SENSOR_VALUE_PRECISION = 2 -ATTR_AIR_QUALITY = "air_quality" +SENSOR_VALUE_PRECISION: Final = 2 +ATTR_AIR_QUALITY: Final = "air_quality" # Define variables to store the device names, as referred to by the Canary API. # Note: If Canary change the name of any of their devices (which they have done), # then these variables will need updating, otherwise the sensors will stop working # and disappear in Home Assistant. -CANARY_PRO = "Canary Pro" -CANARY_FLEX = "Canary Flex" +CANARY_PRO: Final = "Canary Pro" +CANARY_FLEX: Final = "Canary Flex" # Sensor types are defined like so: # sensor type name, unit_of_measurement, icon, device class, products supported -SENSOR_TYPES = [ - ["temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, [CANARY_PRO]], - ["humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, [CANARY_PRO]], - ["air_quality", None, "mdi:weather-windy", None, [CANARY_PRO]], - [ +SENSOR_TYPES: Final[list[SensorTypeItem]] = [ + ("temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, [CANARY_PRO]), + ("humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, [CANARY_PRO]), + ("air_quality", None, "mdi:weather-windy", None, [CANARY_PRO]), + ( "wifi", SIGNAL_STRENGTH_DECIBELS_MILLIWATT, None, DEVICE_CLASS_SIGNAL_STRENGTH, [CANARY_FLEX], - ], - ["battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY, [CANARY_FLEX]], + ), + ("battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY, [CANARY_FLEX]), ] -STATE_AIR_QUALITY_NORMAL = "normal" -STATE_AIR_QUALITY_ABNORMAL = "abnormal" -STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal" +STATE_AIR_QUALITY_NORMAL: Final = "normal" +STATE_AIR_QUALITY_ABNORMAL: Final = "abnormal" +STATE_AIR_QUALITY_VERY_ABNORMAL: Final = "very_abnormal" async def async_setup_entry( @@ -61,7 +65,7 @@ async def async_setup_entry( coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - sensors = [] + sensors: list[CanarySensor] = [] for location in coordinator.data["locations"].values(): for device in location.devices: @@ -79,8 +83,17 @@ async def async_setup_entry( class CanarySensor(CoordinatorEntity, SensorEntity): """Representation of a Canary sensor.""" - def __init__(self, coordinator, sensor_type, location, device): + coordinator: CanaryDataUpdateCoordinator + + def __init__( + self, + coordinator: CanaryDataUpdateCoordinator, + sensor_type: SensorTypeItem, + location: Location, + device: Device, + ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self._sensor_type = sensor_type self._device_id = device.device_id @@ -105,7 +118,7 @@ class CanarySensor(CoordinatorEntity, SensorEntity): self._canary_type = canary_sensor_type @property - def reading(self): + def reading(self) -> float | None: """Return the device sensor reading.""" readings = self.coordinator.data["readings"][self._device_id] @@ -124,22 +137,22 @@ class CanarySensor(CoordinatorEntity, SensorEntity): return None @property - def name(self): + def name(self) -> str: """Return the name of the Canary sensor.""" return self._name @property - def state(self): + def state(self) -> float | None: """Return the state of the sensor.""" return self.reading @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of this sensor.""" return f"{self._device_id}_{self._sensor_type[0]}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" return { "identifiers": {(DOMAIN, str(self._device_id))}, @@ -149,22 +162,22 @@ class CanarySensor(CoordinatorEntity, SensorEntity): } @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" return self._sensor_type[1] @property - def device_class(self): + def device_class(self) -> str | None: """Device class for the sensor.""" return self._sensor_type[3] @property - def icon(self): + def icon(self) -> str | None: """Icon for the sensor.""" return self._sensor_type[2] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str] | None: """Return the state attributes.""" reading = self.reading @@ -174,7 +187,7 @@ class CanarySensor(CoordinatorEntity, SensorEntity): air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL elif reading <= 0.59: air_quality = STATE_AIR_QUALITY_ABNORMAL - elif reading <= 1.0: + else: air_quality = STATE_AIR_QUALITY_NORMAL return {ATTR_AIR_QUALITY: air_quality} diff --git a/mypy.ini b/mypy.ini index c2af1b1f643..ae8d4d7fa63 100644 --- a/mypy.ini +++ b/mypy.ini @@ -209,6 +209,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.canary.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.cover.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -799,9 +810,6 @@ ignore_errors = true [mypy-homeassistant.components.bsblan.*] ignore_errors = true -[mypy-homeassistant.components.canary.*] -ignore_errors = true - [mypy-homeassistant.components.cast.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 3372750b507..743c17088c6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -31,7 +31,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.bluetooth_tracker.*", "homeassistant.components.bmw_connected_drive.*", "homeassistant.components.bsblan.*", - "homeassistant.components.canary.*", "homeassistant.components.cast.*", "homeassistant.components.cert_expiry.*", "homeassistant.components.climacell.*", From b704f0e729dd366798ec40a8c578cdb43b03b988 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Sat, 22 May 2021 09:15:15 +0100 Subject: [PATCH 635/852] Add strict typing to device_tracker (#50930) * Add strict typing to device_tracker * Update homeassistant/components/device_tracker/legacy.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + .../components/device_tracker/__init__.py | 4 +- .../components/device_tracker/config_entry.py | 40 +++-- .../components/device_tracker/const.py | 51 +++--- .../device_tracker/device_trigger.py | 10 +- .../components/device_tracker/legacy.py | 160 ++++++++++-------- mypy.ini | 11 ++ 7 files changed, 157 insertions(+), 120 deletions(-) diff --git a/.strict-typing b/.strict-typing index 062eee858d5..a6a8a4c3e22 100644 --- a/.strict-typing +++ b/.strict-typing @@ -21,6 +21,7 @@ homeassistant.components.camera.* homeassistant.components.canary.* homeassistant.components.cover.* homeassistant.components.device_automation.* +homeassistant.components.device_tracker.* homeassistant.components.elgato.* homeassistant.components.fitbit.* homeassistant.components.fritzbox.* diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index dfdfd678c0f..fa537d4af53 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -37,12 +37,12 @@ from .legacy import ( # noqa: F401 @bind_hass -def is_on(hass: HomeAssistant, entity_id: str): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return the state if any or a specified device is home.""" return hass.states.is_state(entity_id, STATE_HOME) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the device tracker.""" await async_setup_legacy_integration(hass, config) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 6f4e608520c..97b79306e7b 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import final from homeassistant.components import zone +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, @@ -12,13 +13,15 @@ from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import StateType from .const import ATTR_HOST_NAME, ATTR_IP, ATTR_MAC, ATTR_SOURCE_TYPE, DOMAIN, LOGGER -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an entry.""" component: EntityComponent | None = hass.data.get(DOMAIN) @@ -28,16 +31,17 @@ async def async_setup_entry(hass, entry): return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class BaseTrackerEntity(Entity): """Represent a tracked device.""" @property - def battery_level(self): + def battery_level(self) -> int | None: """Return the battery level of the device. Percentage from 0-100. @@ -45,16 +49,16 @@ class BaseTrackerEntity(Entity): return None @property - def source_type(self): + def source_type(self) -> str: """Return the source type, eg gps or router, of the device.""" raise NotImplementedError @property - def state_attributes(self): + def state_attributes(self) -> dict[str, StateType]: """Return the device state attributes.""" - attr = {ATTR_SOURCE_TYPE: self.source_type} + attr: dict[str, StateType] = {ATTR_SOURCE_TYPE: self.source_type} - if self.battery_level: + if self.battery_level is not None: attr[ATTR_BATTERY_LEVEL] = self.battery_level return attr @@ -64,17 +68,17 @@ class TrackerEntity(BaseTrackerEntity): """Base class for a tracked device.""" @property - def should_poll(self): + def should_poll(self) -> bool: """No polling for entities that have location pushed.""" return False @property - def force_update(self): + def force_update(self) -> bool: """All updates need to be written to the state machine if we're not polling.""" return not self.should_poll @property - def location_accuracy(self): + def location_accuracy(self) -> int: """Return the location accuracy of the device. Value in meters. @@ -97,9 +101,9 @@ class TrackerEntity(BaseTrackerEntity): raise NotImplementedError @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" - if self.location_name: + if self.location_name is not None: return self.location_name if self.latitude is not None and self.longitude is not None: @@ -118,11 +122,11 @@ class TrackerEntity(BaseTrackerEntity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, StateType]: """Return the device state attributes.""" - attr = {} + attr: dict[str, StateType] = {} attr.update(super().state_attributes) - if self.latitude is not None: + if self.latitude is not None and self.longitude is not None: attr[ATTR_LATITUDE] = self.latitude attr[ATTR_LONGITUDE] = self.longitude attr[ATTR_GPS_ACCURACY] = self.location_accuracy @@ -162,9 +166,9 @@ class ScannerEntity(BaseTrackerEntity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, StateType]: """Return the device state attributes.""" - attr = {} + attr: dict[str, StateType] = {} attr.update(super().state_attributes) if self.ip_address is not None: attr[ATTR_IP] = self.ip_address diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index aa1b349ef12..09102372db6 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -1,37 +1,38 @@ """Device tracker constants.""" from datetime import timedelta import logging +from typing import Final -LOGGER = logging.getLogger(__package__) +LOGGER: Final = logging.getLogger(__package__) -DOMAIN = "device_tracker" +DOMAIN: Final = "device_tracker" -PLATFORM_TYPE_LEGACY = "legacy" -PLATFORM_TYPE_ENTITY = "entity_platform" +PLATFORM_TYPE_LEGACY: Final = "legacy" +PLATFORM_TYPE_ENTITY: Final = "entity_platform" -SOURCE_TYPE_GPS = "gps" -SOURCE_TYPE_ROUTER = "router" -SOURCE_TYPE_BLUETOOTH = "bluetooth" -SOURCE_TYPE_BLUETOOTH_LE = "bluetooth_le" +SOURCE_TYPE_GPS: Final = "gps" +SOURCE_TYPE_ROUTER: Final = "router" +SOURCE_TYPE_BLUETOOTH: Final = "bluetooth" +SOURCE_TYPE_BLUETOOTH_LE: Final = "bluetooth_le" -CONF_SCAN_INTERVAL = "interval_seconds" -SCAN_INTERVAL = timedelta(seconds=12) +CONF_SCAN_INTERVAL: Final = "interval_seconds" +SCAN_INTERVAL: Final = timedelta(seconds=12) -CONF_TRACK_NEW = "track_new_devices" -DEFAULT_TRACK_NEW = True +CONF_TRACK_NEW: Final = "track_new_devices" +DEFAULT_TRACK_NEW: Final = True -CONF_CONSIDER_HOME = "consider_home" -DEFAULT_CONSIDER_HOME = timedelta(seconds=180) +CONF_CONSIDER_HOME: Final = "consider_home" +DEFAULT_CONSIDER_HOME: Final = timedelta(seconds=180) -CONF_NEW_DEVICE_DEFAULTS = "new_device_defaults" +CONF_NEW_DEVICE_DEFAULTS: Final = "new_device_defaults" -ATTR_ATTRIBUTES = "attributes" -ATTR_BATTERY = "battery" -ATTR_DEV_ID = "dev_id" -ATTR_GPS = "gps" -ATTR_HOST_NAME = "host_name" -ATTR_LOCATION_NAME = "location_name" -ATTR_MAC = "mac" -ATTR_SOURCE_TYPE = "source_type" -ATTR_CONSIDER_HOME = "consider_home" -ATTR_IP = "ip" +ATTR_ATTRIBUTES: Final = "attributes" +ATTR_BATTERY: Final = "battery" +ATTR_DEV_ID: Final = "dev_id" +ATTR_GPS: Final = "gps" +ATTR_HOST_NAME: Final = "host_name" +ATTR_LOCATION_NAME: Final = "location_name" +ATTR_MAC: Final = "mac" +ATTR_SOURCE_TYPE: Final = "source_type" +ATTR_CONSIDER_HOME: Final = "consider_home" +ATTR_IP: Final = "ip" diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py index 81a16545c74..3c7f9ac35ad 100644 --- a/homeassistant/components/device_tracker/device_trigger.py +++ b/homeassistant/components/device_tracker/device_trigger.py @@ -1,6 +1,8 @@ """Provides device automations for Device Tracker.""" from __future__ import annotations +from typing import Final + import voluptuous as vol from homeassistant.components.automation import AutomationActionType @@ -21,9 +23,9 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -TRIGGER_TYPES = {"enters", "leaves"} +TRIGGER_TYPES: Final[set[str]] = {"enters", "leaves"} -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA: Final = TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), @@ -88,7 +90,9 @@ async def async_attach_trigger( ) -async def async_get_trigger_capabilities(hass: HomeAssistant, config: ConfigType): +async def async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List trigger capabilities.""" zones = { ent.entity_id: ent.name diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index bdef9b83c94..333549e82e0 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Sequence +from collections.abc import Coroutine, Sequence from datetime import timedelta import hashlib from types import ModuleType @@ -28,7 +28,7 @@ from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv @@ -38,7 +38,7 @@ from homeassistant.helpers.event import ( async_track_utc_time_change, ) from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, GPSType +from homeassistant.helpers.typing import ConfigType, GPSType, StateType from homeassistant.setup import async_prepare_setup_platform, async_start_setup from homeassistant.util import dt as dt_util from homeassistant.util.yaml import dump @@ -69,9 +69,9 @@ from .const import ( SOURCE_TYPE_ROUTER, ) -SERVICE_SEE = "see" +SERVICE_SEE: Final = "see" -SOURCE_TYPES = ( +SOURCE_TYPES: Final[tuple[str, ...]] = ( SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, SOURCE_TYPE_BLUETOOTH, @@ -92,9 +92,11 @@ PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA.extend( vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA, } ) -PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) +PLATFORM_SCHEMA_BASE: Final[vol.Schema] = cv.PLATFORM_SCHEMA_BASE.extend( + PLATFORM_SCHEMA.schema +) -SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema( +SERVICE_SEE_PAYLOAD_SCHEMA: Final[vol.Schema] = vol.Schema( vol.All( cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), { @@ -115,23 +117,23 @@ SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema( ) ) -YAML_DEVICES = "known_devices.yaml" -EVENT_NEW_DEVICE = "device_tracker_new_device" +YAML_DEVICES: Final = "known_devices.yaml" +EVENT_NEW_DEVICE: Final = "device_tracker_new_device" def see( hass: HomeAssistant, - mac: str = None, - dev_id: str = None, - host_name: str = None, - location_name: str = None, - gps: GPSType = None, - gps_accuracy=None, - battery: int = None, - attributes: dict = None, -): + mac: str | None = None, + dev_id: str | None = None, + host_name: str | None = None, + location_name: str | None = None, + gps: GPSType | None = None, + gps_accuracy: int | None = None, + battery: int | None = None, + attributes: dict | None = None, +) -> None: """Call service to notify you see device.""" - data = { + data: dict[str, Any] = { key: value for key, value in ( (ATTR_MAC, mac), @@ -144,7 +146,7 @@ def see( ) if value is not None } - if attributes: + if attributes is not None: data[ATTR_ATTRIBUTES] = attributes hass.services.call(DOMAIN, SERVICE_SEE, data) @@ -163,7 +165,9 @@ async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> No if setup_tasks: await asyncio.wait(setup_tasks) - async def async_platform_discovered(p_type, info): + async def async_platform_discovered( + p_type: str, info: dict[str, Any] | None + ) -> None: """Load a platform.""" platform = await async_create_platform_type(hass, config, p_type, {}) @@ -179,7 +183,7 @@ async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> No hass, tracker.async_update_stale, second=range(0, 60, 5) ) - async def async_see_service(call): + async def async_see_service(call: ServiceCall) -> None: """Service to see a device.""" # Temp workaround for iOS, introduced in 0.65 data = dict(call.data) @@ -199,7 +203,7 @@ async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> No class DeviceTrackerPlatform: """Class to hold platform information.""" - LEGACY_SETUP = ( + LEGACY_SETUP: Final[tuple[str, ...]] = ( "async_get_scanner", "get_scanner", "async_setup_scanner", @@ -211,17 +215,22 @@ class DeviceTrackerPlatform: config: dict = attr.ib() @property - def type(self): + def type(self) -> str | None: """Return platform type.""" - for methods, platform_type in ((self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),): - for meth in methods: - if hasattr(self.platform, meth): - return platform_type - + methods, platform_type = self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY + for method in methods: + if hasattr(self.platform, method): + return platform_type return None - async def async_setup_legacy(self, hass, tracker, discovery_info=None): + async def async_setup_legacy( + self, + hass: HomeAssistant, + tracker: DeviceTracker, + discovery_info: dict[str, Any] | None = None, + ) -> None: """Set up a legacy platform.""" + assert self.type == PLATFORM_TYPE_LEGACY full_name = f"{DOMAIN}.{self.name}" LOGGER.info("Setting up %s", full_name) with async_start_setup(hass, [full_name]): @@ -229,20 +238,22 @@ class DeviceTrackerPlatform: scanner = None setup = None if hasattr(self.platform, "async_get_scanner"): - scanner = await self.platform.async_get_scanner( + scanner = await self.platform.async_get_scanner( # type: ignore[attr-defined] hass, {DOMAIN: self.config} ) elif hasattr(self.platform, "get_scanner"): scanner = await hass.async_add_executor_job( - self.platform.get_scanner, hass, {DOMAIN: self.config} + self.platform.get_scanner, # type: ignore[attr-defined] + hass, + {DOMAIN: self.config}, ) elif hasattr(self.platform, "async_setup_scanner"): - setup = await self.platform.async_setup_scanner( + setup = await self.platform.async_setup_scanner( # type: ignore[attr-defined] hass, self.config, tracker.async_see, discovery_info ) elif hasattr(self.platform, "setup_scanner"): setup = await hass.async_add_executor_job( - self.platform.setup_scanner, + self.platform.setup_scanner, # type: ignore[attr-defined] hass, self.config, tracker.see, @@ -251,12 +262,12 @@ class DeviceTrackerPlatform: else: raise HomeAssistantError("Invalid legacy device_tracker platform.") - if scanner: + if scanner is not None: async_setup_scanner_platform( hass, self.config, scanner, tracker.async_see, self.type ) - if not setup and not scanner: + if setup is None and scanner is None: LOGGER.error( "Error setting up platform %s %s", self.type, self.name ) @@ -270,9 +281,11 @@ class DeviceTrackerPlatform: ) -async def async_extract_config(hass, config): +async def async_extract_config( + hass: HomeAssistant, config: ConfigType +) -> list[DeviceTrackerPlatform]: """Extract device tracker config and split between legacy and modern.""" - legacy = [] + legacy: list[DeviceTrackerPlatform] = [] for platform in await asyncio.gather( *( @@ -294,7 +307,7 @@ async def async_extract_config(hass, config): async def async_create_platform_type( - hass, config, p_type, p_config + hass: HomeAssistant, config: ConfigType, p_type: str, p_config: dict ) -> DeviceTrackerPlatform | None: """Determine type of platform.""" platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) @@ -310,9 +323,9 @@ def async_setup_scanner_platform( hass: HomeAssistant, config: ConfigType, scanner: DeviceScanner, - async_see_device: Callable, + async_see_device: Callable[..., Coroutine[None, None, None]], platform: str, -): +) -> None: """Set up the connect scanner-based platform to device tracker. This method must be run in the event loop. @@ -324,7 +337,7 @@ def async_setup_scanner_platform( # Initial scan of each mac we also tell about host name for config seen: Any = set() - async def async_device_tracker_scan(now: dt_util.dt.datetime | None): + async def async_device_tracker_scan(now: dt_util.dt.datetime | None) -> None: """Handle interval matches.""" if update_lock.locked(): LOGGER.warning( @@ -350,7 +363,7 @@ def async_setup_scanner_platform( except NotImplementedError: extra_attributes = {} - kwargs = { + kwargs: dict[str, Any] = { "mac": mac, "host_name": host_name, "source_type": SOURCE_TYPE_ROUTER, @@ -361,7 +374,7 @@ def async_setup_scanner_platform( } zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME) - if zone_home: + if zone_home is not None: kwargs["gps"] = [ zone_home.attributes[ATTR_LATITUDE], zone_home.attributes[ATTR_LONGITUDE], @@ -374,7 +387,7 @@ def async_setup_scanner_platform( hass.async_create_task(async_device_tracker_scan(None)) -async def get_tracker(hass, config): +async def get_tracker(hass: HomeAssistant, config: ConfigType) -> DeviceTracker: """Create a tracker.""" yaml_path = hass.config.path(YAML_DEVICES) @@ -400,12 +413,12 @@ class DeviceTracker: hass: HomeAssistant, consider_home: timedelta, track_new: bool, - defaults: dict, - devices: Sequence, + defaults: dict[str, Any], + devices: Sequence[Device], ) -> None: """Initialize a device tracker.""" self.hass = hass - self.devices = {dev.dev_id: dev for dev in devices} + self.devices: dict[str, Device] = {dev.dev_id: dev for dev in devices} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} self.consider_home = consider_home self.track_new = ( @@ -436,7 +449,7 @@ class DeviceTracker: picture: str | None = None, icon: str | None = None, consider_home: timedelta | None = None, - ): + ) -> None: """Notify the device tracker that you see a device.""" self.hass.create_task( self.async_see( @@ -556,7 +569,7 @@ class DeviceTracker: ) ) - async def async_update_config(self, path, dev_id, device): + async def async_update_config(self, path: str, dev_id: str, device: Device) -> None: """Add device to YAML configuration file. This method is a coroutine. @@ -567,7 +580,7 @@ class DeviceTracker: ) @callback - def async_update_stale(self, now: dt_util.dt.datetime): + def async_update_stale(self, now: dt_util.dt.datetime) -> None: """Update stale devices. This method must be run in the event loop. @@ -576,18 +589,18 @@ class DeviceTracker: if (device.track and device.last_update_home) and device.stale(now): self.hass.async_create_task(device.async_update_ha_state(True)) - async def async_setup_tracked_device(self): + async def async_setup_tracked_device(self) -> None: """Set up all not exists tracked devices. This method is a coroutine. """ - async def async_init_single_device(dev): + async def async_init_single_device(dev: Device) -> None: """Init a single device_tracker entity.""" await dev.async_added_to_hass() dev.async_write_ha_state() - tasks = [] + tasks: list[asyncio.Task] = [] for device in self.devices.values(): if device.track and not device.last_seen: tasks.append( @@ -610,8 +623,8 @@ class Device(RestoreEntity): attributes: dict | None = None # Track if the last update of this device was HOME. - last_update_home = False - _state = STATE_NOT_HOME + last_update_home: bool = False + _state: str = STATE_NOT_HOME def __init__( self, @@ -644,6 +657,7 @@ class Device(RestoreEntity): self.config_name = name # Configured picture + self.config_picture: str | None if gravatar is not None: self.config_picture = get_gravatar_for_email(gravatar) else: @@ -656,32 +670,32 @@ class Device(RestoreEntity): self._attributes: dict[str, Any] = {} @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self.config_name or self.host_name or self.dev_id or DEVICE_DEFAULT_NAME @property - def state(self): + def state(self) -> str: """Return the state of the device.""" return self._state @property - def entity_picture(self): + def entity_picture(self) -> str | None: """Return the picture of the device.""" return self.config_picture @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, StateType]: """Return the device state attributes.""" - attributes = {ATTR_SOURCE_TYPE: self.source_type} + attributes: dict[str, StateType] = {ATTR_SOURCE_TYPE: self.source_type} - if self.gps: + if self.gps is not None: attributes[ATTR_LATITUDE] = self.gps[0] attributes[ATTR_LONGITUDE] = self.gps[1] attributes[ATTR_GPS_ACCURACY] = self.gps_accuracy - if self.battery: + if self.battery is not None: attributes[ATTR_BATTERY] = self.battery return attributes @@ -742,13 +756,13 @@ class Device(RestoreEntity): or (now or dt_util.utcnow()) - self.last_seen > self.consider_home ) - def mark_stale(self): + def mark_stale(self) -> None: """Mark the device state as stale.""" self._state = STATE_NOT_HOME self.gps = None self.last_update_home = False - async def async_update(self): + async def async_update(self) -> None: """Update state of entity. This method is a coroutine. @@ -773,7 +787,7 @@ class Device(RestoreEntity): self._state = STATE_HOME self.last_update_home = True - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add an entity.""" await super().async_added_to_hass() state = await self.async_get_last_state() @@ -807,7 +821,7 @@ class DeviceScanner: """Scan for devices.""" raise NotImplementedError() - async def async_scan_devices(self) -> Any: + async def async_scan_devices(self) -> list[str]: """Scan for devices.""" assert ( self.hass is not None @@ -829,7 +843,7 @@ class DeviceScanner: """Get the extra attributes of a device.""" raise NotImplementedError() - async def async_get_extra_attributes(self, device: str) -> Any: + async def async_get_extra_attributes(self, device: str) -> dict: """Get the extra attributes of a device.""" assert ( self.hass is not None @@ -837,7 +851,9 @@ class DeviceScanner: return await self.hass.async_add_executor_job(self.get_extra_attributes, device) -async def async_load_config(path: str, hass: HomeAssistant, consider_home: timedelta): +async def async_load_config( + path: str, hass: HomeAssistant, consider_home: timedelta +) -> list[Device]: """Load devices from YAML configuration file. This method is a coroutine. @@ -857,7 +873,7 @@ async def async_load_config(path: str, hass: HomeAssistant, consider_home: timed ), } ) - result = [] + result: list[Device] = [] try: devices = await hass.async_add_executor_job(load_yaml_config_file, path) except HomeAssistantError as err: @@ -880,7 +896,7 @@ async def async_load_config(path: str, hass: HomeAssistant, consider_home: timed return result -def update_config(path: str, dev_id: str, device: Device): +def update_config(path: str, dev_id: str, device: Device) -> None: """Add device to YAML configuration file.""" with open(path, "a") as out: device_config = { @@ -896,7 +912,7 @@ def update_config(path: str, dev_id: str, device: Device): out.write(dump(device_config)) -def get_gravatar_for_email(email: str): +def get_gravatar_for_email(email: str) -> str: """Return an 80px Gravatar for the given email address. Async friendly. diff --git a/mypy.ini b/mypy.ini index ae8d4d7fa63..452a07afbb9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -242,6 +242,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.device_tracker.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.elgato.*] check_untyped_defs = true disallow_incomplete_defs = true From 016abda12e10950c8fdfe536a1dd90d8ff84ce4b Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Sat, 22 May 2021 09:15:30 +0100 Subject: [PATCH 636/852] Pylint plugin to check that relative imports are used (#50937) * Pylint plugin to check that relative imports are used * Fix existing sites * Update description message * Fix typo --- homeassistant/auth/__init__.py | 3 +- homeassistant/components/adguard/__init__.py | 23 ++++---- .../components/azure_devops/__init__.py | 9 +--- homeassistant/components/blink/__init__.py | 13 ++--- .../components/color_extractor/__init__.py | 8 +-- homeassistant/components/somfy/__init__.py | 3 +- homeassistant/components/spotify/__init__.py | 2 +- .../components/twentemilieu/__init__.py | 15 +++--- pylint/plugins/hass_constructor.py | 4 +- pylint/plugins/hass_imports.py | 52 +++++++++++++++++++ pyproject.toml | 1 + 11 files changed, 89 insertions(+), 44 deletions(-) create mode 100644 pylint/plugins/hass_imports.py diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 931c2f4c11a..519582ea48c 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -9,13 +9,12 @@ from typing import Any, Dict, Mapping, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow -from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.util import dt as dt_util from . import auth_store, models -from .const import GROUP_ID_ADMIN +from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config from .providers import AuthProvider, LoginFlow, auth_provider_from_config diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 0a4a79b65f5..8f0c73a8a64 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -6,17 +6,6 @@ import logging from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError import voluptuous as vol -from homeassistant.components.adguard.const import ( - CONF_FORCE, - DATA_ADGUARD_CLIENT, - DATA_ADGUARD_VERSION, - DOMAIN, - SERVICE_ADD_URL, - SERVICE_DISABLE_URL, - SERVICE_ENABLE_URL, - SERVICE_REFRESH, - SERVICE_REMOVE_URL, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -34,6 +23,18 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo, Entity +from .const import ( + CONF_FORCE, + DATA_ADGUARD_CLIENT, + DATA_ADGUARD_VERSION, + DOMAIN, + SERVICE_ADD_URL, + SERVICE_DISABLE_URL, + SERVICE_ENABLE_URL, + SERVICE_REFRESH, + SERVICE_REMOVE_URL, +) + _LOGGER = logging.getLogger(__name__) SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): cv.url}) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index ba9020e3e88..5e3971adcc4 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -6,18 +6,13 @@ import logging from aioazuredevops.client import DevOpsClient import aiohttp -from homeassistant.components.azure_devops.const import ( - CONF_ORG, - CONF_PAT, - CONF_PROJECT, - DATA_AZURE_DEVOPS_CLIENT, - DOMAIN, -) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo, Entity +from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DATA_AZURE_DEVOPS_CLIENT, DOMAIN + _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index ce47fcf7908..5845c61c330 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -7,7 +7,13 @@ from blinkpy.blinkpy import Blink import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.components.blink.const import ( +from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.const import CONF_FILENAME, CONF_NAME, CONF_PIN, CONF_SCAN_INTERVAL +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv + +from .const import ( DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS, @@ -15,11 +21,6 @@ from homeassistant.components.blink.const import ( SERVICE_SAVE_VIDEO, SERVICE_SEND_PIN, ) -from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import CONF_FILENAME, CONF_NAME, CONF_PIN, CONF_SCAN_INTERVAL -from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index ddd2ae967e4..b0ab5c2aba7 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -9,12 +9,6 @@ import async_timeout from colorthief import ColorThief import voluptuous as vol -from homeassistant.components.color_extractor.const import ( - ATTR_PATH, - ATTR_URL, - DOMAIN, - SERVICE_TURN_ON, -) from homeassistant.components.light import ( ATTR_RGB_COLOR, DOMAIN as LIGHT_DOMAIN, @@ -24,6 +18,8 @@ from homeassistant.components.light import ( from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv +from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON + _LOGGER = logging.getLogger(__name__) # Extend the existing light.turn_on service schema diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index e5c3015d2fa..f159b19c92a 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -6,7 +6,6 @@ import logging from pymfy.api.devices.category import Category import voluptuous as vol -from homeassistant.components.somfy import config_flow from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_OPTIMISTIC from homeassistant.core import HomeAssistant, callback @@ -21,7 +20,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from . import api +from . import api, config_flow from .const import API, COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 3aab37c9392..8af58edd4b4 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -5,7 +5,6 @@ from spotipy import Spotify, SpotifyException import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.components.spotify import config_flow from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CREDENTIALS, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant @@ -17,6 +16,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from homeassistant.helpers.typing import ConfigType +from . import config_flow from .const import ( DATA_SPOTIFY_CLIENT, DATA_SPOTIFY_ME, diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index 94495cb83ce..81e0a040333 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -7,13 +7,6 @@ from datetime import timedelta from twentemilieu import TwenteMilieu import voluptuous as vol -from homeassistant.components.twentemilieu.const import ( - CONF_HOUSE_LETTER, - CONF_HOUSE_NUMBER, - CONF_POST_CODE, - DATA_UPDATE, - DOMAIN, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant @@ -23,6 +16,14 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType +from .const import ( + CONF_HOUSE_LETTER, + CONF_HOUSE_NUMBER, + CONF_POST_CODE, + DATA_UPDATE, + DOMAIN, +) + SCAN_INTERVAL = timedelta(seconds=3600) SERVICE_UPDATE = "update" diff --git a/pylint/plugins/hass_constructor.py b/pylint/plugins/hass_constructor.py index 3a3012b9f69..f0f23ef4c95 100644 --- a/pylint/plugins/hass_constructor.py +++ b/pylint/plugins/hass_constructor.py @@ -1,5 +1,5 @@ """Plugin for constructor definitions.""" -from astroid import ClassDef, Const, FunctionDef +from astroid import Const, FunctionDef from pylint.checkers import BaseChecker from pylint.interfaces import IAstroidChecker from pylint.lint import PyLinter @@ -43,7 +43,7 @@ class HassConstructorFormatChecker(BaseChecker): # type: ignore[misc] return # Check that return type is specified and it is "None". - if not isinstance(node.returns, Const) or node.returns.value != None: + if not isinstance(node.returns, Const) or node.returns.value is not None: self.add_message("hass-constructor-return", node=node) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py new file mode 100644 index 00000000000..341abff202a --- /dev/null +++ b/pylint/plugins/hass_imports.py @@ -0,0 +1,52 @@ +"""Plugin for checking imports.""" +from __future__ import annotations + +from astroid import Import, ImportFrom, Module +from pylint.checkers import BaseChecker +from pylint.interfaces import IAstroidChecker +from pylint.lint import PyLinter + + +class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] + """Checker for imports.""" + + __implements__ = IAstroidChecker + + name = "hass_imports" + priority = -1 + msgs = { + "W0011": ( + "Relative import should be used", + "hass-relative-import", + "Used when absolute import should be replaced with relative import", + ), + } + options = () + + def __init__(self, linter: PyLinter | None = None) -> None: + super().__init__(linter) + self.current_module: str | None = None + + def visit_module(self, node: Module) -> None: + """Called when a Import node is visited.""" + self.current_module = node.name + + def visit_import(self, node: Import) -> None: + """Called when a Import node is visited.""" + for module, _alias in node.names: + if module.startswith(f"{self.current_module}."): + self.add_message("hass-relative-import", node=node) + + def visit_importfrom(self, node: ImportFrom) -> None: + """Called when a ImportFrom node is visited.""" + if node.level is not None: + return + if node.modname == self.current_module or node.modname.startswith( + f"{self.current_module}." + ): + self.add_message("hass-relative-import", node=node) + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(HassImportsFormatChecker(linter)) diff --git a/pyproject.toml b/pyproject.toml index 33af823fae4..f8d47624c8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ load-plugins = [ "pylint.extensions.typing", "pylint_strict_informational", "hass_constructor", + "hass_imports", "hass_logger", ] persistent = false From 59ae78e5f01a47a25fd5f7571dee1cab8375af78 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 22 May 2021 13:38:05 +0200 Subject: [PATCH 637/852] Add restore_state to modbus binary_sensor (#50922) * Add restore_state to binary_sensor. * Update return value in State. --- .../components/modbus/binary_sensor.py | 9 ++++++- ...binary_sensor.py => test_binary_sensor.py} | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) rename tests/components/modbus/{test_modbus_binary_sensor.py => test_binary_sensor.py} (81%) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 045447f7246..c27fde6d946 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -17,9 +17,11 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, + STATE_ON, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .base_platform import BasePlatform @@ -96,12 +98,17 @@ async def async_setup_platform( async_add_entities(sensors) -class ModbusBinarySensor(BasePlatform, BinarySensorEntity): +class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): """Modbus binary sensor.""" async def async_added_to_hass(self): """Handle entity which will be added.""" await self.async_base_added_to_hass() + state = await self.async_get_last_state() + if state: + self._value = state.state == STATE_ON + else: + self._value = None @property def is_on(self): diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py similarity index 81% rename from tests/components/modbus/test_modbus_binary_sensor.py rename to tests/components/modbus/test_binary_sensor.py index 27821c170e1..5089d0271dd 100644 --- a/tests/components/modbus/test_modbus_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -18,9 +18,12 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.core import State from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from tests.common import mock_restore_cache + @pytest.mark.parametrize("do_discovery", [False, True]) @pytest.mark.parametrize( @@ -130,3 +133,26 @@ async def test_service_binary_sensor_update(hass, mock_pymodbus): "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == STATE_ON + + +async def test_restore_state_binary_sensor(hass): + """Run test for binary sensor restore state.""" + + sensor_name = "test_binary_sensor" + test_value = STATE_ON + config_sensor = {CONF_NAME: sensor_name, CONF_ADDRESS: 17} + mock_restore_cache( + hass, + (State(f"{SENSOR_DOMAIN}.{sensor_name}", test_value),), + ) + await base_config_test( + hass, + config_sensor, + sensor_name, + SENSOR_DOMAIN, + CONF_BINARY_SENSORS, + None, + method_discovery=True, + ) + entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" + assert hass.states.get(entity_id).state == test_value From afb372a680223a692118616b231dcfbfaeabaef1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 22 May 2021 14:00:53 +0200 Subject: [PATCH 638/852] Add Final type for constants in sensor component (#50955) --- homeassistant/components/sensor/__init__.py | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 75fea9e044b..a0532a65cf3 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from datetime import datetime, timedelta import logging -from typing import Any, cast, final +from typing import Any, Final, cast, final import voluptuous as vol @@ -34,17 +34,17 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -ATTR_LAST_RESET = "last_reset" -ATTR_STATE_CLASS = "state_class" +ATTR_LAST_RESET: Final = "last_reset" +ATTR_STATE_CLASS: Final = "state_class" -DOMAIN = "sensor" +DOMAIN: Final = "sensor" -ENTITY_ID_FORMAT = DOMAIN + ".{}" +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" -SCAN_INTERVAL = timedelta(seconds=30) -DEVICE_CLASSES = [ +SCAN_INTERVAL: Final = timedelta(seconds=30) +DEVICE_CLASSES: Final[list[str]] = [ DEVICE_CLASS_BATTERY, # % of battery that is left DEVICE_CLASS_CO, # ppm (parts per million) Carbon Monoxide gas concentration DEVICE_CLASS_CO2, # ppm (parts per million) Carbon Dioxide gas concentration @@ -61,14 +61,14 @@ DEVICE_CLASSES = [ DEVICE_CLASS_VOLTAGE, # voltage (V) ] -DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) +DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) # The state represents a measurement in present time -STATE_CLASS_MEASUREMENT = "measurement" +STATE_CLASS_MEASUREMENT: Final = "measurement" -STATE_CLASSES = [STATE_CLASS_MEASUREMENT] +STATE_CLASSES: Final[list[str]] = [STATE_CLASS_MEASUREMENT] -STATE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(STATE_CLASSES)) +STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(STATE_CLASSES)) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: From 560dd0a0cccebb45dc23a026ca438e4e250b99c3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 14:47:26 +0200 Subject: [PATCH 639/852] Typing improvements for TPLink (#50947) * Typing improvements for TPLink * Update homeassistant/components/tplink/common.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/tplink/__init__.py | 16 ++++-- homeassistant/components/tplink/common.py | 20 ++++---- homeassistant/components/tplink/light.py | 57 ++++++++++++--------- homeassistant/components/tplink/switch.py | 35 ++++++++----- 4 files changed, 78 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index f424f90d6d3..69241f1cb44 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -4,6 +4,7 @@ import logging import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -54,7 +55,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the TP-Link component.""" conf = config.get(DOMAIN) @@ -71,7 +72,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigType): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up TPLink from a config entry.""" config_data = hass.data[DOMAIN].get(ATTR_CONFIG) @@ -97,19 +98,24 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigType): forward_setup = hass.config_entries.async_forward_entry_setup if lights: _LOGGER.debug( - "Got %s lights: %s", len(lights), ", ".join([d.host for d in lights]) + "Got %s lights: %s", len(lights), ", ".join(d.host for d in lights) ) + hass.async_create_task(forward_setup(config_entry, "light")) + if switches: _LOGGER.debug( - "Got %s switches: %s", len(switches), ", ".join([d.host for d in switches]) + "Got %s switches: %s", + len(switches), + ", ".join(d.host for d in switches), ) + hass.async_create_task(forward_setup(config_entry, "switch")) return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" platforms = [platform for platform in PLATFORMS if hass.data[DOMAIN].get(platform)] unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index f9ed57d26fb..8b1ee4a44b1 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Callable from pyHS100 import ( Discover, @@ -38,16 +39,16 @@ class SmartDevices: self._switches = switches or [] @property - def lights(self): + def lights(self) -> list[SmartDevice]: """Get the lights.""" return self._lights @property - def switches(self): + def switches(self) -> list[SmartDevice]: """Get the switches.""" return self._switches - def has_device_with_host(self, host): + def has_device_with_host(self, host: str) -> bool: """Check if a devices exists with a specific host.""" for device in self.lights + self.switches: if device.host == host: @@ -56,12 +57,11 @@ class SmartDevices: return False -async def async_get_discoverable_devices(hass): +async def async_get_discoverable_devices(hass: HomeAssistant) -> dict[str, SmartDevice]: """Return if there are devices that can be discovered.""" - def discover(): - devs = Discover.discover() - return devs + def discover() -> dict[str, SmartDevice]: + return Discover.discover() return await hass.async_add_executor_job(discover) @@ -77,7 +77,7 @@ async def async_discover_devices( lights = [] switches = [] - def process_devices(): + def process_devices() -> None: for dev in devices.values(): # If this device already exists, ignore dynamic setup. if existing_devices.has_device_with_host(dev.host): @@ -132,7 +132,9 @@ def get_static_devices(config_data) -> SmartDevices: return SmartDevices(lights, switches) -def add_available_devices(hass, device_type, device_class): +def add_available_devices( + hass: HomeAssistant, device_type: str, device_class: Callable +) -> list: """Get sysinfo for all devices.""" devices = hass.data[TPLINK_DOMAIN][device_type] diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 5984698a796..e5217cbc143 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping from datetime import timedelta import logging import re @@ -19,9 +20,12 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, LightEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, PlatformNotReady import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, color_temperature_mired_to_kelvin as mired_to_kelvin, @@ -78,7 +82,11 @@ FALLBACK_MIN_COLOR = 2700 FALLBACK_MAX_COLOR = 5000 -async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up lights.""" entities = await hass.async_add_executor_job( add_available_devices, hass, CONF_LIGHT, TPLinkSmartBulb @@ -111,10 +119,9 @@ class LightState(NamedTuple): def to_param(self): """Return a version that we can send to the bulb.""" + color_temp = None if self.color_temp: color_temp = mired_to_kelvin(self.color_temp) - else: - color_temp = None return { LIGHT_STATE_ON_OFF: 1 if self.state else 0, @@ -157,17 +164,17 @@ class TPLinkSmartBulb(LightEntity): self._alias = None @property - def unique_id(self): + def unique_id(self) -> str | None: """Return a unique ID.""" return self._light_features.mac @property - def name(self): + def name(self) -> str | None: """Return the name of the Smart Bulb.""" return self._light_features.alias @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return information about the device.""" return { "name": self._light_features.alias, @@ -183,11 +190,11 @@ class TPLinkSmartBulb(LightEntity): return self._is_available @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes of the device.""" return self._emeter_params - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) @@ -220,7 +227,7 @@ class TPLinkSmartBulb(LightEntity): ), ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._async_set_light_state_retry( self._light_state, @@ -228,36 +235,36 @@ class TPLinkSmartBulb(LightEntity): ) @property - def min_mireds(self): + def min_mireds(self) -> int: """Return minimum supported color temperature.""" return self._light_features.min_mireds @property - def max_mireds(self): + def max_mireds(self) -> int: """Return maximum supported color temperature.""" return self._light_features.max_mireds @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the color temperature of this light in mireds for HA.""" return self._light_state.color_temp @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" return self._light_state.brightness @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the color.""" return self._light_state.hs @property - def is_on(self): + def is_on(self) -> bool: """Return True if device is on.""" return self._light_state.state - def attempt_update(self, update_attempt): + def attempt_update(self, update_attempt: int) -> bool: """Attempt to get details the TP-Link bulb.""" # State is currently being set, ignore. if self._is_setting_light_state: @@ -283,11 +290,11 @@ class TPLinkSmartBulb(LightEntity): return False @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return self._light_features.supported_features - def _get_valid_temperature_range(self): + def _get_valid_temperature_range(self) -> tuple[int, int]: """Return the device-specific white temperature range (in Kelvin). :return: White temperature range in Kelvin (minimum, maximum) @@ -300,7 +307,7 @@ class TPLinkSmartBulb(LightEntity): # use "safe" values for something that advertises color temperature return FALLBACK_MIN_COLOR, FALLBACK_MAX_COLOR - def _get_light_features(self): + def _get_light_features(self) -> LightFeatures: """Determine all supported features in one go.""" sysinfo = self.smartbulb.sys_info supported_features = 0 @@ -333,7 +340,7 @@ class TPLinkSmartBulb(LightEntity): has_emeter=has_emeter, ) - def _light_state_from_params(self, light_state_params) -> LightState: + def _light_state_from_params(self, light_state_params: Any) -> LightState: brightness = None color_temp = None hue_saturation = None @@ -374,7 +381,7 @@ class TPLinkSmartBulb(LightEntity): self._update_emeter() return self._light_state_from_params(self._get_device_state()) - def _update_emeter(self): + def _update_emeter(self) -> None: if not self._light_features.has_emeter: return @@ -456,7 +463,7 @@ class TPLinkSmartBulb(LightEntity): return self._set_device_state(diff) - def _get_device_state(self): + def _get_device_state(self) -> dict: """State of the bulb or smart dimmer switch.""" if isinstance(self.smartbulb, SmartBulb): return self.smartbulb.get_light_state() @@ -493,7 +500,7 @@ class TPLinkSmartBulb(LightEntity): return self._get_device_state() - async def async_update(self): + async def async_update(self) -> None: """Update the TP-Link bulb's state.""" for update_attempt in range(MAX_ATTEMPTS): is_ready = await self.hass.async_add_executor_job( @@ -521,7 +528,9 @@ class TPLinkSmartBulb(LightEntity): self._is_available = False -def _light_state_diff(old_light_state: LightState, new_light_state: LightState): +def _light_state_diff( + old_light_state: LightState, new_light_state: LightState +) -> dict[str, Any]: old_state_param = old_light_state.to_param() new_state_param = new_light_state.to_param() diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 011caa463b2..d088584c4ad 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -1,8 +1,12 @@ """Support for TPLink HS100/HS110/HS200 smart switch.""" +from __future__ import annotations + import asyncio +from collections.abc import Mapping from contextlib import suppress import logging import time +from typing import Any from pyHS100 import SmartDeviceException, SmartPlug @@ -11,10 +15,13 @@ from homeassistant.components.switch import ( ATTR_TODAY_ENERGY_KWH, SwitchEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_VOLTAGE from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import CONF_SWITCH, DOMAIN as TPLINK_DOMAIN from .common import add_available_devices @@ -30,7 +37,11 @@ MAX_ATTEMPTS = 300 SLEEP_TIME = 2 -async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up switches.""" entities = await hass.async_add_executor_job( add_available_devices, hass, CONF_SWITCH, SmartPlugSwitch @@ -62,17 +73,17 @@ class SmartPlugSwitch(SwitchEntity): self._host = None @property - def unique_id(self): + def unique_id(self) -> str | None: """Return a unique ID.""" return self._device_id @property - def name(self): + def name(self) -> str | None: """Return the name of the Smart Plug.""" return self._alias @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return information about the device.""" return { "name": self._alias, @@ -88,37 +99,37 @@ class SmartPlugSwitch(SwitchEntity): return self._is_available @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if switch is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self.smartplug.turn_on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self.smartplug.turn_off() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes of the device.""" return self._emeter_params @property - def _plug_from_context(self): + def _plug_from_context(self) -> Any: """Return the plug from the context.""" children = self.smartplug.sys_info["children"] return next(c for c in children if c["id"] == self.smartplug.context) - def update_state(self): + def update_state(self) -> None: """Update the TP-Link switch's state.""" if self.smartplug.context is None: self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON else: self._state = self._plug_from_context["state"] == 1 - def attempt_update(self, update_attempt): + def attempt_update(self, update_attempt: int) -> bool: """Attempt to get details from the TP-Link switch.""" try: if not self._sysinfo: @@ -168,7 +179,7 @@ class SmartPlugSwitch(SwitchEntity): ) return False - async def async_update(self): + async def async_update(self) -> None: """Update the TP-Link switch's state.""" for update_attempt in range(MAX_ATTEMPTS): is_ready = await self.hass.async_add_executor_job( From 4a64f7a6968289787af6730769d4e704afa78fe2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 22 May 2021 16:45:18 +0200 Subject: [PATCH 640/852] Add strict type annotations to tcp (#50877) * add strict type annotations * apply suggestions * rename to TCP_PLATFORM_SCHEMA * Replace DiscoveryInfoType --- .strict-typing | 1 + homeassistant/components/tcp/binary_sensor.py | 23 +++-- homeassistant/components/tcp/const.py | 13 +++ homeassistant/components/tcp/model.py | 22 +++++ homeassistant/components/tcp/sensor.py | 87 +++++++++++-------- mypy.ini | 14 ++- script/hassfest/mypy_config.py | 1 - 7 files changed, 115 insertions(+), 46 deletions(-) create mode 100644 homeassistant/components/tcp/const.py create mode 100644 homeassistant/components/tcp/model.py diff --git a/.strict-typing b/.strict-typing index a6a8a4c3e22..9e02dad19d2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -59,6 +59,7 @@ homeassistant.components.sun.* homeassistant.components.switch.* homeassistant.components.synology_dsm.* homeassistant.components.systemmonitor.* +homeassistant.components.tcp.* homeassistant.components.tts.* homeassistant.components.upcloud.* homeassistant.components.vacuum.* diff --git a/homeassistant/components/tcp/binary_sensor.py b/homeassistant/components/tcp/binary_sensor.py index 5437cef02de..c0e53fba334 100644 --- a/homeassistant/components/tcp/binary_sensor.py +++ b/homeassistant/components/tcp/binary_sensor.py @@ -1,12 +1,25 @@ """Provides a binary sensor which gets its values from a TCP socket.""" +from __future__ import annotations + +from typing import Any, Final + from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType -from .sensor import CONF_VALUE_ON, PLATFORM_SCHEMA, TcpSensor +from .const import CONF_VALUE_ON +from .sensor import PLATFORM_SCHEMA as TCP_PLATFORM_SCHEMA, TcpSensor -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}) +PLATFORM_SCHEMA: Final = TCP_PLATFORM_SCHEMA -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, +) -> None: """Set up the TCP binary sensor.""" add_entities([TcpBinarySensor(hass, config)]) @@ -14,9 +27,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class TcpBinarySensor(BinarySensorEntity, TcpSensor): """A binary sensor which is on when its state == CONF_VALUE_ON.""" - required = (CONF_VALUE_ON,) - @property - def is_on(self): + def is_on(self) -> bool: """Return true if the binary sensor is on.""" return self._state == self._config[CONF_VALUE_ON] diff --git a/homeassistant/components/tcp/const.py b/homeassistant/components/tcp/const.py new file mode 100644 index 00000000000..3a42736c753 --- /dev/null +++ b/homeassistant/components/tcp/const.py @@ -0,0 +1,13 @@ +"""Constants for TCP platform.""" +from __future__ import annotations + +from typing import Final + +CONF_BUFFER_SIZE: Final = "buffer_size" +CONF_VALUE_ON: Final = "value_on" + +DEFAULT_BUFFER_SIZE: Final = 1024 +DEFAULT_NAME: Final = "TCP Sensor" +DEFAULT_TIMEOUT: Final = 10 +DEFAULT_SSL: Final = False +DEFAULT_VERIFY_SSL: Final = True diff --git a/homeassistant/components/tcp/model.py b/homeassistant/components/tcp/model.py new file mode 100644 index 00000000000..814f6fdb126 --- /dev/null +++ b/homeassistant/components/tcp/model.py @@ -0,0 +1,22 @@ +"""Models for TCP platform.""" +from __future__ import annotations + +from typing import TypedDict + +from homeassistant.helpers.template import Template + + +class TcpSensorConfig(TypedDict): + """TypedDict for TcpSensor config.""" + + name: str + host: str + port: str + timeout: int + payload: str + unit_of_measurement: str | None + value_template: Template | None + value_on: str | None + buffer_size: int + ssl: bool + verify_ssl: bool diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index ff436f8ecaf..84f92582947 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -1,12 +1,18 @@ """Support for TCP socket based sensors.""" +from __future__ import annotations + import logging import select import socket import ssl +from typing import Any, Final import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -18,21 +24,27 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) +from .const import ( + CONF_BUFFER_SIZE, + CONF_VALUE_ON, + DEFAULT_BUFFER_SIZE, + DEFAULT_NAME, + DEFAULT_SSL, + DEFAULT_TIMEOUT, + DEFAULT_VERIFY_SSL, +) +from .model import TcpSensorConfig -CONF_BUFFER_SIZE = "buffer_size" -CONF_VALUE_ON = "value_on" +_LOGGER: Final = logging.getLogger(__name__) -DEFAULT_BUFFER_SIZE = 1024 -DEFAULT_NAME = "TCP Sensor" -DEFAULT_TIMEOUT = 10 -DEFAULT_SSL = False -DEFAULT_VERIFY_SSL = True - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, @@ -49,7 +61,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, +) -> None: """Set up the TCP Sensor.""" add_entities([TcpSensor(hass, config)]) @@ -57,55 +74,54 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class TcpSensor(SensorEntity): """Implementation of a TCP socket based sensor.""" - required = () - - def __init__(self, hass, config): + def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: """Set all the config values if they exist and get initial state.""" - value_template = config.get(CONF_VALUE_TEMPLATE) + value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass self._hass = hass - self._config = { - CONF_NAME: config.get(CONF_NAME), - CONF_HOST: config.get(CONF_HOST), - CONF_PORT: config.get(CONF_PORT), - CONF_TIMEOUT: config.get(CONF_TIMEOUT), - CONF_PAYLOAD: config.get(CONF_PAYLOAD), + self._config: TcpSensorConfig = { + CONF_NAME: config[CONF_NAME], + CONF_HOST: config[CONF_HOST], + CONF_PORT: config[CONF_PORT], + CONF_TIMEOUT: config[CONF_TIMEOUT], + CONF_PAYLOAD: config[CONF_PAYLOAD], CONF_UNIT_OF_MEASUREMENT: config.get(CONF_UNIT_OF_MEASUREMENT), CONF_VALUE_TEMPLATE: value_template, CONF_VALUE_ON: config.get(CONF_VALUE_ON), - CONF_BUFFER_SIZE: config.get(CONF_BUFFER_SIZE), + CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE], + CONF_SSL: config[CONF_SSL], + CONF_VERIFY_SSL: config[CONF_VERIFY_SSL], } - if config[CONF_SSL]: + self._ssl_context: ssl.SSLContext | None = None + if self._config[CONF_SSL]: self._ssl_context = ssl.create_default_context() - if not config[CONF_VERIFY_SSL]: + if not self._config[CONF_VERIFY_SSL]: self._ssl_context.check_hostname = False self._ssl_context.verify_mode = ssl.CERT_NONE - else: - self._ssl_context = None - self._state = None + self._state: str | None = None self.update() @property - def name(self): + def name(self) -> str: """Return the name of this sensor.""" return self._config[CONF_NAME] @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" return self._config[CONF_UNIT_OF_MEASUREMENT] - def update(self): + def update(self) -> None: """Get the latest value for this sensor.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(self._config[CONF_TIMEOUT]) @@ -151,11 +167,10 @@ class TcpSensor(SensorEntity): value = sock.recv(self._config[CONF_BUFFER_SIZE]).decode() - if self._config[CONF_VALUE_TEMPLATE] is not None: + value_template = self._config[CONF_VALUE_TEMPLATE] + if value_template is not None: try: - self._state = self._config[CONF_VALUE_TEMPLATE].render( - parse_result=False, value=value - ) + self._state = value_template.render(parse_result=False, value=value) return except TemplateError: _LOGGER.error( diff --git a/mypy.ini b/mypy.ini index 452a07afbb9..aea1073d4f2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -660,6 +660,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.tcp.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.tts.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1331,9 +1342,6 @@ ignore_errors = true [mypy-homeassistant.components.tasmota.*] ignore_errors = true -[mypy-homeassistant.components.tcp.*] -ignore_errors = true - [mypy-homeassistant.components.telegram_bot.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 743c17088c6..820ab7b814a 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -201,7 +201,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.system_log.*", "homeassistant.components.tado.*", "homeassistant.components.tasmota.*", - "homeassistant.components.tcp.*", "homeassistant.components.telegram_bot.*", "homeassistant.components.template.*", "homeassistant.components.tesla.*", From 9f04c7ea237849a42c17aff8c965d1be882c80bf Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Sat, 22 May 2021 16:54:47 +0200 Subject: [PATCH 641/852] Add Openweathermap cloud coverage forecast (#50961) --- homeassistant/components/openweathermap/const.py | 2 ++ .../components/openweathermap/weather_update_coordinator.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 36080a8e6f6..c1ca96188d8 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -111,6 +111,7 @@ FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + ATTR_API_CLOUDS, ] LANGUAGES = [ "af", @@ -270,4 +271,5 @@ FORECAST_SENSOR_TYPES = { SENSOR_NAME: "Wind speed", SENSOR_UNIT: SPEED_METERS_PER_SECOND, }, + ATTR_API_CLOUDS: {SENSOR_NAME: "Cloud coverage", SENSOR_UNIT: PERCENTAGE}, } diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 20cc71da725..4518e3b6bda 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -167,6 +167,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_CONDITION: self._get_condition( entry.weather_code, entry.reference_time("unix") ), + ATTR_API_CLOUDS: entry.clouds, } temperature_dict = entry.temperature("celsius") From aa9b99713c70935c8c0b2cda436b8eb5d214d984 Mon Sep 17 00:00:00 2001 From: PeteBa Date: Sat, 22 May 2021 16:30:05 +0100 Subject: [PATCH 642/852] Add purge_entities service call to recorder (#48069) --- homeassistant/components/recorder/__init__.py | 58 ++++++++- homeassistant/components/recorder/purge.py | 21 +++- .../components/recorder/services.yaml | 27 +++- tests/components/recorder/test_init.py | 2 + tests/components/recorder/test_purge.py | 116 ++++++++++++++++++ 5 files changed, 219 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 91f29225cd2..0d6dddfa2d5 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -34,6 +34,7 @@ from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER, convert_include_exclude_filter, + generate_filter, ) from homeassistant.helpers.event import ( async_track_time_change, @@ -42,6 +43,7 @@ from homeassistant.helpers.event import ( from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) +from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util @@ -63,6 +65,7 @@ from .util import ( _LOGGER = logging.getLogger(__name__) SERVICE_PURGE = "purge" +SERVICE_PURGE_ENTITIES = "purge_entities" SERVICE_ENABLE = "enable" SERVICE_DISABLE = "disable" @@ -79,6 +82,18 @@ SERVICE_PURGE_SCHEMA = vol.Schema( vol.Optional(ATTR_APPLY_FILTER, default=False): cv.boolean, } ) + +ATTR_DOMAINS = "domains" +ATTR_ENTITY_GLOBS = "entity_globs" + +SERVICE_PURGE_ENTITIES_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_GLOBS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } +).extend(cv.ENTITY_SERVICE_FIELDS) SERVICE_ENABLE_SCHEMA = vol.Schema({}) SERVICE_DISABLE_SCHEMA = vol.Schema({}) @@ -252,11 +267,29 @@ def _async_register_services(hass, instance): DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA ) - async def async_handle_enable_sevice(service): + async def async_handle_purge_entities_service(service): + """Handle calls to the purge entities service.""" + entity_ids = await async_extract_entity_ids(hass, service) + domains = service.data.get(ATTR_DOMAINS, []) + entity_globs = service.data.get(ATTR_ENTITY_GLOBS, []) + + instance.do_adhoc_purge_entities(entity_ids, domains, entity_globs) + + hass.services.async_register( + DOMAIN, + SERVICE_PURGE_ENTITIES, + async_handle_purge_entities_service, + schema=SERVICE_PURGE_ENTITIES_SCHEMA, + ) + + async def async_handle_enable_service(service): instance.set_enable(True) hass.services.async_register( - DOMAIN, SERVICE_ENABLE, async_handle_enable_sevice, schema=SERVICE_ENABLE_SCHEMA + DOMAIN, + SERVICE_ENABLE, + async_handle_enable_service, + schema=SERVICE_ENABLE_SCHEMA, ) async def async_handle_disable_service(service): @@ -278,6 +311,12 @@ class PurgeTask(NamedTuple): apply_filter: bool +class PurgeEntitiesTask(NamedTuple): + """Object to store entity information about purge task.""" + + entity_filter: Callable[[str], bool] + + class PerodicCleanupTask: """An object to insert into the recorder to trigger cleanup tasks when auto purge is disabled.""" @@ -414,6 +453,11 @@ class Recorder(threading.Thread): self.queue.put(PurgeTask(keep_days, repack, apply_filter)) + def do_adhoc_purge_entities(self, entity_ids, domains, entity_globs): + """Trigger an adhoc purge of requested entities.""" + entity_filter = generate_filter(domains, entity_ids, [], [], entity_globs) + self.queue.put(PurgeEntitiesTask(entity_filter)) + def do_adhoc_statistics(self, **kwargs): """Trigger an adhoc statistics run.""" start = kwargs.get("start") @@ -663,6 +707,13 @@ class Recorder(threading.Thread): # Schedule a new purge task if this one didn't finish self.queue.put(PurgeTask(keep_days, repack, apply_filter)) + def _run_purge_entities(self, entity_filter): + """Purge entities from the database.""" + if purge.purge_entity_data(self, entity_filter): + return + # Schedule a new purge task if this one didn't finish + self.queue.put(PurgeEntitiesTask(entity_filter)) + def _run_statistics(self, start): """Run statistics task.""" if statistics.compile_statistics(self, start): @@ -675,6 +726,9 @@ class Recorder(threading.Thread): if isinstance(event, PurgeTask): self._run_purge(event.keep_days, event.repack, event.apply_filter) return + if isinstance(event, PurgeEntitiesTask): + self._run_purge_entities(event.entity_filter) + return if isinstance(event, PerodicCleanupTask): perodic_db_cleanups(self) return diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 62914c01de7..e1cf15e331d 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct @@ -195,3 +195,22 @@ def _purge_filtered_events(session: Session, excluded_event_types: list[str]) -> state_ids: list[int] = [state.state_id for state in states] _purge_state_ids(session, state_ids) _purge_event_ids(session, event_ids) + + +@retryable_database_job("purge") +def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) -> bool: + """Purge states and events of specified entities.""" + with session_scope(session=instance.get_session()) as session: # type: ignore + selected_entity_ids: list[str] = [ + entity_id + for (entity_id,) in session.query(distinct(States.entity_id)).all() + if entity_filter(entity_id) + ] + _LOGGER.debug("Purging entity data for %s", selected_entity_ids) + if len(selected_entity_ids) > 0: + # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record + _purge_filtered_states(session, selected_entity_ids) + _LOGGER.debug("Purging entity data hasn't fully completed yet") + return False + + return True diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index dcd8477d4bd..67879867cc7 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -18,8 +18,7 @@ purge: repack: name: Repack - description: - Attempt to save disk space by rewriting the entire database file. + description: Attempt to save disk space by rewriting the entire database file. example: true default: false selector: @@ -33,6 +32,30 @@ purge: selector: boolean: +purge_entities: + name: Purge Entities + description: Start purge task to remove specific entities from your database. + target: + entity: {} + fields: + domains: + name: Domains to remove + description: List the domains that need to be removed from the recorder database. + example: "sun" + required: false + default: [] + selector: + object: + + entity_globs: + name: Entity Globs to remove + description: List the regular expressions to select entities for removal from the recorder database. + example: "domain*.object_id*" + required: false + default: [] + selector: + object: + disable: name: Disable description: Stop the recording of events and state changes diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 5d4620ef29c..195e56dc748 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -17,6 +17,7 @@ from homeassistant.components.recorder import ( SERVICE_DISABLE, SERVICE_ENABLE, SERVICE_PURGE, + SERVICE_PURGE_ENTITIES, SQLITE_URL_PREFIX, Recorder, run_information, @@ -822,6 +823,7 @@ def test_has_services(hass_recorder): assert hass.services.has_service(DOMAIN, SERVICE_DISABLE) assert hass.services.has_service(DOMAIN, SERVICE_ENABLE) assert hass.services.has_service(DOMAIN, SERVICE_PURGE) + assert hass.services.has_service(DOMAIN, SERVICE_PURGE_ENTITIES) def test_service_disable_events_not_recording(hass, hass_recorder): diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index cdbe6e3c338..6727b4da495 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -653,6 +653,122 @@ async def test_purge_filtered_events_state_changed( assert session.query(States).get(63).old_state_id == 62 # should have been kept +async def test_purge_entities( + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT +): + """Test purging of specific entities.""" + instance = await async_setup_recorder_instance(hass) + + async def _purge_entities(hass, entity_ids, domains, entity_globs): + service_data = { + "entity_id": entity_ids, + "domains": domains, + "entity_globs": entity_globs, + } + + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE_ENTITIES, service_data + ) + await hass.async_block_till_done() + + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) + + def _add_purge_records(hass: HomeAssistant) -> None: + with recorder.session_scope(hass=hass) as session: + # Add states and state_changed events that should be purged + for days in range(1, 4): + timestamp = dt_util.utcnow() - timedelta(days=days) + for event_id in range(1000, 1020): + _add_state_and_state_changed_event( + session, + "sensor.purge_entity", + "purgeme", + timestamp, + event_id * days, + ) + timestamp = dt_util.utcnow() - timedelta(days=days) + for event_id in range(10000, 10020): + _add_state_and_state_changed_event( + session, + "purge_domain.entity", + "purgeme", + timestamp, + event_id * days, + ) + timestamp = dt_util.utcnow() - timedelta(days=days) + for event_id in range(100000, 100020): + _add_state_and_state_changed_event( + session, + "binary_sensor.purge_glob", + "purgeme", + timestamp, + event_id * days, + ) + + def _add_keep_records(hass: HomeAssistant) -> None: + with recorder.session_scope(hass=hass) as session: + # Add states and state_changed events that should be kept + timestamp = dt_util.utcnow() - timedelta(days=2) + for event_id in range(200, 210): + _add_state_and_state_changed_event( + session, + "sensor.keep", + "keep", + timestamp, + event_id, + ) + + _add_purge_records(hass) + _add_keep_records(hass) + + # Confirm standard service call + with session_scope(hass=hass) as session: + states = session.query(States) + assert states.count() == 190 + + await _purge_entities( + hass, "sensor.purge_entity", "purge_domain", "*purge_glob" + ) + assert states.count() == 10 + + states_sensor_kept = session.query(States).filter( + States.entity_id == "sensor.keep" + ) + assert states_sensor_kept.count() == 10 + + _add_purge_records(hass) + + # Confirm each parameter purges only the associated records + with session_scope(hass=hass) as session: + states = session.query(States) + assert states.count() == 190 + + await _purge_entities(hass, "sensor.purge_entity", [], []) + assert states.count() == 130 + + await _purge_entities(hass, [], "purge_domain", []) + assert states.count() == 70 + + await _purge_entities(hass, [], [], "*purge_glob") + assert states.count() == 10 + + states_sensor_kept = session.query(States).filter( + States.entity_id == "sensor.keep" + ) + assert states_sensor_kept.count() == 10 + + _add_purge_records(hass) + + # Confirm calling service without arguments matches all records (default filter behaviour) + with session_scope(hass=hass) as session: + states = session.query(States) + assert states.count() == 190 + + await _purge_entities(hass, [], [], []) + assert states.count() == 0 + + async def _add_test_states(hass: HomeAssistant, instance: recorder.Recorder): """Add multiple states to the db for testing.""" utcnow = dt_util.utcnow() From b9a0fb93ebcddcb5da2d3629daa34bc702180f61 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 22 May 2021 17:41:18 +0200 Subject: [PATCH 643/852] Add samsungtv dhcp and zeroconf discovery (#48022) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + CODEOWNERS | 2 +- .../components/samsungtv/__init__.py | 110 ++- homeassistant/components/samsungtv/bridge.py | 61 +- .../components/samsungtv/config_flow.py | 280 +++++-- homeassistant/components/samsungtv/const.py | 10 + .../components/samsungtv/manifest.json | 18 +- .../components/samsungtv/media_player.py | 76 +- .../components/samsungtv/strings.json | 22 +- .../components/samsungtv/translations/en.json | 18 +- homeassistant/generated/dhcp.py | 4 + homeassistant/generated/zeroconf.py | 6 + tests/components/samsungtv/__init__.py | 14 + tests/components/samsungtv/conftest.py | 112 +++ .../components/samsungtv/test_config_flow.py | 709 ++++++++++++++---- tests/components/samsungtv/test_init.py | 43 +- .../components/samsungtv/test_media_player.py | 103 ++- 17 files changed, 1174 insertions(+), 415 deletions(-) create mode 100644 tests/components/samsungtv/conftest.py diff --git a/.coveragerc b/.coveragerc index 227d932643c..e56773678c2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -873,6 +873,7 @@ omit = homeassistant/components/russound_rnet/media_player.py homeassistant/components/sabnzbd/* homeassistant/components/saj/sensor.py + homeassistant/components/samsungtv/bridge.py homeassistant/components/satel_integra/* homeassistant/components/schluter/* homeassistant/components/scrape/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 12689918e55..8049fec94b0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -412,7 +412,7 @@ homeassistant/components/rpi_power/* @shenxn @swetoast homeassistant/components/ruckus_unleashed/* @gabe565 homeassistant/components/safe_mode/* @home-assistant/core homeassistant/components/saj/* @fredericvl -homeassistant/components/samsungtv/* @escoand +homeassistant/components/samsungtv/* @escoand @chemelli74 homeassistant/components/scene/* @home-assistant/core homeassistant/components/schluter/* @prairieapps homeassistant/components/scrape/* @fabaff diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 64646533b2d..31b666793af 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -5,20 +5,31 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import ( + CONF_HOST, + CONF_METHOD, + CONF_NAME, + CONF_PORT, + CONF_TOKEN, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import CONF_ON_ACTION, DEFAULT_NAME, DOMAIN +from .bridge import SamsungTVBridge +from .const import CONF_ON_ACTION, DEFAULT_NAME, DOMAIN, LOGGER def ensure_unique_hosts(value): """Validate that all configs have a unique host.""" vol.Schema(vol.Unique("duplicate host entries found"))( - [socket.gethostbyname(entry[CONF_HOST]) for entry in value] + [entry[CONF_HOST] for entry in value] ) return value +PLATFORMS = [MP_DOMAIN] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -43,30 +54,87 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Samsung TV integration.""" - if DOMAIN in config: - hass.data[DOMAIN] = {} - for entry_config in config[DOMAIN]: - ip_address = await hass.async_add_executor_job( - socket.gethostbyname, entry_config[CONF_HOST] - ) - hass.data[DOMAIN][ip_address] = { - CONF_ON_ACTION: entry_config.get(CONF_ON_ACTION) - } - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_config, - ) - ) + hass.data[DOMAIN] = {} + if DOMAIN not in config: + return True + for entry_config in config[DOMAIN]: + ip_address = await hass.async_add_executor_job( + socket.gethostbyname, entry_config[CONF_HOST] + ) + hass.data[DOMAIN][ip_address] = { + CONF_ON_ACTION: entry_config.get(CONF_ON_ACTION) + } + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_config, + ) + ) return True +@callback +def _async_get_device_bridge(data): + """Get device bridge.""" + return SamsungTVBridge.get_bridge( + data[CONF_METHOD], + data[CONF_HOST], + data[CONF_PORT], + data.get(CONF_TOKEN), + ) + + async def async_setup_entry(hass, entry): """Set up the Samsung TV platform.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) + + # Initialize bridge + data = entry.data.copy() + bridge = _async_get_device_bridge(data) + if bridge.port is None and bridge.default_port is not None: + # For backward compat, set default port for websocket tv + data[CONF_PORT] = bridge.default_port + hass.config_entries.async_update_entry(entry, data=data) + bridge = _async_get_device_bridge(data) + + def stop_bridge(event): + """Stop SamsungTV bridge connection.""" + bridge.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) ) + hass.data[DOMAIN][entry.entry_id] = bridge + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN][entry.entry_id].stop() + return unload_ok + + +async def async_migrate_entry(hass, config_entry): + """Migrate old entry.""" + version = config_entry.version + + LOGGER.debug("Migrating from version %s", version) + + # 1 -> 2: Unique ID format changed, so delete and re-import: + if version == 1: + dev_reg = await hass.helpers.device_registry.async_get_registry() + dev_reg.async_clear_config_entry(config_entry) + + en_reg = await hass.helpers.entity_registry.async_get_registry() + en_reg.async_clear_config_entry(config_entry) + + version = config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry) + LOGGER.debug("Migration to version %s successful", version) + return True diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index dc8eb862ff7..84b518a4633 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -1,10 +1,11 @@ """samsungctl and samsungtvws bridge classes.""" from abc import ABC, abstractmethod +import contextlib from samsungctl import Remote from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse from samsungtvws import SamsungTVWS -from samsungtvws.exceptions import ConnectionFailure +from samsungtvws.exceptions import ConnectionFailure, HttpApiError from websocket import WebSocketException from homeassistant.const import ( @@ -25,8 +26,11 @@ from .const import ( RESULT_CANNOT_CONNECT, RESULT_NOT_SUPPORTED, RESULT_SUCCESS, + TIMEOUT_REQUEST, + TIMEOUT_WEBSOCKET, VALUE_CONF_ID, VALUE_CONF_NAME, + WEBSOCKET_PORTS, ) @@ -58,9 +62,14 @@ class SamsungTVBridge(ABC): def try_connect(self): """Try to connect to the TV.""" + @abstractmethod + def device_info(self): + """Try to gather infos of this TV.""" + def is_on(self): """Tells if the TV is on.""" - self.close_remote() + if self._remote: + self.close_remote() try: return self._get_remote() is not None @@ -104,7 +113,7 @@ class SamsungTVBridge(ABC): """Send the key.""" @abstractmethod - def _get_remote(self): + def _get_remote(self, avoid_open: bool = False): """Get Remote object.""" def close_remote(self): @@ -149,7 +158,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): CONF_METHOD: self.method, CONF_PORT: None, # We need this high timeout because waiting for auth popup is just an open socket - CONF_TIMEOUT: 31, + CONF_TIMEOUT: TIMEOUT_REQUEST, } try: LOGGER.debug("Try config: %s", config) @@ -162,11 +171,15 @@ class SamsungTVLegacyBridge(SamsungTVBridge): except UnhandledResponse: LOGGER.debug("Working but unsupported config: %s", config) return RESULT_NOT_SUPPORTED - except OSError as err: + except (ConnectionClosed, OSError) as err: LOGGER.debug("Failing config: %s, error: %s", config, err) return RESULT_CANNOT_CONNECT - def _get_remote(self): + def device_info(self): + """Try to gather infos of this device.""" + return None + + def _get_remote(self, avoid_open: bool = False): """Create or return a remote control instance.""" if self._remote is None: # We need to create a new instance to reconnect. @@ -184,6 +197,11 @@ class SamsungTVLegacyBridge(SamsungTVBridge): """Send the key using legacy protocol.""" self._get_remote().control(key) + def stop(self): + """Stop Bridge.""" + LOGGER.debug("Stopping SamsungRemote") + self.close_remote() + class SamsungTVWSBridge(SamsungTVBridge): """The Bridge for WebSocket TVs.""" @@ -196,14 +214,14 @@ class SamsungTVWSBridge(SamsungTVBridge): def try_connect(self): """Try to connect to the Websocket TV.""" - for self.port in (8001, 8002): + for self.port in WEBSOCKET_PORTS: config = { CONF_NAME: VALUE_CONF_NAME, CONF_HOST: self.host, CONF_METHOD: self.method, CONF_PORT: self.port, # We need this high timeout because waiting for auth popup is just an open socket - CONF_TIMEOUT: 31, + CONF_TIMEOUT: TIMEOUT_REQUEST, } result = None @@ -234,31 +252,46 @@ class SamsungTVWSBridge(SamsungTVBridge): return RESULT_CANNOT_CONNECT + def device_info(self): + """Try to gather infos of this TV.""" + remote = self._get_remote(avoid_open=True) + if not remote: + return None + with contextlib.suppress(HttpApiError): + return remote.rest_device_info() + def _send_key(self, key): """Send the key using websocket protocol.""" if key == "KEY_POWEROFF": key = "KEY_POWER" self._get_remote().send_key(key) - def _get_remote(self): + def _get_remote(self, avoid_open: bool = False): """Create or return a remote control instance.""" if self._remote is None: # We need to create a new instance to reconnect. try: - LOGGER.debug("Create SamsungTVWS") + LOGGER.debug( + "Create SamsungTVWS for %s (%s)", VALUE_CONF_NAME, self.host + ) self._remote = SamsungTVWS( host=self.host, port=self.port, token=self.token, - timeout=8, + timeout=TIMEOUT_WEBSOCKET, name=VALUE_CONF_NAME, ) - self._remote.open() + if not avoid_open: + self._remote.open() # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket except ConnectionFailure: self._notify_callback() - raise - except WebSocketException: + except (WebSocketException, OSError): self._remote = None return self._remote + + def stop(self): + """Stop Bridge.""" + LOGGER.debug("Stopping SamsungTVWS") + self.close_remote() diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index ed728b19eba..b45f6c5670b 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -4,7 +4,8 @@ from urllib.parse import urlparse import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER, @@ -13,59 +14,85 @@ from homeassistant.components.ssdp import ( ) from homeassistant.const import ( CONF_HOST, - CONF_ID, - CONF_IP_ADDRESS, + CONF_MAC, CONF_METHOD, CONF_NAME, CONF_PORT, CONF_TOKEN, ) +from homeassistant.core import callback +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.typing import DiscoveryInfoType from .bridge import SamsungTVBridge from .const import ( + ATTR_PROPERTIES, CONF_MANUFACTURER, CONF_MODEL, + DEFAULT_MANUFACTURER, DOMAIN, + LEGACY_PORT, LOGGER, METHOD_LEGACY, METHOD_WEBSOCKET, RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT, + RESULT_NOT_SUPPORTED, RESULT_SUCCESS, + RESULT_UNKNOWN_HOST, + WEBSOCKET_PORTS, ) DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) SUPPORTED_METHODS = [METHOD_LEGACY, METHOD_WEBSOCKET] -def _get_ip(host): - if host is None: - return None - return socket.gethostbyname(host) +def _get_device_info(host): + """Fetch device info by any websocket method.""" + for port in WEBSOCKET_PORTS: + bridge = SamsungTVBridge.get_bridge(METHOD_WEBSOCKET, host, port) + if info := bridge.device_info(): + return info + return None + + +async def async_get_device_info(hass, bridge, host): + """Fetch device info from bridge or websocket.""" + if bridge: + return await hass.async_add_executor_job(bridge.device_info) + + return await hass.async_add_executor_job(_get_device_info, host) + + +def _strip_uuid(udn): + return udn[5:] if udn.startswith("uuid:") else udn class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Samsung TV config flow.""" - VERSION = 1 + VERSION = 2 def __init__(self): """Initialize flow.""" + self._reauth_entry = None self._host = None - self._ip = None + self._mac = None + self._udn = None self._manufacturer = None self._model = None self._name = None self._title = None self._id = None self._bridge = None + self._device_info = None - def _get_entry(self): + def _get_entry_from_bridge(self): + """Get device entry.""" data = { CONF_HOST: self._host, - CONF_ID: self._id, - CONF_IP_ADDRESS: self._ip, - CONF_MANUFACTURER: self._manufacturer, + CONF_MAC: self._mac, + CONF_MANUFACTURER: self._manufacturer or DEFAULT_MANUFACTURER, CONF_METHOD: self._bridge.method, CONF_MODEL: self._model, CONF_NAME: self._name, @@ -78,98 +105,205 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data=data, ) + async def _async_set_device_unique_id(self, raise_on_progress=True): + """Set device unique_id.""" + await self._async_get_and_check_device_info() + await self._async_set_unique_id_from_udn(raise_on_progress) + + async def _async_set_unique_id_from_udn(self, raise_on_progress=True): + """Set the unique id from the udn.""" + await self.async_set_unique_id(self._udn, raise_on_progress=raise_on_progress) + self._async_update_existing_host_entry(self._host) + updates = {CONF_HOST: self._host} + if self._mac: + updates[CONF_MAC] = self._mac + self._abort_if_unique_id_configured(updates=updates) + def _try_connect(self): """Try to connect and check auth.""" for method in SUPPORTED_METHODS: self._bridge = SamsungTVBridge.get_bridge(method, self._host) result = self._bridge.try_connect() + if result == RESULT_SUCCESS: + return if result != RESULT_CANNOT_CONNECT: - return result + raise data_entry_flow.AbortFlow(result) LOGGER.debug("No working config found") - return RESULT_CANNOT_CONNECT + raise data_entry_flow.AbortFlow(RESULT_CANNOT_CONNECT) + + async def _async_get_and_check_device_info(self): + """Try to get the device info.""" + info = await async_get_device_info(self.hass, self._bridge, self._host) + if not info: + raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + dev_info = info.get("device", {}) + device_type = dev_info.get("type") + if device_type != "Samsung SmartTV": + raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + self._model = dev_info.get("modelName") + name = dev_info.get("name") + self._name = name.replace("[TV] ", "") if name else device_type + self._title = f"{self._name} ({self._model})" + self._udn = _strip_uuid(dev_info.get("udn", info["id"])) + if dev_info.get("networkType") == "wireless" and dev_info.get("wifiMac"): + self._mac = format_mac(dev_info.get("wifiMac")) + self._device_info = info async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" - return await self.async_step_user(user_input) + # We need to import even if we cannot validate + # since the TV may be off at startup + await self._async_set_name_host_from_input(user_input) + self._async_abort_entries_match({CONF_HOST: self._host}) + if user_input.get(CONF_PORT) in WEBSOCKET_PORTS: + user_input[CONF_METHOD] = METHOD_WEBSOCKET + else: + user_input[CONF_METHOD] = METHOD_LEGACY + user_input[CONF_PORT] = LEGACY_PORT + user_input[CONF_MANUFACTURER] = DEFAULT_MANUFACTURER + return self.async_create_entry( + title=self._title, + data=user_input, + ) + + async def _async_set_name_host_from_input(self, user_input): + try: + self._host = await self.hass.async_add_executor_job( + socket.gethostbyname, user_input[CONF_HOST] + ) + except socket.gaierror as err: + raise data_entry_flow.AbortFlow(RESULT_UNKNOWN_HOST) from err + self._name = user_input[CONF_NAME] + self._title = self._name async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if user_input is not None: - ip_address = await self.hass.async_add_executor_job( - _get_ip, user_input[CONF_HOST] - ) - - await self.async_set_unique_id(ip_address) - self._abort_if_unique_id_configured() - - self._host = user_input.get(CONF_HOST) - self._ip = self.context[CONF_IP_ADDRESS] = ip_address - self._name = user_input.get(CONF_NAME) - self._title = self._name - - result = await self.hass.async_add_executor_job(self._try_connect) - - if result != RESULT_SUCCESS: - return self.async_abort(reason=result) - return self._get_entry() + await self._async_set_name_host_from_input(user_input) + await self.hass.async_add_executor_job(self._try_connect) + self._async_abort_entries_match({CONF_HOST: self._host}) + if self._bridge.method != METHOD_LEGACY: + # Legacy bridge does not provide device info + await self._async_set_device_unique_id(raise_on_progress=False) + return self._get_entry_from_bridge() return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) - async def async_step_ssdp(self, discovery_info): - """Handle a flow initialized by discovery.""" - host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname - ip_address = await self.hass.async_add_executor_job(_get_ip, host) + @callback + def _async_update_existing_host_entry(self, host): + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_HOST] != host: + continue + entry_kw_args = {} + if self.unique_id and entry.unique_id is None: + entry_kw_args["unique_id"] = self.unique_id + if self._mac and not entry.data.get(CONF_MAC): + data_copy = dict(entry.data) + data_copy[CONF_MAC] = self._mac + entry_kw_args["data"] = data_copy + if entry_kw_args: + self.hass.config_entries.async_update_entry(entry, **entry_kw_args) + return entry + return None + + async def _async_start_discovery_for_host(self, host): + """Start discovery for a host.""" + if entry := self._async_update_existing_host_entry(host): + if entry.unique_id: + # Let the flow continue to fill the missing + # unique id as we may be able to obtain it + # in the next step + raise data_entry_flow.AbortFlow("already_configured") + + self.context[CONF_HOST] = host + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == host: + raise data_entry_flow.AbortFlow("already_in_progress") self._host = host - self._ip = self.context[CONF_IP_ADDRESS] = ip_address - self._manufacturer = discovery_info.get(ATTR_UPNP_MANUFACTURER) - self._model = discovery_info.get(ATTR_UPNP_MODEL_NAME) - self._name = f"Samsung {self._model}" - self._id = discovery_info.get(ATTR_UPNP_UDN) - self._title = self._model - # probably access denied - if self._id is None: - return self.async_abort(reason=RESULT_AUTH_MISSING) - if self._id.startswith("uuid:"): - self._id = self._id[5:] - - await self.async_set_unique_id(ip_address) - self._abort_if_unique_id_configured( - { - CONF_ID: self._id, - CONF_MANUFACTURER: self._manufacturer, - CONF_MODEL: self._model, - } + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType): + """Handle a flow initialized by ssdp discovery.""" + self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN]) + await self._async_set_unique_id_from_udn() + await self._async_start_discovery_for_host( + urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname ) + self._manufacturer = discovery_info[ATTR_UPNP_MANUFACTURER] + if not self._manufacturer or not self._manufacturer.lower().startswith( + "samsung" + ): + raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + self._name = self._title = self._model = discovery_info.get( + ATTR_UPNP_MODEL_NAME + ) + self.context["title_placeholders"] = {"device": self._title} + return await self.async_step_confirm() - self.context["title_placeholders"] = {"model": self._model} + async def async_step_dhcp(self, discovery_info: DiscoveryInfoType): + """Handle a flow initialized by dhcp discovery.""" + self._mac = discovery_info[MAC_ADDRESS] + await self._async_start_discovery_for_host(discovery_info[IP_ADDRESS]) + await self._async_set_device_unique_id() + self.context["title_placeholders"] = {"device": self._title} + return await self.async_step_confirm() + + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + """Handle a flow initialized by zeroconf discovery.""" + self._mac = format_mac(discovery_info[ATTR_PROPERTIES]["deviceid"]) + await self._async_start_discovery_for_host(discovery_info[CONF_HOST]) + await self._async_set_device_unique_id() + self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() async def async_step_confirm(self, user_input=None): """Handle user-confirmation of discovered node.""" if user_input is not None: - result = await self.hass.async_add_executor_job(self._try_connect) - if result != RESULT_SUCCESS: - return self.async_abort(reason=result) - return self._get_entry() + await self.hass.async_add_executor_job(self._try_connect) + return self._get_entry_from_bridge() + self._set_confirm_only() return self.async_show_form( - step_id="confirm", description_placeholders={"model": self._model} + step_id="confirm", description_placeholders={"device": self._title} ) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, data): """Handle configuration by re-auth.""" - self._host = user_input[CONF_HOST] - self._id = user_input.get(CONF_ID) - self._ip = user_input[CONF_IP_ADDRESS] - self._manufacturer = user_input.get(CONF_MANUFACTURER) - self._model = user_input.get(CONF_MODEL) - self._name = user_input.get(CONF_NAME) - self._title = self._model or self._name + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + data = self._reauth_entry.data + if data.get(CONF_MODEL) and data.get(CONF_NAME): + self._title = f"{data[CONF_NAME]} ({data[CONF_MODEL]})" + else: + self._title = data.get(CONF_NAME) or data[CONF_HOST] + return await self.async_step_reauth_confirm() - await self.async_set_unique_id(self._ip) - self.context["title_placeholders"] = {"model": self._title} + async def async_step_reauth_confirm(self, user_input=None): + """Confirm reauth.""" + errors = {} + if user_input is not None: + bridge = SamsungTVBridge.get_bridge( + self._reauth_entry.data[CONF_METHOD], self._reauth_entry.data[CONF_HOST] + ) + result = bridge.try_connect() + if result == RESULT_SUCCESS: + new_data = dict(self._reauth_entry.data) + new_data[CONF_TOKEN] = bridge.token + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=new_data + ) + return self.async_abort(reason="reauth_successful") + if result not in (RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT): + return self.async_abort(reason=result) - return await self.async_step_confirm() + # On websocket we will get RESULT_CANNOT_CONNECT when auth is missing + errors = {"base": RESULT_AUTH_MISSING} + + self.context["title_placeholders"] = {"device": self._title} + return self.async_show_form( + step_id="reauth_confirm", + errors=errors, + description_placeholders={"device": self._title}, + ) diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index e043c74b347..f2571372b1f 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -4,7 +4,10 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "samsungtv" +ATTR_PROPERTIES = "properties" + DEFAULT_NAME = "Samsung TV" +DEFAULT_MANUFACTURER = "Samsung" VALUE_CONF_NAME = "HomeAssistant" VALUE_CONF_ID = "ha.component.samsung" @@ -18,6 +21,13 @@ RESULT_AUTH_MISSING = "auth_missing" RESULT_SUCCESS = "success" RESULT_CANNOT_CONNECT = "cannot_connect" RESULT_NOT_SUPPORTED = "not_supported" +RESULT_UNKNOWN_HOST = "unknown" METHOD_LEGACY = "legacy" METHOD_WEBSOCKET = "websocket" + +TIMEOUT_REQUEST = 31 +TIMEOUT_WEBSOCKET = 5 + +LEGACY_PORT = 55000 +WEBSOCKET_PORTS = (8002, 8001) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 81e08ddeaa6..4206aca7213 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -2,13 +2,27 @@ "domain": "samsungtv", "name": "Samsung Smart TV", "documentation": "https://www.home-assistant.io/integrations/samsungtv", - "requirements": ["samsungctl[websocket]==0.7.1", "samsungtvws==1.6.0"], + "requirements": [ + "samsungctl[websocket]==0.7.1", + "samsungtvws==1.6.0" + ], "ssdp": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], - "codeowners": ["@escoand"], + "zeroconf": [ + {"type":"_airplay._tcp.local.","manufacturer":"samsung*"} + ], + "dhcp": [ + { + "hostname": "tizen*" + } + ], + "codeowners": [ + "@escoand", + "@chemelli74" + ], "config_flow": true, "iot_class": "local_polling" } diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index a4b61369f99..72e21ed205c 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -19,22 +19,12 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import ( - CONF_HOST, - CONF_ID, - CONF_IP_ADDRESS, - CONF_METHOD, - CONF_NAME, - CONF_PORT, - CONF_TOKEN, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.script import Script from homeassistant.util import dt as dt_util -from .bridge import SamsungTVBridge from .const import ( CONF_MANUFACTURER, CONF_MODEL, @@ -60,41 +50,19 @@ SUPPORT_SAMSUNGTV = ( ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Samsung TV from a config entry.""" - ip_address = config_entry.data[CONF_IP_ADDRESS] + bridge = hass.data[DOMAIN][entry.entry_id] + + host = entry.data[CONF_HOST] on_script = None - if ( - DOMAIN in hass.data - and ip_address in hass.data[DOMAIN] - and CONF_ON_ACTION in hass.data[DOMAIN][ip_address] - and hass.data[DOMAIN][ip_address][CONF_ON_ACTION] - ): - turn_on_action = hass.data[DOMAIN][ip_address][CONF_ON_ACTION] + data = hass.data[DOMAIN] + if turn_on_action := data.get(host, {}).get(CONF_ON_ACTION): on_script = Script( - hass, turn_on_action, config_entry.data.get(CONF_NAME, DEFAULT_NAME), DOMAIN + hass, turn_on_action, entry.data.get(CONF_NAME, DEFAULT_NAME), DOMAIN ) - # Initialize bridge - data = config_entry.data.copy() - bridge = SamsungTVBridge.get_bridge( - data[CONF_METHOD], - data[CONF_HOST], - data[CONF_PORT], - data.get(CONF_TOKEN), - ) - if bridge.port is None and bridge.default_port is not None: - # For backward compat, set default port for websocket tv - data[CONF_PORT] = bridge.default_port - hass.config_entries.async_update_entry(config_entry, data=data) - bridge = SamsungTVBridge.get_bridge( - data[CONF_METHOD], - data[CONF_HOST], - data[CONF_PORT], - data.get(CONF_TOKEN), - ) - - async_add_entities([SamsungTVDevice(bridge, config_entry, on_script)]) + async_add_entities([SamsungTVDevice(bridge, entry, on_script)], True) class SamsungTVDevice(MediaPlayerEntity): @@ -103,11 +71,12 @@ class SamsungTVDevice(MediaPlayerEntity): def __init__(self, bridge, config_entry, on_script): """Initialize the Samsung device.""" self._config_entry = config_entry + self._mac = config_entry.data.get(CONF_MAC) self._manufacturer = config_entry.data.get(CONF_MANUFACTURER) self._model = config_entry.data.get(CONF_MODEL) self._name = config_entry.data.get(CONF_NAME) self._on_script = on_script - self._uuid = config_entry.data.get(CONF_ID) + self._uuid = config_entry.unique_id # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode @@ -117,21 +86,28 @@ class SamsungTVDevice(MediaPlayerEntity): # sending the next command to avoid turning the TV back ON). self._end_of_power_off = None self._bridge = bridge + self._auth_failed = False self._bridge.register_reauth_callback(self.access_denied) def access_denied(self): """Access denied callback.""" LOGGER.debug("Access denied in getting remote object") + self._auth_failed = True self.hass.add_job( self.hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_REAUTH}, + context={ + "source": SOURCE_REAUTH, + "entry_id": self._config_entry.entry_id, + }, data=self._config_entry.data, ) ) def update(self): """Update state of device.""" + if self._auth_failed: + return if self._power_off_in_progress(): self._state = STATE_OFF else: @@ -165,15 +141,25 @@ class SamsungTVDevice(MediaPlayerEntity): """Return the state of the device.""" return self._state + @property + def available(self): + """Return the availability of the device.""" + if self._auth_failed: + return False + return self._state == STATE_ON or self._on_script + @property def device_info(self): """Return device specific attributes.""" - return { + info = { "name": self.name, "identifiers": {(DOMAIN, self.unique_id)}, "manufacturer": self._manufacturer, "model": self._model, } + if self._mac: + info["connections"] = {(CONNECTION_NETWORK_MAC, self._mac)} + return info @property def is_volume_muted(self): diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index 3854d040d3e..f92990e6163 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "{model}", + "flow_title": "{device}", "step": { "user": { "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", @@ -10,16 +10,24 @@ } }, "confirm": { - "title": "Samsung TV", - "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization. Manual configurations for this TV will be overwritten." - } + "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." + }, + "reauth_confirm": { + "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds." + } + }, + "error": { + "auth_missing": "[%key:component::samsungtv::config::abort::auth_missing%]" }, "abort": { "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.", + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant.", + "id_missing": "This Samsung device doesn't have a SerialNumber.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "not_supported": "This Samsung TV device is currently not supported." + "not_supported": "This Samsung device is currently not supported.", + "unknown": "[%key:common::config_flow::error::unknown%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 8f05775eb0c..8b48de950ee 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -3,15 +3,23 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.", + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant.", "cannot_connect": "Failed to connect", - "not_supported": "This Samsung TV device is currently not supported." + "id_missing": "This Samsung device doesn't have a SerialNumber.", + "not_supported": "This Samsung device is currently not supported.", + "reauth_successful": "Re-authentication was successful", + "unknown": "Unexpected error" }, - "flow_title": "{model}", + "error": { + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant." + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization. Manual configurations for this TV will be overwritten.", - "title": "Samsung TV" + "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." + }, + "reauth_confirm": { + "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds." }, "user": { "data": { diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 2a592953123..7ea9d1c1992 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -171,6 +171,10 @@ DHCP = [ "hostname": "roomba-*", "macaddress": "80A589*" }, + { + "domain": "samsungtv", + "hostname": "tizen*" + }, { "domain": "screenlogic", "hostname": "pentair: *", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index dce459e2083..014edc4b1f3 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -11,6 +11,12 @@ ZEROCONF = { "domain": "volumio" } ], + "_airplay._tcp.local.": [ + { + "domain": "samsungtv", + "manufacturer": "samsung*" + } + ], "_api._udp.local.": [ { "domain": "guardian" diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index 4ad1622c6ca..84328736822 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -1 +1,15 @@ """Tests for the samsungtv component.""" +from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def setup_samsungtv(hass: HomeAssistant, config: dict): + """Set up mock Samsung TV.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry(domain=SAMSUNGTV_DOMAIN, data=config) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py new file mode 100644 index 00000000000..278c6d7f18a --- /dev/null +++ b/tests/components/samsungtv/conftest.py @@ -0,0 +1,112 @@ +"""Fixtures for Samsung TV.""" +from unittest.mock import Mock, patch + +import pytest + +import homeassistant.util.dt as dt_util + +RESULT_ALREADY_CONFIGURED = "already_configured" +RESULT_ALREADY_IN_PROGRESS = "already_in_progress" + + +@pytest.fixture(name="remote") +def remote_fixture(): + """Patch the samsungctl Remote.""" + with patch( + "homeassistant.components.samsungtv.bridge.Remote" + ) as remote_class, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + remote = Mock() + remote.__enter__ = Mock() + remote.__exit__ = Mock() + remote_class.return_value = remote + yield remote + + +@pytest.fixture(name="remotews") +def remotews_fixture(): + """Patch the samsungtvws SamsungTVWS.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" + ) as remotews_class, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + remotews = Mock() + remotews.__enter__ = Mock() + remotews.__exit__ = Mock() + remotews.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + "networkType": "wireless", + }, + } + remotews_class.return_value = remotews + remotews_class().__enter__().token = "FAKE_TOKEN" + yield remotews + + +@pytest.fixture(name="remotews_no_device_info") +def remotews_no_device_info_fixture(): + """Patch the samsungtvws SamsungTVWS.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" + ) as remotews_class, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + remotews = Mock() + remotews.__enter__ = Mock() + remotews.__exit__ = Mock() + remotews.rest_device_info.return_value = None + remotews_class.return_value = remotews + remotews_class().__enter__().token = "FAKE_TOKEN" + yield remotews + + +@pytest.fixture(name="remotews_soundbar") +def remotews_soundbar_fixture(): + """Patch the samsungtvws SamsungTVWS.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" + ) as remotews_class, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + remotews = Mock() + remotews.__enter__ = Mock() + remotews.__exit__ = Mock() + remotews.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "mac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SoundBar", + }, + } + remotews_class.return_value = remotews + remotews_class().__enter__().token = "FAKE_TOKEN" + yield remotews + + +@pytest.fixture(name="delay") +def delay_fixture(): + """Patch the delay script function.""" + with patch( + "homeassistant.components.samsungtv.media_player.Script.async_run" + ) as delay: + yield delay + + +@pytest.fixture +def mock_now(): + """Fixture for dtutil.now.""" + return dt_util.utcnow() diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index fb1b2a2bc67..5ac6caf40a9 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,16 +1,27 @@ """Tests for Samsung TV config flow.""" -from unittest.mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch +import socket +from unittest.mock import Mock, PropertyMock, call, patch -import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse from samsungtvws.exceptions import ConnectionFailure -from websocket import WebSocketProtocolException +from websocket import WebSocketException, WebSocketProtocolException from homeassistant import config_entries +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.components.samsungtv.const import ( + ATTR_PROPERTIES, CONF_MANUFACTURER, CONF_MODEL, + DEFAULT_MANUFACTURER, DOMAIN, + METHOD_LEGACY, + METHOD_WEBSOCKET, + RESULT_AUTH_MISSING, + RESULT_CANNOT_CONNECT, + RESULT_NOT_SUPPORTED, + RESULT_UNKNOWN_HOST, + TIMEOUT_REQUEST, + TIMEOUT_WEBSOCKET, ) from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, @@ -19,22 +30,81 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_MODEL_NAME, ATTR_UPNP_UDN, ) -from homeassistant.const import CONF_HOST, CONF_ID, CONF_METHOD, CONF_NAME, CONF_TOKEN +from homeassistant.const import ( + CONF_HOST, + CONF_ID, + CONF_IP_ADDRESS, + CONF_MAC, + CONF_METHOD, + CONF_NAME, + CONF_PORT, + CONF_TOKEN, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry +from tests.components.samsungtv.conftest import ( + RESULT_ALREADY_CONFIGURED, + RESULT_ALREADY_IN_PROGRESS, +) + +MOCK_IMPORT_DATA = { + CONF_HOST: "fake_host", + CONF_NAME: "fake", + CONF_PORT: 55000, +} +MOCK_IMPORT_WSDATA = { + CONF_HOST: "fake_host", + CONF_NAME: "fake", + CONF_PORT: 8002, +} MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", - ATTR_UPNP_FRIENDLY_NAME: "[TV]fake_name", - ATTR_UPNP_MANUFACTURER: "fake_manufacturer", + ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", + ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", ATTR_UPNP_MODEL_NAME: "fake_model", - ATTR_UPNP_UDN: "uuid:fake_uuid", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", } MOCK_SSDP_DATA_NOPREFIX = { ATTR_SSDP_LOCATION: "http://fake2_host:12345/test", ATTR_UPNP_FRIENDLY_NAME: "fake2_name", - ATTR_UPNP_MANUFACTURER: "fake2_manufacturer", + ATTR_UPNP_MANUFACTURER: "Samsung fake2_manufacturer", ATTR_UPNP_MODEL_NAME: "fake2_model", - ATTR_UPNP_UDN: "fake2_uuid", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", +} +MOCK_SSDP_DATA_WRONGMODEL = { + ATTR_SSDP_LOCATION: "http://fake2_host:12345/test", + ATTR_UPNP_FRIENDLY_NAME: "fake2_name", + ATTR_UPNP_MANUFACTURER: "fake2_manufacturer", + ATTR_UPNP_MODEL_NAME: "HW-Qfake", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", +} +MOCK_DHCP_DATA = {IP_ADDRESS: "fake_host", MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"} +MOCK_ZEROCONF_DATA = { + CONF_HOST: "fake_host", + CONF_PORT: 1234, + ATTR_PROPERTIES: { + "deviceid": "aa:bb:cc:dd:ee:ff", + "manufacturer": "fake_manufacturer", + "model": "fake_model", + "serialNumber": "fake_serial", + }, +} +MOCK_OLD_ENTRY = { + CONF_HOST: "fake_host", + CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old", + CONF_IP_ADDRESS: "fake_ip_old", + CONF_METHOD: "legacy", + CONF_PORT: None, +} +MOCK_WS_ENTRY = { + CONF_HOST: "fake_host", + CONF_METHOD: METHOD_WEBSOCKET, + CONF_PORT: 8002, + CONF_MODEL: "any", + CONF_NAME: "any", } AUTODETECT_LEGACY = { @@ -44,62 +114,32 @@ AUTODETECT_LEGACY = { "method": "legacy", "port": None, "host": "fake_host", - "timeout": 31, + "timeout": TIMEOUT_REQUEST, } AUTODETECT_WEBSOCKET_PLAIN = { "host": "fake_host", "name": "HomeAssistant", "port": 8001, - "timeout": 31, + "timeout": TIMEOUT_REQUEST, "token": None, } AUTODETECT_WEBSOCKET_SSL = { "host": "fake_host", "name": "HomeAssistant", "port": 8002, - "timeout": 31, + "timeout": TIMEOUT_REQUEST, "token": None, } +DEVICEINFO_WEBSOCKET_SSL = { + "host": "fake_host", + "name": "HomeAssistant", + "port": 8002, + "timeout": TIMEOUT_WEBSOCKET, + "token": "123456789", +} -@pytest.fixture(name="remote") -def remote_fixture(): - """Patch the samsungctl Remote.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote" - ) as remote_class, patch( - "homeassistant.components.samsungtv.config_flow.socket" - ) as socket_class: - remote = Mock() - remote.__enter__ = Mock() - remote.__exit__ = Mock() - remote_class.return_value = remote - socket = Mock() - socket_class.return_value = socket - socket_class.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield remote - - -@pytest.fixture(name="remotews") -def remotews_fixture(): - """Patch the samsungtvws SamsungTVWS.""" - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remotews_class, patch( - "homeassistant.components.samsungtv.config_flow.socket" - ) as socket_class: - remotews = Mock() - remotews.__enter__ = Mock() - remotews.__exit__ = Mock() - remotews_class.return_value = remotews - remotews_class().__enter__().token = "FAKE_TOKEN" - socket = Mock() - socket_class.return_value = socket - socket_class.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield remotews - - -async def test_user_legacy(hass, remote): +async def test_user_legacy(hass: HomeAssistant, remote: Mock): """Test starting a flow by user.""" # show form result = await hass.config_entries.flow.async_init( @@ -118,12 +158,12 @@ async def test_user_legacy(hass, remote): assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_name" assert result["data"][CONF_METHOD] == "legacy" - assert result["data"][CONF_MANUFACTURER] is None + assert result["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result["data"][CONF_MODEL] is None - assert result["data"][CONF_ID] is None + assert result["result"].unique_id is None -async def test_user_websocket(hass, remotews): +async def test_user_websocket(hass: HomeAssistant, remotews: Mock): """Test starting a flow by user.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom") @@ -139,46 +179,46 @@ async def test_user_websocket(hass, remotews): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - # legacy tv entry created + # websocket tv entry created assert result["type"] == "create_entry" - assert result["title"] == "fake_name" + assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "fake_name" + assert result["data"][CONF_NAME] == "Living Room" assert result["data"][CONF_METHOD] == "websocket" - assert result["data"][CONF_MANUFACTURER] is None - assert result["data"][CONF_MODEL] is None - assert result["data"][CONF_ID] is None + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -async def test_user_legacy_missing_auth(hass): +async def test_user_legacy_missing_auth(hass: HomeAssistant, remote: Mock): """Test starting a flow by user with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=AccessDenied("Boom"), - ), patch("homeassistant.components.samsungtv.config_flow.socket"): + ): # legacy device missing authentication result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "auth_missing" + assert result["reason"] == RESULT_AUTH_MISSING -async def test_user_legacy_not_supported(hass): +async def test_user_legacy_not_supported(hass: HomeAssistant, remote: Mock): """Test starting a flow by user for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=UnhandledResponse("Boom"), - ), patch("homeassistant.components.samsungtv.config_flow.socket"): + ): # legacy device not supported result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_user_websocket_not_supported(hass): +async def test_user_websocket_not_supported(hass: HomeAssistant, remotews: Mock): """Test starting a flow by user for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -186,18 +226,16 @@ async def test_user_websocket_not_supported(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=WebSocketProtocolException("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): # websocket device not supported result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_user_not_successful(hass): +async def test_user_not_successful(hass: HomeAssistant, remotews: Mock): """Test starting a flow by user but no connection found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -205,17 +243,15 @@ async def test_user_not_successful(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_user_not_successful_2(hass): +async def test_user_not_successful_2(hass: HomeAssistant, remotews: Mock): """Test starting a flow by user but no connection found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -223,34 +259,15 @@ async def test_user_not_successful_2(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=ConnectionFailure("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_user_already_configured(hass, remote): - """Test starting a flow by user when already configured.""" - - # entry was added - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA - ) - assert result["type"] == "create_entry" - - # failed as already configured - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA - ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - -async def test_ssdp(hass, remote): +async def test_ssdp(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery.""" # confirm to add the entry @@ -267,13 +284,13 @@ async def test_ssdp(hass, remote): assert result["type"] == "create_entry" assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "Samsung fake_model" - assert result["data"][CONF_MANUFACTURER] == "fake_manufacturer" + assert result["data"][CONF_NAME] == "fake_model" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "fake_model" - assert result["data"][CONF_ID] == "fake_uuid" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_ssdp_noprefix(hass, remote): +async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery without prefixes.""" # confirm to add the entry @@ -292,18 +309,18 @@ async def test_ssdp_noprefix(hass, remote): assert result["type"] == "create_entry" assert result["title"] == "fake2_model" assert result["data"][CONF_HOST] == "fake2_host" - assert result["data"][CONF_NAME] == "Samsung fake2_model" - assert result["data"][CONF_MANUFACTURER] == "fake2_manufacturer" + assert result["data"][CONF_NAME] == "fake2_model" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" assert result["data"][CONF_MODEL] == "fake2_model" - assert result["data"][CONF_ID] == "fake2_uuid" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" -async def test_ssdp_legacy_missing_auth(hass): +async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=AccessDenied("Boom"), - ), patch("homeassistant.components.samsungtv.config_flow.socket"): + ): # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -317,15 +334,15 @@ async def test_ssdp_legacy_missing_auth(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "auth_missing" + assert result["reason"] == RESULT_AUTH_MISSING -async def test_ssdp_legacy_not_supported(hass): +async def test_ssdp_legacy_not_supported(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=UnhandledResponse("Boom"), - ), patch("homeassistant.components.samsungtv.config_flow.socket"): + ): # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -339,10 +356,10 @@ async def test_ssdp_legacy_not_supported(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_ssdp_websocket_not_supported(hass): +async def test_ssdp_websocket_not_supported(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -350,8 +367,6 @@ async def test_ssdp_websocket_not_supported(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=WebSocketProtocolException("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -365,10 +380,23 @@ async def test_ssdp_websocket_not_supported(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_ssdp_not_successful(hass): +async def test_ssdp_model_not_supported(hass: HomeAssistant, remote: Mock): + """Test starting a flow from discovery.""" + + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_WRONGMODEL, + ) + assert result["type"] == "abort" + assert result["reason"] == RESULT_NOT_SUPPORTED + + +async def test_ssdp_not_successful(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery but no device found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -376,8 +404,6 @@ async def test_ssdp_not_successful(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): # confirm to add the entry @@ -392,10 +418,10 @@ async def test_ssdp_not_successful(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_ssdp_not_successful_2(hass): +async def test_ssdp_not_successful_2(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery but no device found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -403,8 +429,6 @@ async def test_ssdp_not_successful_2(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=ConnectionFailure("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): # confirm to add the entry @@ -419,10 +443,10 @@ async def test_ssdp_not_successful_2(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_ssdp_already_in_progress(hass, remote): +async def test_ssdp_already_in_progress(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery twice.""" # confirm to add the entry @@ -437,10 +461,10 @@ async def test_ssdp_already_in_progress(hass, remote): DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "abort" - assert result["reason"] == "already_in_progress" + assert result["reason"] == RESULT_ALREADY_IN_PROGRESS -async def test_ssdp_already_configured(hass, remote): +async def test_ssdp_already_configured(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery when already configured.""" # entry was added @@ -449,60 +473,204 @@ async def test_ssdp_already_configured(hass, remote): ) assert result["type"] == "create_entry" entry = result["result"] - assert entry.data[CONF_MANUFACTURER] is None + assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert entry.data[CONF_MODEL] is None - assert entry.data[CONF_ID] is None + assert entry.unique_id is None # failed as already configured result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result2["type"] == "abort" - assert result2["reason"] == "already_configured" + assert result2["reason"] == RESULT_ALREADY_CONFIGURED # check updated device info - assert entry.data[CONF_MANUFACTURER] == "fake_manufacturer" - assert entry.data[CONF_MODEL] == "fake_model" - assert entry.data[CONF_ID] == "fake_uuid" + assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_autodetect_websocket(hass, remote, remotews): - """Test for send key with autodetection of protocol.""" +async def test_import_legacy(hass: HomeAssistant): + """Test importing from yaml with hostname.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remotews: - enter = Mock() - type(enter).token = PropertyMock(return_value="123456789") - remote = Mock() - remote.__enter__ = Mock(return_value=enter) - remote.__exit__ = Mock(return_value=False) - remotews.return_value = remote - + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA, ) - assert result["type"] == "create_entry" - assert result["data"][CONF_METHOD] == "websocket" - assert result["data"][CONF_TOKEN] == "123456789" - assert remotews.call_count == 1 - assert remotews.call_args_list == [call(**AUTODETECT_WEBSOCKET_PLAIN)] + await hass.async_block_till_done() + assert result["type"] == "create_entry" + assert result["title"] == "fake" + assert result["data"][CONF_METHOD] == METHOD_LEGACY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["result"].unique_id is None -async def test_autodetect_websocket_ssl(hass, remote, remotews): +async def test_import_websocket(hass: HomeAssistant): + """Test importing from yaml with hostname.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_WSDATA, + ) + await hass.async_block_till_done() + assert result["type"] == "create_entry" + assert result["title"] == "fake" + assert result["data"][CONF_METHOD] == METHOD_WEBSOCKET + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["result"].unique_id is None + + +async def test_import_unknown_host(hass: HomeAssistant, remotews: Mock): + """Test importing from yaml with hostname that does not resolve.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + side_effect=socket.gaierror, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == RESULT_UNKNOWN_HOST + + +async def test_dhcp(hass: HomeAssistant, remotews: Mock): + """Test starting a flow from dhcp.""" + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=MOCK_DHCP_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "Living Room (82GXARRS)" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "Living Room" + assert result["data"][CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +async def test_zeroconf(hass: HomeAssistant, remotews: Mock): + """Test starting a flow from zeroconf.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "Living Room (82GXARRS)" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "Living Room" + assert result["data"][CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, remotews_soundbar: Mock): + """Test starting a flow from zeroconf where the device is actually a soundbar.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + +async def test_zeroconf_no_device_info( + hass: HomeAssistant, remotews_no_device_info: Mock +): + """Test starting a flow from zeroconf where device_info returns None.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + +async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant, remotews: Mock): + """Test starting a flow from zeroconf and dhcp.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=MOCK_DHCP_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result2["type"] == "abort" + assert result2["reason"] == "already_in_progress" + + +async def test_autodetect_websocket(hass: HomeAssistant, remote: Mock, remotews: Mock): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", - side_effect=[WebSocketProtocolException("Boom"), DEFAULT_MOCK], + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" ) as remotews: enter = Mock() type(enter).token = PropertyMock(return_value="123456789") remote = Mock() remote.__enter__ = Mock(return_value=enter) remote.__exit__ = Mock(return_value=False) + remote.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "mac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + }, + } remotews.return_value = remote result = await hass.config_entries.flow.async_init( @@ -513,54 +681,61 @@ async def test_autodetect_websocket_ssl(hass, remote, remotews): assert result["data"][CONF_TOKEN] == "123456789" assert remotews.call_count == 2 assert remotews.call_args_list == [ - call(**AUTODETECT_WEBSOCKET_PLAIN), call(**AUTODETECT_WEBSOCKET_SSL), + call(**DEVICEINFO_WEBSOCKET_SSL), ] -async def test_autodetect_auth_missing(hass, remote): +async def test_autodetect_auth_missing(hass: HomeAssistant, remote: Mock): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[AccessDenied("Boom")], - ) as remote: + ) as remote, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "auth_missing" + assert result["reason"] == RESULT_AUTH_MISSING assert remote.call_count == 1 assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -async def test_autodetect_not_supported(hass, remote): +async def test_autodetect_not_supported(hass: HomeAssistant, remote: Mock): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[UnhandledResponse("Boom")], - ) as remote: + ) as remote, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED assert remote.call_count == 1 assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -async def test_autodetect_legacy(hass, remote): +async def test_autodetect_legacy(hass: HomeAssistant, remote: Mock): """Test for send key with autodetection of protocol.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) + print(result) assert result["type"] == "create_entry" assert result["data"][CONF_METHOD] == "legacy" assert remote.call_count == 1 assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -async def test_autodetect_none(hass, remote, remotews): +async def test_autodetect_none(hass: HomeAssistant, remote: Mock, remotews: Mock): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -568,18 +743,228 @@ async def test_autodetect_none(hass, remote, remotews): ) as remote, patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), - ) as remotews: + ) as remotews, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CANNOT_CONNECT assert remote.call_count == 1 assert remote.call_args_list == [ call(AUTODETECT_LEGACY), ] assert remotews.call_count == 2 assert remotews.call_args_list == [ - call(**AUTODETECT_WEBSOCKET_PLAIN), call(**AUTODETECT_WEBSOCKET_SSL), + call(**AUTODETECT_WEBSOCKET_PLAIN), ] + + +async def test_update_old_entry(hass: HomeAssistant, remote: Mock): + """Test update of old entry.""" + with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: + remote().rest_device_info.return_value = { + "device": { + "modelName": "fake_model2", + "name": "[TV] Fake Name", + "udn": "uuid:fake_serial", + } + } + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) + entry.add_to_hass(hass) + + config_entries_domain = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries_domain) == 1 + assert entry is config_entries_domain[0] + assert entry.data[CONF_ID] == "0d1cef00-00dc-1000-9c80-4844f7b172de_old" + assert entry.data[CONF_IP_ADDRESS] == "fake_ip_old" + assert not entry.unique_id + + assert await async_setup_component(hass, DOMAIN, {}) is True + await hass.async_block_till_done() + + # failed as already configured + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == RESULT_ALREADY_CONFIGURED + + config_entries_domain = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries_domain) == 1 + entry2 = config_entries_domain[0] + + # check updated device info + assert entry2.data.get(CONF_ID) is not None + assert entry2.data.get(CONF_IP_ADDRESS) is not None + assert entry2.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + + +async def test_update_missing_mac_unique_id_added_from_dhcp(hass, remotews: Mock): + """Test missing mac and unique id added.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=MOCK_DHCP_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +async def test_update_missing_mac_unique_id_added_from_zeroconf(hass, remotews: Mock): + """Test missing mac and unique id added.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( + hass, remotews: Mock +): + """Test missing mac and unique id added.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_OLD_ENTRY, + unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + + +async def test_form_reauth_legacy(hass, remote: Mock): + """Test reauthenticate legacy.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, + data=entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_form_reauth_websocket(hass, remotews: Mock): + """Test reauthenticate websocket.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, + data=entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): + """Test reauthenticate websocket when we cannot connect on the first attempt.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, + data=entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS", + side_effect=ConnectionFailure, + ), patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + import pprint + + pprint.pprint(result2) + assert result2["type"] == "form" + assert result2["errors"] == {"base": RESULT_AUTH_MISSING} + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "abort" + assert result3["reason"] == "reauth_successful" + + +async def test_form_reauth_websocket_not_supported(hass, remotews: Mock): + """Test reauthenticate websocket when the device is not supported.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, + data=entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS", + side_effect=WebSocketException, + ), patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "not_supported" diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index bb19f120cf6..f728fd4af10 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,21 +1,22 @@ """Tests for the Samsung TV Integration.""" from unittest.mock import Mock, call, patch -import pytest - from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON from homeassistant.components.samsungtv.const import ( CONF_ON_ACTION, DOMAIN as SAMSUNGTV_DOMAIN, + METHOD_WEBSOCKET, ) from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_HOST, + CONF_METHOD, CONF_NAME, SERVICE_VOLUME_UP, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component ENTITY_ID = f"{DOMAIN}.fake_name" @@ -25,6 +26,7 @@ MOCK_CONFIG = { CONF_HOST: "fake_host", CONF_NAME: "fake_name", CONF_ON_ACTION: [{"delay": "00:00:01"}], + CONF_METHOD: METHOD_WEBSOCKET, } ] } @@ -32,37 +34,22 @@ REMOTE_CALL = { "name": "HomeAssistant", "description": "HomeAssistant", "id": "ha.component.samsung", - "method": "legacy", "host": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_HOST], + "method": "legacy", "port": None, "timeout": 1, } -@pytest.fixture(name="remote") -def remote_fixture(): - """Patch the samsungctl Remote.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote" - ) as remote_class, patch( - "homeassistant.components.samsungtv.config_flow.socket" - ) as socket1, patch( - "homeassistant.components.samsungtv.socket" - ) as socket2: - remote = Mock() - remote.__enter__ = Mock() - remote.__exit__ = Mock() - remote_class.return_value = remote - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield remote - - -async def test_setup(hass, remote): +async def test_setup(hass: HomeAssistant, remote: Mock): """Test Samsung TV integration is setup.""" - with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: - await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) - await hass.async_block_till_done() + with patch("homeassistant.components.samsungtv.bridge.Remote") as remote, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) # test name and turn_on @@ -80,7 +67,7 @@ async def test_setup(hass, remote): assert remote.call_args == call(REMOTE_CALL) -async def test_setup_duplicate_config(hass, remote, caplog): +async def test_setup_duplicate_config(hass: HomeAssistant, remote: Mock, caplog): """Test duplicate setup of platform.""" DUPLICATE = { SAMSUNGTV_DOMAIN: [ @@ -95,7 +82,7 @@ async def test_setup_duplicate_config(hass, remote, caplog): assert "duplicate host entries found" in caplog.text -async def test_setup_duplicate_entries(hass, remote, caplog): +async def test_setup_duplicate_entries(hass: HomeAssistant, remote: Mock, caplog): """Test duplicate setup of platform.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 6415f02cdf5..0cf54e32807 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -25,6 +25,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.samsungtv.const import ( CONF_ON_ACTION, DOMAIN as SAMSUNGTV_DOMAIN, + TIMEOUT_WEBSOCKET, ) from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV from homeassistant.const import ( @@ -37,10 +38,12 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_PORT, + CONF_TIMEOUT, CONF_TOKEN, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -49,6 +52,7 @@ from homeassistant.const import ( SERVICE_VOLUME_UP, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -59,7 +63,7 @@ ENTITY_ID = f"{DOMAIN}.fake" MOCK_CONFIG = { SAMSUNGTV_DOMAIN: [ { - CONF_HOST: "fake", + CONF_HOST: "fake_host", CONF_NAME: "fake", CONF_PORT: 55000, CONF_ON_ACTION: [{"delay": "00:00:01"}], @@ -69,7 +73,7 @@ MOCK_CONFIG = { MOCK_CONFIGWS = { SAMSUNGTV_DOMAIN: [ { - CONF_HOST: "fake", + CONF_HOST: "fake_host", CONF_NAME: "fake", CONF_PORT: 8001, CONF_TOKEN: "123456789", @@ -78,27 +82,20 @@ MOCK_CONFIGWS = { ] } MOCK_CALLS_WS = { - "host": "fake", - "port": 8001, - "token": None, - "timeout": 31, - "name": "HomeAssistant", + CONF_HOST: "fake_host", + CONF_PORT: 8001, + CONF_TOKEN: "123456789", + CONF_TIMEOUT: TIMEOUT_WEBSOCKET, + CONF_NAME: "HomeAssistant", } MOCK_ENTRY_WS = { CONF_IP_ADDRESS: "test", - CONF_HOST: "fake", + CONF_HOST: "fake_host", CONF_METHOD: "websocket", CONF_NAME: "fake", CONF_PORT: 8001, - CONF_TOKEN: "abcde", -} -MOCK_CALLS_ENTRY_WS = { - "host": "fake", - "name": "HomeAssistant", - "port": 8001, - "timeout": 8, - "token": "abcde", + CONF_TOKEN: "123456789", } ENTITY_ID_NOTURNON = f"{DOMAIN}.fake_noturnon" @@ -109,45 +106,6 @@ MOCK_CONFIG_NOTURNON = { } -@pytest.fixture(name="remote") -def remote_fixture(): - """Patch the samsungctl Remote.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote" - ) as remote_class, patch( - "homeassistant.components.samsungtv.config_flow.socket" - ) as socket1, patch( - "homeassistant.components.samsungtv.socket" - ) as socket2: - remote = Mock() - remote.__enter__ = Mock() - remote.__exit__ = Mock() - remote_class.return_value = remote - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield remote - - -@pytest.fixture(name="remotews") -def remotews_fixture(): - """Patch the samsungtvws SamsungTVWS.""" - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remote_class, patch( - "homeassistant.components.samsungtv.config_flow.socket" - ) as socket1, patch( - "homeassistant.components.samsungtv.socket" - ) as socket2: - remote = Mock() - remote.__enter__ = Mock() - remote.__exit__ = Mock() - remote_class.return_value = remote - remote_class().__enter__().token = "FAKE_TOKEN" - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield remote - - @pytest.fixture(name="delay") def delay_fixture(): """Patch the delay script function.""" @@ -226,7 +184,7 @@ async def test_setup_websocket_2(hass, mock_now): state = hass.states.get(entity_id) assert state assert remote.call_count == 1 - assert remote.call_args_list == [call(**MOCK_CALLS_ENTRY_WS)] + assert remote.call_args_list == [call(**MOCK_CALLS_WS)] async def test_update_on(hass, remote, mock_now): @@ -272,12 +230,18 @@ async def test_update_access_denied(hass, remote, mock_now): with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + next_update = mock_now + timedelta(minutes=10) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() assert [ flow for flow in hass.config_entries.flow.async_progress() if flow["context"]["source"] == "reauth" ] + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE async def test_update_connection_failure(hass, remotews, mock_now): @@ -296,12 +260,18 @@ async def test_update_connection_failure(hass, remotews, mock_now): with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + next_update = mock_now + timedelta(minutes=10) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() assert [ flow for flow in hass.config_entries.flow.async_progress() if flow["context"]["source"] == "reauth" ] + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE async def test_update_unhandled_response(hass, remote, mock_now): @@ -438,7 +408,8 @@ async def test_state_without_turnon(hass, remote): DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True ) state = hass.states.get(ENTITY_ID_NOTURNON) - assert state.state == STATE_OFF + # Should be STATE_UNAVAILABLE since there is no way to turn it back on + assert state.state == STATE_UNAVAILABLE async def test_supported_features_with_turnon(hass, remote): @@ -555,6 +526,15 @@ async def test_media_play(hass, remote): assert remote.close.call_count == 1 assert remote.close.call_args_list == [call()] + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_PLAY"), call("KEY_PAUSE")] + assert remote.close.call_count == 2 + assert remote.close.call_args_list == [call(), call()] + async def test_media_pause(hass, remote): """Test for media_pause.""" @@ -568,6 +548,15 @@ async def test_media_pause(hass, remote): assert remote.close.call_count == 1 assert remote.close.call_args_list == [call()] + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_PAUSE"), call("KEY_PLAY")] + assert remote.close.call_count == 2 + assert remote.close.call_args_list == [call(), call()] + async def test_media_next_track(hass, remote): """Test for media_next_track.""" From 38d095aa18ccb0901f817409df4a1a8fe0fa3d5b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 18:13:50 +0200 Subject: [PATCH 644/852] Define entity attributes as entity class variables (#50925) * Define entity attributes as entity class variables * Example coronavirus integration * Example verisure * Cleanup/typing fixes * Fix Coronavirus * Revert "Fix Coronavirus" This reverts commit 060843860fe300f8448d0d2476de8963d5ddf5a2. * Revert "Cleanup/typing fixes" This reverts commit 659b79e75a97007f7181064e446c3e988c2d54bb. * Define entity attributes as entity class variables (attr alternative) * Example coronavirus * Example nut * Example verisure * Mark private * Cleanup after all reverting/cherrypicking/merging * Implement all entity properties * Update coronavirus example * Update nut example * Update verisure example * Lets not talk about this one... * Fix multiple class attribute --- .../components/coronavirus/sensor.py | 29 ++++------- homeassistant/components/nut/sensor.py | 31 ++--------- .../verisure/alarm_control_panel.py | 21 ++------ .../components/verisure/binary_sensor.py | 30 +++-------- homeassistant/components/verisure/camera.py | 13 ++--- homeassistant/components/verisure/lock.py | 14 ++--- homeassistant/components/verisure/sensor.py | 51 ++++--------------- homeassistant/components/verisure/switch.py | 14 ++--- homeassistant/helpers/entity.py | 50 ++++++++++++------ 9 files changed, 80 insertions(+), 173 deletions(-) diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index 472b8bc8d1c..b467a5fee12 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -27,17 +27,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class CoronavirusSensor(CoordinatorEntity, SensorEntity): """Sensor representing corona virus data.""" - name = None - unique_id = None + _attr_unit_of_measurement = "people" def __init__(self, coordinator, country, info_type): """Initialize coronavirus sensor.""" super().__init__(coordinator) + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attr_icon = SENSORS[info_type] + self._attr_unique_id = f"{country}-{info_type}" if country == OPTION_WORLDWIDE: - self.name = f"Worldwide Coronavirus {info_type}" + self._attr_name = f"Worldwide Coronavirus {info_type}" else: - self.name = f"{coordinator.data[country].country} Coronavirus {info_type}" - self.unique_id = f"{country}-{info_type}" + self._attr_name = ( + f"{coordinator.data[country].country} Coronavirus {info_type}" + ) + self.country = country self.info_type = info_type @@ -62,18 +66,3 @@ class CoronavirusSensor(CoordinatorEntity, SensorEntity): return sum_cases return getattr(self.coordinator.data[self.country], self.info_type) - - @property - def icon(self): - """Return the icon.""" - return SENSORS[self.info_type] - - @property - def unit_of_measurement(self): - """Return unit of measurement.""" - return "people" - - @property - def extra_state_attributes(self): - """Return device attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 2e3826935fe..1eb67e45aa5 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -98,11 +98,14 @@ class NUTSensor(CoordinatorEntity, SensorEntity): self._firmware = firmware self._model = model self._device_name = name - self._name = f"{name} {SENSOR_TYPES[sensor_type][SENSOR_NAME]}" - self._unit = SENSOR_TYPES[sensor_type][SENSOR_UNIT] self._data = data self._unique_id = unique_id + self._attr_device_class = SENSOR_TYPES[self._type][SENSOR_DEVICE_CLASS] + self._attr_icon = SENSOR_TYPES[self._type][SENSOR_ICON] + self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][SENSOR_NAME]}" + self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][SENSOR_UNIT] + @property def device_info(self): """Device info for the ups.""" @@ -127,25 +130,6 @@ class NUTSensor(CoordinatorEntity, SensorEntity): return None return f"{self._unique_id}_{self._type}" - @property - def name(self): - """Return the name of the UPS sensor.""" - return self._name - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - if SENSOR_TYPES[self._type][SENSOR_DEVICE_CLASS]: - # The UI will assign an icon - # if it has a class - return None - return SENSOR_TYPES[self._type][SENSOR_ICON] - - @property - def device_class(self): - """Device class of the sensor.""" - return SENSOR_TYPES[self._type][SENSOR_DEVICE_CLASS] - @property def state(self): """Return entity state from ups.""" @@ -155,11 +139,6 @@ class NUTSensor(CoordinatorEntity, SensorEntity): return _format_display_state(self._data.status) return self._data.status.get(self._type) - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit - @property def extra_state_attributes(self): """Return the sensor attributes.""" diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 64ee024dfd7..3c77541cea8 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -35,18 +35,10 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): coordinator: VerisureDataUpdateCoordinator + _attr_name = "Verisure Alarm" + _attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + _changed_by: str | None = None - _state: str | None = None - - @property - def name(self) -> str: - """Return the name of the entity.""" - return "Verisure Alarm" - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return self.coordinator.entry.data[CONF_GIID] @property def device_info(self) -> DeviceInfo: @@ -58,11 +50,6 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): "identifiers": {(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, } - @property - def state(self) -> str | None: - """Return the state of the entity.""" - return self._state - @property def supported_features(self) -> int: """Return the list of supported features.""" @@ -109,7 +96,7 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._state = ALARM_STATE_TO_HA.get( + self._attr_state = ALARM_STATE_TO_HA.get( self.coordinator.data["alarm"]["statusType"] ) self._changed_by = self.coordinator.data["alarm"].get("name") diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index ab79c6f45fe..4d9b1e84770 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -39,23 +39,17 @@ class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): coordinator: VerisureDataUpdateCoordinator + _attr_device_class = DEVICE_CLASS_OPENING + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure door window sensor.""" super().__init__(coordinator) + self._attr_name = coordinator.data["door_window"][serial_number]["area"] + self._attr_unique_id = f"{serial_number}_door_window" self.serial_number = serial_number - @property - def name(self) -> str: - """Return the name of this entity.""" - return self.coordinator.data["door_window"][self.serial_number]["area"] - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return f"{self.serial_number}_door_window" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -69,11 +63,6 @@ class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), } - @property - def device_class(self) -> str: - """Return the class of this entity.""" - return DEVICE_CLASS_OPENING - @property def is_on(self) -> bool: """Return the state of the sensor.""" @@ -95,10 +84,8 @@ class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): coordinator: VerisureDataUpdateCoordinator - @property - def name(self) -> str: - """Return the name of this entity.""" - return "Verisure Ethernet status" + _attr_name = "Verisure Ethernet status" + _attr_device_class = DEVICE_CLASS_CONNECTIVITY @property def unique_id(self) -> str: @@ -124,8 +111,3 @@ class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): def available(self) -> bool: """Return True if entity is available.""" return super().available and self.coordinator.data["ethernet"] is not None - - @property - def device_class(self) -> str: - """Return the class of this entity.""" - return DEVICE_CLASS_CONNECTIVITY diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 0dfda45999a..a137f61d98f 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -58,21 +58,14 @@ class VerisureSmartcam(CoordinatorEntity, Camera): super().__init__(coordinator) Camera.__init__(self) + self._attr_name = coordinator.data["cameras"][serial_number]["area"] + self._attr_unique_id = serial_number + self.serial_number = serial_number self._directory_path = directory_path self._image = None self._image_id = None - @property - def name(self) -> str: - """Return the name of this entity.""" - return self.coordinator.data["cameras"][self.serial_number]["area"] - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return self.serial_number - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index c33ccda208a..e645bf3f8c1 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -65,22 +65,16 @@ class VerisureDoorlock(CoordinatorEntity, LockEntity): ) -> None: """Initialize the Verisure lock.""" super().__init__(coordinator) + + self._attr_name = coordinator.data["locks"][serial_number]["area"] + self._attr_unique_id = serial_number + self.serial_number = serial_number self._state = None self._digits = coordinator.entry.options.get( CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS ) - @property - def name(self) -> str: - """Return the name of this entity.""" - return self.coordinator.data["locks"][self.serial_number]["area"] - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return self.serial_number - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 028e5877e51..d39c235e9d5 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -50,11 +50,15 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): coordinator: VerisureDataUpdateCoordinator + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_unit_of_measurement = TEMP_CELSIUS + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + self._attr_unique_id = f"{serial_number}_temperature" self.serial_number = serial_number @property @@ -63,16 +67,6 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): name = self.coordinator.data["climate"][self.serial_number]["deviceArea"] return f"{name} Temperature" - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return f"{self.serial_number}_temperature" - - @property - def device_class(self) -> str: - """Return the class of this entity.""" - return DEVICE_CLASS_TEMPERATURE - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -103,22 +97,21 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): and "temperature" in self.coordinator.data["climate"][self.serial_number] ) - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity.""" - return TEMP_CELSIUS - class VerisureHygrometer(CoordinatorEntity, SensorEntity): """Representation of a Verisure hygrometer.""" coordinator: VerisureDataUpdateCoordinator + _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_unit_of_measurement = PERCENTAGE + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + self._attr_unique_id = f"{serial_number}_humidity" self.serial_number = serial_number @property @@ -127,16 +120,6 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): name = self.coordinator.data["climate"][self.serial_number]["deviceArea"] return f"{name} Humidity" - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return f"{self.serial_number}_humidity" - - @property - def device_class(self) -> str: - """Return the class of this entity.""" - return DEVICE_CLASS_HUMIDITY - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -167,22 +150,20 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): and "humidity" in self.coordinator.data["climate"][self.serial_number] ) - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity.""" - return PERCENTAGE - class VerisureMouseDetection(CoordinatorEntity, SensorEntity): """Representation of a Verisure mouse detector.""" coordinator: VerisureDataUpdateCoordinator + _attr_unit_of_measurement = "Mice" + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + self._attr_unique_id = f"{serial_number}_mice" self.serial_number = serial_number @property @@ -191,11 +172,6 @@ class VerisureMouseDetection(CoordinatorEntity, SensorEntity): name = self.coordinator.data["mice"][self.serial_number]["area"] return f"{name} Mouse" - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return f"{self.serial_number}_mice" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -222,8 +198,3 @@ class VerisureMouseDetection(CoordinatorEntity, SensorEntity): and self.serial_number in self.coordinator.data["mice"] and "detections" in self.coordinator.data["mice"][self.serial_number] ) - - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity.""" - return "Mice" diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 97b2ff0186a..f428f70cda6 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -37,20 +37,14 @@ class VerisureSmartplug(CoordinatorEntity, SwitchEntity): ) -> None: """Initialize the Verisure device.""" super().__init__(coordinator) + + self._attr_name = coordinator.data["smart_plugs"][serial_number]["area"] + self._attr_unique_id = serial_number + self.serial_number = serial_number self._change_timestamp = 0 self._state = False - @property - def name(self) -> str: - """Return the name of this entity.""" - return self.coordinator.data["smart_plugs"][self.serial_number]["area"] - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return self.serial_number - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 2e2c1b3b3f9..724280b19c9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -168,28 +168,46 @@ class Entity(ABC): # If entity is added to an entity platform _added = False + # Entity Properties + _attr_assumed_state: bool = False + _attr_available: bool = True + _attr_context_recent_time: timedelta = timedelta(seconds=5) + _attr_device_class: str | None = None + _attr_device_info: DeviceInfo | None = None + _attr_entity_picture: str | None = None + _attr_entity_registry_enabled_default: bool = True + _attr_extra_state_attributes: Mapping[str, Any] | None = None + _attr_force_update: bool = False + _attr_icon: str | None = None + _attr_name: str | None = None + _attr_should_poll: bool = True + _attr_state: StateType = STATE_UNKNOWN + _attr_supported_features: int | None = None + _attr_unique_id: str | None = None + _attr_unit_of_measurement: str | None = None + @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 True + return self._attr_should_poll @property def unique_id(self) -> str | None: """Return a unique ID.""" - return None + return self._attr_unique_id @property def name(self) -> str | None: """Return the name of the entity.""" - return None + return self._attr_name @property def state(self) -> StateType: """Return the state of the entity.""" - return STATE_UNKNOWN + return self._attr_state @property def capability_attributes(self) -> Mapping[str, Any] | None: @@ -227,7 +245,7 @@ class Entity(ABC): Implemented by platform classes. Convention for attribute names is lowercase snake_case. """ - return None + return self._attr_extra_state_attributes @property def device_info(self) -> DeviceInfo | None: @@ -235,37 +253,37 @@ class Entity(ABC): Implemented by platform classes. """ - return None + return self._attr_device_info @property def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" - return None + return self._attr_device_class @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" - return None + return self._attr_unit_of_measurement @property def icon(self) -> str | None: """Return the icon to use in the frontend, if any.""" - return None + return self._attr_icon @property def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend, if any.""" - return None + return self._attr_entity_picture @property def available(self) -> bool: """Return True if entity is available.""" - return True + return self._attr_available @property def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" - return False + return self._attr_assumed_state @property def force_update(self) -> bool: @@ -274,22 +292,22 @@ class Entity(ABC): If True, a state change will be triggered anytime the state property is updated, not just when the value changes. """ - return False + return self._attr_force_update @property def supported_features(self) -> int | None: """Flag supported features.""" - return None + return self._attr_supported_features @property def context_recent_time(self) -> timedelta: """Time that a context is considered recent.""" - return timedelta(seconds=5) + return self._attr_context_recent_time @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return True + return self._attr_entity_registry_enabled_default # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they From f7bc456bd24780dee2741c3cf7021edafdecf638 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 18:17:35 +0200 Subject: [PATCH 645/852] Define sensor entity attributes as class variables (#50942) --- homeassistant/components/sensor/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index a0532a65cf3..2857a3cc5c2 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -96,15 +96,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class SensorEntity(Entity): """Base class for sensor entities.""" + _attr_state_class: str | None = None + _attr_last_reset: datetime | None = None + @property def state_class(self) -> str | None: """Return the state class of this entity, from STATE_CLASSES, if any.""" - return None + return self._attr_state_class @property def last_reset(self) -> datetime | None: """Return the time when the sensor was last reset, if any.""" - return None + return self._attr_last_reset @property def capability_attributes(self) -> Mapping[str, Any] | None: From d3bc2bc47f30cb6f33aca6126a06b3ad102b8351 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 18:20:34 +0200 Subject: [PATCH 646/852] Define binary_sensor entity attribute as class variables (#50940) --- homeassistant/components/binary_sensor/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index f022509f9de..9bf53407b38 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -1,4 +1,5 @@ """Component to interface with binary sensors.""" +from __future__ import annotations from datetime import timedelta import logging @@ -12,6 +13,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import StateType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -147,21 +149,18 @@ async def async_unload_entry(hass, entry): class BinarySensorEntity(Entity): """Represent a binary sensor.""" + _attr_is_on: bool | None = None + @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return None @property - def state(self): + def state(self) -> StateType: """Return the state of the binary sensor.""" return STATE_ON if self.is_on else STATE_OFF - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return None - class BinarySensorDevice(BinarySensorEntity): """Represent a binary sensor (for backwards compatibility).""" From cad4ec867b66ba64b6f12664f78dd2aced6a7237 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 18:21:31 +0200 Subject: [PATCH 647/852] Define light entity attributes as class variables (#50941) --- homeassistant/components/light/__init__.py | 43 +++++++++++++++------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 8cb35a8bffe..83361380918 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -559,15 +559,30 @@ class Profiles: class LightEntity(ToggleEntity): """Base class for light entities.""" + _attr_brightness: int | None = None + _attr_color_mode: str | None = None + _attr_color_temp: int | None = None + _attr_effect_list: list[str] | None = None + _attr_effect: str | None = None + _attr_hs_color: tuple[float, float] | None = None + _attr_max_mired: int = 500 + _attr_min_mired: int = 153 + _attr_rgb_color: tuple[int, int, int] | None = None + _attr_rgbw_color: tuple[int, int, int, int] | None = None + _attr_rgbww_color: tuple[int, int, int, int, int] | None = None + _attr_supported_color_modes: set | None = None + _attr_supported_features: int = 0 + _attr_xy_color: tuple[float, float] | None = None + @property def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" - return None + return self._attr_brightness @property def color_mode(self) -> str | None: """Return the color mode of the light.""" - return None + return self._attr_color_mode @property def _light_internal_color_mode(self) -> str: @@ -600,22 +615,22 @@ class LightEntity(ToggleEntity): @property def hs_color(self) -> tuple[float, float] | None: """Return the hue and saturation color value [float, float].""" - return None + return self._attr_hs_color @property def xy_color(self) -> tuple[float, float] | None: """Return the xy color value [float, float].""" - return None + return self._attr_xy_color @property def rgb_color(self) -> tuple[int, int, int] | None: """Return the rgb color value [int, int, int].""" - return None + return self._attr_rgb_color @property def rgbw_color(self) -> tuple[int, int, int, int] | None: """Return the rgbw color value [int, int, int, int].""" - return None + return self._attr_rgbw_color @property def _light_internal_rgbw_color(self) -> tuple[int, int, int, int] | None: @@ -639,26 +654,26 @@ class LightEntity(ToggleEntity): @property def rgbww_color(self) -> tuple[int, int, int, int, int] | None: """Return the rgbww color value [int, int, int, int, int].""" - return None + return self._attr_rgbww_color @property def color_temp(self) -> int | None: """Return the CT color value in mireds.""" - return None + return self._attr_color_temp @property def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" # Default to the Philips Hue value that HA has always assumed # https://developers.meethue.com/documentation/core-concepts - return 153 + return self._attr_min_mired @property def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" # Default to the Philips Hue value that HA has always assumed # https://developers.meethue.com/documentation/core-concepts - return 500 + return self._attr_max_mired @property def white_value(self) -> int | None: @@ -668,12 +683,12 @@ class LightEntity(ToggleEntity): @property def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" - return None + return self._attr_effect_list @property def effect(self) -> str | None: """Return the current effect.""" - return None + return self._attr_effect @property def capability_attributes(self): @@ -808,12 +823,12 @@ class LightEntity(ToggleEntity): @property def supported_color_modes(self) -> set | None: """Flag supported color modes.""" - return None + return self._attr_supported_color_modes @property def supported_features(self) -> int: """Flag supported features.""" - return 0 + return self._attr_supported_features class Light(LightEntity): From e64b5afa58006b6e25faf50ef2bef63c7b9522d0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 19:09:19 +0200 Subject: [PATCH 648/852] Typing improvements for Sentry (#50787) * Typing improvements for Sentry * Fix event tags access * Fix tests --- homeassistant/components/sentry/__init__.py | 12 +++-- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - tests/components/sentry/conftest.py | 10 ++-- tests/components/sentry/test_config_flow.py | 51 ++++++++++++--------- tests/components/sentry/test_init.py | 14 ++---- 6 files changed, 48 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 3cd209dfaf3..37941a0bcaa 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations import re +from types import MappingProxyType +from typing import Any import sentry_sdk from sentry_sdk.integrations.aiohttp import AioHttpIntegration @@ -120,19 +122,19 @@ def get_channel(version: str) -> str: def process_before_send( hass: HomeAssistant, - options, + options: MappingProxyType[str, Any], channel: str, huuid: str, system_info: dict[str, bool | str], custom_components: dict[str, Integration], - event, - hint, + event: dict[str, Any], + hint: dict[str, Any], ): """Process a Sentry event before sending it to Sentry.""" # Filter out handled events by default if ( "tags" in event - and event.tags.get("handled", "no") == "yes" + and event["tags"].get("handled", "no") == "yes" and not options.get(CONF_EVENT_HANDLED) ): return None @@ -204,7 +206,7 @@ def process_before_send( "channel": channel, "custom_components": "\n".join(sorted(custom_components)), "integrations": "\n".join(sorted(integrations)), - **system_info, + **system_info, # type: ignore[arg-type] }, } ) diff --git a/mypy.ini b/mypy.ini index aea1073d4f2..bb75b29fa6d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1267,9 +1267,6 @@ ignore_errors = true [mypy-homeassistant.components.sense.*] ignore_errors = true -[mypy-homeassistant.components.sentry.*] -ignore_errors = true - [mypy-homeassistant.components.sesame.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 820ab7b814a..1b49dd023f7 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -176,7 +176,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.script.*", "homeassistant.components.search.*", "homeassistant.components.sense.*", - "homeassistant.components.sentry.*", "homeassistant.components.sesame.*", "homeassistant.components.sharkiq.*", "homeassistant.components.sma.*", diff --git a/tests/components/sentry/conftest.py b/tests/components/sentry/conftest.py index 77da4119166..a7347d44bab 100644 --- a/tests/components/sentry/conftest.py +++ b/tests/components/sentry/conftest.py @@ -1,4 +1,8 @@ -"""Configuration for Sonos tests.""" +"""Configuration for Sentry tests.""" +from __future__ import annotations + +from typing import Any + import pytest from homeassistant.components.sentry import DOMAIN @@ -7,12 +11,12 @@ from tests.common import MockConfigEntry @pytest.fixture(name="config_entry") -def config_entry_fixture(): +def config_entry_fixture() -> MockConfigEntry: """Create a mock config entry.""" return MockConfigEntry(domain=DOMAIN, title="Sentry") @pytest.fixture(name="config") -def config_fixture(): +def config_fixture() -> dict[str, Any]: """Create hass config fixture.""" return {DOMAIN: {"dsn": "http://public@sentry.local/1"}} diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 259d8c65e16..2246cabe33a 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -16,20 +16,26 @@ from homeassistant.components.sentry.const import ( DOMAIN, ) from homeassistant.config_entries import SOURCE_USER -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -async def test_full_user_flow_implementation(hass): +async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: """Test we get the form.""" await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert "flow_id" in result with patch("homeassistant.components.sentry.config_flow.Dsn"), patch( "homeassistant.components.sentry.async_setup_entry", @@ -40,9 +46,9 @@ async def test_full_user_flow_implementation(hass): {"dsn": "http://public@sentry.local/1"}, ) - assert result2["type"] == "create_entry" - assert result2["title"] == "Sentry" - assert result2["data"] == { + assert result2.get("type") == "create_entry" + assert result2.get("title") == "Sentry" + assert result2.get("data") == { "dsn": "http://public@sentry.local/1", } await hass.async_block_till_done() @@ -50,22 +56,23 @@ async def test_full_user_flow_implementation(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_integration_already_exists(hass): +async def test_integration_already_exists(hass: HomeAssistant) -> None: """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": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "single_instance_allowed" -async def test_user_flow_bad_dsn(hass): +async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None: """Test we handle bad dsn error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) + assert "flow_id" in result with patch( "homeassistant.components.sentry.config_flow.Dsn", @@ -76,15 +83,16 @@ async def test_user_flow_bad_dsn(hass): {"dsn": "foo"}, ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "bad_dsn"} + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("errors") == {"base": "bad_dsn"} -async def test_user_flow_unkown_exception(hass): +async def test_user_flow_unkown_exception(hass: HomeAssistant) -> None: """Test we handle any unknown exception error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) + assert "flow_id" in result with patch( "homeassistant.components.sentry.config_flow.Dsn", @@ -95,11 +103,11 @@ async def test_user_flow_unkown_exception(hass): {"dsn": "foo"}, ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "unknown"} + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("errors") == {"base": "unknown"} -async def test_options_flow(hass): +async def test_options_flow(hass: HomeAssistant) -> None: """Test options config flow.""" entry = MockConfigEntry( domain=DOMAIN, @@ -113,8 +121,9 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "init" + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "init" + assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -130,8 +139,8 @@ async def test_options_flow(hass): }, ) - assert result["type"] == "create_entry" - assert result["data"] == { + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("data") == { CONF_ENVIRONMENT: "Test", CONF_EVENT_CUSTOM_COMPONENTS: True, CONF_EVENT_HANDLED: True, diff --git a/tests/components/sentry/test_init.py b/tests/components/sentry/test_init.py index e920437b2f7..206018f50a5 100644 --- a/tests/components/sentry/test_init.py +++ b/tests/components/sentry/test_init.py @@ -1,6 +1,6 @@ """Tests for Sentry integration.""" import logging -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import Mock, patch import pytest @@ -112,12 +112,12 @@ async def test_setup_entry_with_tracing(hass: HomeAssistant) -> None: ("0.115.0dev0", "dev"), ], ) -async def test_get_channel(version, channel) -> None: +async def test_get_channel(version: str, channel: str) -> None: """Test if channel detection works from Home Assistant version number.""" assert get_channel(version) == channel -async def test_process_before_send(hass: HomeAssistant): +async def test_process_before_send(hass: HomeAssistant) -> None: """Test regular use of the Sentry process before sending function.""" hass.config.components.add("puppies") hass.config.components.add("a_integration") @@ -308,12 +308,6 @@ async def test_filter_log_events(hass: HomeAssistant, logger, options, event): ) async def test_filter_handled_events(hass: HomeAssistant, handled, options, event): """Tests filtering of handled events based on configuration options.""" - - event_mock = MagicMock() - event_mock.__iter__ = ["tags"] - event_mock.__contains__ = lambda _, val: val == "tags" - event_mock.tags = {"handled": handled} - result = process_before_send( hass, options=options, @@ -321,7 +315,7 @@ async def test_filter_handled_events(hass: HomeAssistant, handled, options, even huuid="12345", system_info={"installation_type": "pytest"}, custom_components=[], - event=event_mock, + event={"tags": {"handled": handled}}, hint={}, ) From 3c8707f912eeaa2103c0ea5678c4ddaa795f7b8a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 19:16:11 +0200 Subject: [PATCH 649/852] Fix tcp typing, fixing CI (#50965) --- homeassistant/components/tcp/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index 84f92582947..ff5db39bba7 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -29,7 +29,7 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, StateType from .const import ( CONF_BUFFER_SIZE, @@ -112,7 +112,7 @@ class TcpSensor(SensorEntity): return self._config[CONF_NAME] @property - def state(self) -> str | None: + def state(self) -> StateType: """Return the state of the device.""" return self._state From b9086b5e39ecfd5250fbb487dde4b9752aa9bd55 Mon Sep 17 00:00:00 2001 From: Matej Plavevski Date: Sat, 22 May 2021 19:31:00 +0200 Subject: [PATCH 650/852] Fix Documentation leading to a 404 Page (#50962) --- homeassistant/components/vlc_telnet/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json index 1aa41fb9bb9..d03e9163961 100644 --- a/homeassistant/components/vlc_telnet/manifest.json +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "vlc_telnet", "name": "VLC media player Telnet", - "documentation": "https://www.home-assistant.io/integrations/vlc-telnet", + "documentation": "https://www.home-assistant.io/integrations/vlc_telnet", "requirements": ["python-telnet-vlc==2.0.1"], "codeowners": ["@rodripf", "@dmcc"], "iot_class": "local_polling" From 99e58f3c18939a12ec3777341913a141b4fa2c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9n?= Date: Sat, 22 May 2021 19:31:49 +0200 Subject: [PATCH 651/852] Fix coinbase response pagination (#50890) * Fix issue #50500 * next is a python keyword --- homeassistant/components/coinbase/__init__.py | 18 ++++++++++++++++-- homeassistant/components/coinbase/sensor.py | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 9fd99e993b6..5bcd330c9bb 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -57,7 +57,7 @@ def setup(hass, config): if not hasattr(coinbase_data, "accounts"): return False - for account in coinbase_data.accounts.data: + for account in coinbase_data.accounts: if account_currencies is None or account.currency in account_currencies: load_platform(hass, "sensor", DOMAIN, {"account": account}, config) for currency in exchange_currencies: @@ -90,7 +90,21 @@ class CoinbaseData: """Get the latest data from coinbase.""" try: - self.accounts = self.client.get_accounts() + response = self.client.get_accounts() + accounts = response["data"] + + # Most of Coinbase's API seems paginated now (25 items per page, but first page has 24). + # This API gives a 'next_starting_after' property to send back as a 'starting_after' param. + # Their API documentation is not up to date when writing these lines (2021-05-20) + next_starting_after = response.pagination.next_starting_after + + while next_starting_after: + response = self.client.get_accounts(starting_after=next_starting_after) + accounts = accounts + response["data"] + next_starting_after = response.pagination.next_starting_after + + self.accounts = accounts + self.exchange_rates = self.client.get_exchange_rates() except AuthenticationError as coinbase_error: _LOGGER.error( diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index e4e4e719c9e..3a0e689862f 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -81,7 +81,7 @@ class AccountSensor(SensorEntity): def update(self): """Get the latest state of the sensor.""" self._coinbase_data.update() - for account in self._coinbase_data.accounts["data"]: + for account in self._coinbase_data.accounts: if self._name == f"Coinbase {account['name']}": self._state = account["balance"]["amount"] self._native_balance = account["native_balance"]["amount"] From a03ee1e5284a8e3745e89363baa7bdbdd397a1d4 Mon Sep 17 00:00:00 2001 From: victorclaessen Date: Sat, 22 May 2021 19:33:02 +0200 Subject: [PATCH 652/852] Fix Volvo On Call lock state (#50832) * Fix Volvo On Call: Locked shows as Unlocked #43260 and #49224 * Update binary_sensor.py Black changed 'Door lock' to "Door lock" (double quotes) * Update homeassistant/components/volvooncall/binary_sensor.py Co-authored-by: Martin Hjelmare * Update binary_sensor.py Amend code to pass pylint test Co-authored-by: Martin Hjelmare --- homeassistant/components/volvooncall/binary_sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/volvooncall/binary_sensor.py b/homeassistant/components/volvooncall/binary_sensor.py index f08b4509bd1..7d2a9660703 100644 --- a/homeassistant/components/volvooncall/binary_sensor.py +++ b/homeassistant/components/volvooncall/binary_sensor.py @@ -16,7 +16,9 @@ class VolvoSensor(VolvoEntity, BinarySensorEntity): @property def is_on(self): - """Return True if the binary sensor is on.""" + """Return True if the binary sensor is on, but invert for the 'Door lock'.""" + if self.instrument.attr == "is_locked": + return not self.instrument.is_on return self.instrument.is_on @property From 4948bec8d556638b7821a350d85e6ba1e3fe2cee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 19:45:05 +0200 Subject: [PATCH 653/852] Fix is_on attr not being used in binary sensor (#50968) --- homeassistant/components/binary_sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 9bf53407b38..c7e1bac9952 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -154,7 +154,7 @@ class BinarySensorEntity(Entity): @property def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" - return None + return self._attr_is_on @property def state(self) -> StateType: From f4289b3fca0ff47311f645e7ca44961323f36388 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 May 2021 19:48:58 +0200 Subject: [PATCH 654/852] Improve supported_color_modes typing in Light (#50969) --- homeassistant/components/light/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 83361380918..bd1f21f8ecb 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -570,7 +570,7 @@ class LightEntity(ToggleEntity): _attr_rgb_color: tuple[int, int, int] | None = None _attr_rgbw_color: tuple[int, int, int, int] | None = None _attr_rgbww_color: tuple[int, int, int, int, int] | None = None - _attr_supported_color_modes: set | None = None + _attr_supported_color_modes: set[str] | None = None _attr_supported_features: int = 0 _attr_xy_color: tuple[float, float] | None = None @@ -821,7 +821,7 @@ class LightEntity(ToggleEntity): return supported_color_modes @property - def supported_color_modes(self) -> set | None: + def supported_color_modes(self) -> set[str] | None: """Flag supported color modes.""" return self._attr_supported_color_modes From 45897b59f2a746656fe396ef0c127d05bb7e67de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 May 2021 15:33:37 -0500 Subject: [PATCH 655/852] Turn on samsungtv with wakeonlan (#50964) If we have the mac address from discovery, we can use it to wake the TV. Currently the TV goes unavailable when you turn it off as the only way to turn it back on is wake on lan or via the remote. Users who are not using host networking can use a script instead. --- .../components/samsungtv/manifest.json | 3 +- .../components/samsungtv/media_player.py | 15 +++++++-- requirements_all.txt | 1 + requirements_test_all.txt | 1 + .../components/samsungtv/test_media_player.py | 32 +++++++++++++++++++ 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 4206aca7213..4ffe940f946 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -4,7 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ "samsungctl[websocket]==0.7.1", - "samsungtvws==1.6.0" + "samsungtvws==1.6.0", + "wakeonlan==2.0.1" ], "ssdp": [ { diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 72e21ed205c..5822bafcc55 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta import voluptuous as vol +from wakeonlan import send_magic_packet from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity from homeassistant.components.media_player.const import ( @@ -71,6 +72,7 @@ class SamsungTVDevice(MediaPlayerEntity): def __init__(self, bridge, config_entry, on_script): """Initialize the Samsung device.""" self._config_entry = config_entry + self._host = config_entry.data[CONF_HOST] self._mac = config_entry.data.get(CONF_MAC) self._manufacturer = config_entry.data.get(CONF_MANUFACTURER) self._model = config_entry.data.get(CONF_MODEL) @@ -146,7 +148,7 @@ class SamsungTVDevice(MediaPlayerEntity): """Return the availability of the device.""" if self._auth_failed: return False - return self._state == STATE_ON or self._on_script + return self._state == STATE_ON or self._on_script or self._mac @property def device_info(self): @@ -174,7 +176,7 @@ class SamsungTVDevice(MediaPlayerEntity): @property def supported_features(self): """Flag media player features that are supported.""" - if self._on_script: + if self._on_script or self._mac: return SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON return SUPPORT_SAMSUNGTV @@ -246,10 +248,19 @@ class SamsungTVDevice(MediaPlayerEntity): await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop) await self.hass.async_add_executor_job(self.send_key, "KEY_ENTER") + def _wake_on_lan(self): + """Wake the device via wake on lan.""" + send_magic_packet(self._mac, ip_address=self._host) + # If the ip address changed since we last saw the device + # broadcast a packet as well + send_magic_packet(self._mac) + async def async_turn_on(self): """Turn the media player on.""" if self._on_script: await self._on_script.async_run(context=self._context) + elif self._mac: + await self.hass.async_add_executor_job(self._wake_on_lan) def select_source(self, source): """Select input source.""" diff --git a/requirements_all.txt b/requirements_all.txt index 656a4f7da8d..18365ca14cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2325,6 +2325,7 @@ vtjp==0.1.14 # homeassistant.components.vultr vultr==0.1.2 +# homeassistant.components.samsungtv # homeassistant.components.wake_on_lan wakeonlan==2.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2688ffabed1..e90799028e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1249,6 +1249,7 @@ vsure==1.7.3 # homeassistant.components.vultr vultr==0.1.2 +# homeassistant.components.samsungtv # homeassistant.components.wake_on_lan wakeonlan==2.0.1 diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 0cf54e32807..02eceeaacb7 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -35,6 +35,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_HOST, CONF_IP_ADDRESS, + CONF_MAC, CONF_METHOD, CONF_NAME, CONF_PORT, @@ -98,6 +99,17 @@ MOCK_ENTRY_WS = { CONF_TOKEN: "123456789", } + +MOCK_ENTRY_WS_WITH_MAC = { + CONF_IP_ADDRESS: "test", + CONF_HOST: "fake_host", + CONF_METHOD: "websocket", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "fake", + CONF_PORT: 8002, + CONF_TOKEN: "123456789", +} + ENTITY_ID_NOTURNON = f"{DOMAIN}.fake_noturnon" MOCK_CONFIG_NOTURNON = { SAMSUNGTV_DOMAIN: [ @@ -593,6 +605,26 @@ async def test_turn_on_with_turnon(hass, remote, delay): assert delay.call_count == 1 +async def test_turn_on_wol(hass, remotews): + """Test turn on.""" + entry = MockConfigEntry( + domain=SAMSUNGTV_DOMAIN, + data=MOCK_ENTRY_WS_WITH_MAC, + unique_id="any", + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {}) + await hass.async_block_till_done() + with patch( + "homeassistant.components.samsungtv.media_player.send_magic_packet" + ) as mock_send_magic_packet: + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + await hass.async_block_till_done() + assert mock_send_magic_packet.called + + async def test_turn_on_without_turnon(hass, remote): """Test turn on.""" await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) From c7ada1e8f62852c14d98150ce914755a30c30ac4 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 23 May 2021 02:11:02 +0300 Subject: [PATCH 656/852] Fix flaky Shelly config flow test (#50982) --- tests/components/shelly/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 51659cf7736..71157124806 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -66,7 +66,7 @@ MOCK_SHELLY = { @pytest.fixture(autouse=True) def mock_coap(): """Mock out coap.""" - with patch("homeassistant.components.shelly.get_coap_context"): + with patch("homeassistant.components.shelly.utils.get_coap_context"): yield From 460092ec9a5866f34c3146c3f4df2039a8a7b5f4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 23 May 2021 00:13:25 +0000 Subject: [PATCH 657/852] [ci skip] Translation update --- .../components/bosch_shc/translations/de.json | 23 +++++++++++++++++++ .../enphase_envoy/translations/de.json | 2 +- .../components/fritz/translations/de.json | 9 +++++--- .../garages_amsterdam/translations/de.json | 9 ++++++++ .../components/goalzero/translations/de.json | 4 +++- .../growatt_server/translations/de.json | 3 ++- .../components/kraken/translations/de.json | 10 ++++++++ .../rainmachine/translations/de.json | 1 + .../components/samsungtv/translations/en.json | 3 ++- .../components/samsungtv/translations/et.json | 17 ++++++++++---- .../samsungtv/translations/zh-Hant.json | 17 ++++++++++---- .../screenlogic/translations/de.json | 2 +- .../system_bridge/translations/de.json | 1 + .../components/zha/translations/de.json | 2 +- 14 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/bosch_shc/translations/de.json create mode 100644 homeassistant/components/garages_amsterdam/translations/de.json diff --git a/homeassistant/components/bosch_shc/translations/de.json b/homeassistant/components/bosch_shc/translations/de.json new file mode 100644 index 00000000000..8d3e47d0ab6 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth_confirm": { + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/de.json b/homeassistant/components/enphase_envoy/translations/de.json index b1fb53829ad..6b3ed588042 100644 --- a/homeassistant/components/enphase_envoy/translations/de.json +++ b/homeassistant/components/enphase_envoy/translations/de.json @@ -9,7 +9,7 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index 6a33938ba35..d620396f1b5 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -8,7 +8,7 @@ "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "connection_error": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, @@ -27,7 +27,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Aktualisiere die Anmeldeinformationen von FRITZ! Box Tools f\u00fcr: {host} . \n\nFRITZ! Box Tools kann sich nicht bei deiner FRITZ! Box anmelden.", + "description": "Aktualisiere die Anmeldeinformationen von FRITZ! Box Tools f\u00fcr: {host}. \n\nFRITZ! Box Tools kann sich nicht bei deiner FRITZ! Box anmelden.", "title": "Aktualisieren der FRITZ! Box Tools - Anmeldeinformationen" }, "start_config": { @@ -42,7 +42,10 @@ }, "user": { "data": { - "username": "Nutzername" + "host": "Host", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" } } } diff --git a/homeassistant/components/garages_amsterdam/translations/de.json b/homeassistant/components/garages_amsterdam/translations/de.json new file mode 100644 index 00000000000..71ade34b066 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index 3916b987981..6b88f0a1209 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", + "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/growatt_server/translations/de.json b/homeassistant/components/growatt_server/translations/de.json index f58a513e038..ae24396823a 100644 --- a/homeassistant/components/growatt_server/translations/de.json +++ b/homeassistant/components/growatt_server/translations/de.json @@ -16,7 +16,8 @@ "user": { "data": { "name": "Name", - "username": "Nutzername" + "password": "Passwort", + "username": "Benutzername" }, "title": "Gib deine Growatt-Informationen ein" } diff --git a/homeassistant/components/kraken/translations/de.json b/homeassistant/components/kraken/translations/de.json index 0a1e5f79414..9a5d52f939e 100644 --- a/homeassistant/components/kraken/translations/de.json +++ b/homeassistant/components/kraken/translations/de.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "already_configured": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/de.json b/homeassistant/components/rainmachine/translations/de.json index 7bfd228d717..20c49ea30f4 100644 --- a/homeassistant/components/rainmachine/translations/de.json +++ b/homeassistant/components/rainmachine/translations/de.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 8b48de950ee..91576e76ee5 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -16,7 +16,8 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." + "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", + "title": "Samsung TV" }, "reauth_confirm": { "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds." diff --git a/homeassistant/components/samsungtv/translations/et.json b/homeassistant/components/samsungtv/translations/et.json index fe236da487d..0cc9bf8ebcc 100644 --- a/homeassistant/components/samsungtv/translations/et.json +++ b/homeassistant/components/samsungtv/translations/et.json @@ -3,16 +3,25 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", - "auth_missing": "Home Assistant-il pole selle Samsungi teleriga \u00fchenduse loomiseks luba. Koduabilise autoriseerimiseks kontrolli oma teleri seadeid.", + "auth_missing": "Home Assistantil pole selle Samsungi teleriga \u00fchenduse loomiseks luba. Home Assistanti autoriseerimiseks kontrolli oma teleri seadeid.", "cannot_connect": "\u00dchendamine nurjus", - "not_supported": "Seda Samsungi tv-seadet praegu ei toetata." + "id_missing": "Sellel Samsungi seadmel puudub seerianumber.", + "not_supported": "Seda Samsungi seadet praegu ei toetata.", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "unknown": "Tundmatu t\u00f5rge" }, - "flow_title": "{model}", + "error": { + "auth_missing": "Tuvastamine nurjus" + }, + "flow_title": "{devicel}", "step": { "confirm": { - "description": "Kas soovid seadistada Samsung TV-d {model} ? Kui seda pole kunagi enne Home Assistantiga \u00fchendatud, n\u00e4ed oma teleris h\u00fcpikakent, mis k\u00fcsib tuvastamist. Selle teleri k\u00e4sitsi seadistused kirjutatakse \u00fcle.", + "description": "Kas soovid seadistada {devicel} ? Kui seda pole kunagi enne Home Assistantiga \u00fchendatud, n\u00e4ed oma teleris h\u00fcpikakent, mis k\u00fcsib tuvastamist.", "title": "" }, + "reauth_confirm": { + "description": "P\u00e4rast esitamist n\u00f5ustu {device} h\u00fcpikaknaga, mis taotleb autoriseerimist 30 sekundi jooksul." + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index 529df4b9ee9..950a460965b 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -3,16 +3,25 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u8a2d\u5b9a\u4ee5\u76e1\u8208\u9a57\u8b49\u3002", + "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u5916\u90e8\u88dd\u7f6e\u7ba1\u7406\u54e1\u8a2d\u5b9a\u4ee5\u9032\u884c\u9a57\u8b49\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "not_supported": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u4e09\u661f\u96fb\u8996\u3002" + "id_missing": "\u4e09\u661f\u88dd\u7f6e\u4e26\u672a\u5305\u542b\u5e8f\u865f\u3002", + "not_supported": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u4e09\u661f\u88dd\u7f6e\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "{model}", + "error": { + "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u5916\u90e8\u88dd\u7f6e\u7ba1\u7406\u54e1\u8a2d\u5b9a\u4ee5\u9032\u884c\u9a57\u8b49\u3002" + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4e09\u661f\u96fb\u8996 {model}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002\u624b\u52d5\u8a2d\u5b9a\u5c07\u6703\u8986\u84cb\u539f\u8a2d\u5b9a\u3002", + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {device}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002", "title": "\u4e09\u661f\u96fb\u8996" }, + "reauth_confirm": { + "description": "\u50b3\u9001\u5f8c\u3001\u8acb\u65bc 30 \u79d2\u5167\u540c\u610f {device} \u4e0a\u9396\u986f\u793a\u7684\u5f48\u51fa\u8996\u7a97\u6388\u6b0a\u8a31\u53ef\u3002" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/screenlogic/translations/de.json b/homeassistant/components/screenlogic/translations/de.json index 6afe42e37ee..84f425be218 100644 --- a/homeassistant/components/screenlogic/translations/de.json +++ b/homeassistant/components/screenlogic/translations/de.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/system_bridge/translations/de.json b/homeassistant/components/system_bridge/translations/de.json index d54c4a23e4f..9f03951c429 100644 --- a/homeassistant/components/system_bridge/translations/de.json +++ b/homeassistant/components/system_bridge/translations/de.json @@ -10,6 +10,7 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index dafe1777a9f..bea46792003 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { From caad125b448af16374a967ee056a1db8734aa50a Mon Sep 17 00:00:00 2001 From: Marcin Ciupak <32123526+mciupak@users.noreply.github.com> Date: Sun, 23 May 2021 04:10:27 +0200 Subject: [PATCH 658/852] Add support for Oracle DB in recorder (#50090) --- homeassistant/components/logbook/__init__.py | 8 ++--- .../components/recorder/migration.py | 32 +++++++++++++++++++ homeassistant/components/recorder/models.py | 15 ++++----- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index de0f901be3b..87b66c57730 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -500,10 +500,10 @@ def _generate_events_query(session): def _generate_events_query_without_states(session): return session.query( *EVENT_COLUMNS, - literal(None).label("state"), - literal(None).label("entity_id"), - literal(None).label("domain"), - literal(None).label("attributes"), + literal(value=None, type_=sqlalchemy.String).label("state"), + literal(value=None, type_=sqlalchemy.String).label("entity_id"), + literal(value=None, type_=sqlalchemy.String).label("domain"), + literal(value=None, type_=sqlalchemy.Text).label("attributes"), ) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 51d0f0a86fd..8e6c4861739 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -312,6 +312,34 @@ def _update_states_table_with_foreign_key_options(connection, engine): ) +def _drop_foreign_key_constraints(connection, engine, table, columns): + """Drop foreign key constraints for a table on specific columns.""" + inspector = sqlalchemy.inspect(engine) + drops = [] + for foreign_key in inspector.get_foreign_keys(table): + if ( + foreign_key["name"] + and foreign_key["options"].get("ondelete") + and foreign_key["constrained_columns"] == columns + ): + drops.append(ForeignKeyConstraint((), (), name=foreign_key["name"])) + + # Bind the ForeignKeyConstraints to the table + old_table = Table( # noqa: F841 pylint: disable=unused-variable + table, MetaData(), *drops + ) + + for drop in drops: + try: + connection.execute(DropConstraint(drop)) + except (InternalError, OperationalError): + _LOGGER.exception( + "Could not drop foreign constraints in %s table on %s", + TABLE_STATES, + columns, + ) + + def _apply_update(engine, session, new_version, old_version): """Perform operations to bring schema up to date.""" connection = session.connection() @@ -420,6 +448,10 @@ def _apply_update(engine, session, new_version, old_version): # Recreate the statistics table Statistics.__table__.drop(engine) Statistics.__table__.create(engine) + elif new_version == 16: + _drop_foreign_key_constraints( + connection, engine, TABLE_STATES, ["old_state_id"] + ) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 924f59790b0..4fefcaa19e3 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -8,6 +8,7 @@ from sqlalchemy import ( DateTime, Float, ForeignKey, + Identity, Index, Integer, String, @@ -28,7 +29,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 15 +SCHEMA_VERSION = 16 _LOGGER = logging.getLogger(__name__) @@ -61,7 +62,7 @@ class Events(Base): # type: ignore "mysql_collate": "utf8mb4_unicode_ci", } __tablename__ = TABLE_EVENTS - event_id = Column(Integer, primary_key=True) + event_id = Column(Integer, Identity(), primary_key=True) event_type = Column(String(MAX_LENGTH_EVENT_TYPE)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(32)) @@ -128,7 +129,7 @@ class States(Base): # type: ignore "mysql_collate": "utf8mb4_unicode_ci", } __tablename__ = TABLE_STATES - state_id = Column(Integer, primary_key=True) + state_id = Column(Integer, Identity(), primary_key=True) domain = Column(String(64)) entity_id = Column(String(255)) state = Column(String(255)) @@ -139,9 +140,7 @@ class States(Base): # type: ignore last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) created = Column(DATETIME_TYPE, default=dt_util.utcnow) - old_state_id = Column( - Integer, ForeignKey("states.state_id", ondelete="NO ACTION"), index=True - ) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) @@ -246,7 +245,7 @@ class RecorderRuns(Base): # type: ignore """Representation of recorder run.""" __tablename__ = TABLE_RECORDER_RUNS - run_id = Column(Integer, primary_key=True) + run_id = Column(Integer, Identity(), primary_key=True) start = Column(DateTime(timezone=True), default=dt_util.utcnow) end = Column(DateTime(timezone=True)) closed_incorrect = Column(Boolean, default=False) @@ -297,7 +296,7 @@ class SchemaChanges(Base): # type: ignore """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES - change_id = Column(Integer, primary_key=True) + change_id = Column(Integer, Identity(), primary_key=True) schema_version = Column(Integer) changed = Column(DateTime(timezone=True), default=dt_util.utcnow) From 0cbcb9e0d6a82463f1bc9cbe65508743091825f5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 23 May 2021 05:32:41 +0200 Subject: [PATCH 659/852] Fix Hue overriding property methods, remove ignored typing (#50976) --- homeassistant/components/hue/binary_sensor.py | 2 +- homeassistant/components/hue/sensor.py | 27 ++++++------------- homeassistant/components/hue/sensor_base.py | 5 +++- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 5 files changed, 13 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index d5c6953700d..c675544503c 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HuePresence(GenericZLLSensor, BinarySensorEntity): """The presence sensor entity for a Hue motion sensor device.""" - device_class = DEVICE_CLASS_MOTION + _attr_device_class = DEVICE_CLASS_MOTION @property def is_on(self): diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 75d31fb61ce..0f1fa418287 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -41,8 +41,8 @@ class GenericHueGaugeSensorEntity(GenericZLLSensor, SensorEntity): class HueLightLevel(GenericHueGaugeSensorEntity): """The light level sensor entity for a Hue motion sensor device.""" - device_class = DEVICE_CLASS_ILLUMINANCE - unit_of_measurement = LIGHT_LUX + _attr_device_class = DEVICE_CLASS_ILLUMINANCE + _attr_unit_of_measurement = LIGHT_LUX @property def state(self): @@ -76,8 +76,9 @@ class HueLightLevel(GenericHueGaugeSensorEntity): class HueTemperature(GenericHueGaugeSensorEntity): """The temperature sensor entity for a Hue motion sensor device.""" - device_class = DEVICE_CLASS_TEMPERATURE - unit_of_measurement = TEMP_CELSIUS + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_unit_of_measurement = TEMP_CELSIUS @property def state(self): @@ -87,15 +88,13 @@ class HueTemperature(GenericHueGaugeSensorEntity): return self.sensor.temperature / 100 - @property - def state_class(self): - """Return the state class of the sensor.""" - return STATE_CLASS_MEASUREMENT - class HueBattery(GenericHueSensor, SensorEntity): """Battery class for when a batt-powered device is only represented as an event.""" + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + @property def unique_id(self): """Return a unique identifier for this device.""" @@ -106,16 +105,6 @@ class HueBattery(GenericHueSensor, SensorEntity): """Return the state of the battery.""" return self.sensor.battery - @property - def device_class(self): - """Return the class of the sensor.""" - return DEVICE_CLASS_BATTERY - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return PERCENTAGE - SENSOR_CONFIG_MAP.update( { diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index bb527c63b2a..824f8cf42dc 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -1,6 +1,9 @@ """Support for the Philips Hue sensors as a platform.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from aiohue import AiohueException, Unauthorized from aiohue.sensors import TYPE_ZLL_PRESENCE @@ -16,7 +19,7 @@ from .helpers import remove_devices from .hue_event import EVENT_CONFIG_MAP from .sensor_device import GenericHueDevice -SENSOR_CONFIG_MAP = {} +SENSOR_CONFIG_MAP: dict[str, Any] = {} _LOGGER = logging.getLogger(__name__) diff --git a/mypy.ini b/mypy.ini index bb75b29fa6d..2cb4d318962 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1012,9 +1012,6 @@ ignore_errors = true [mypy-homeassistant.components.honeywell.*] ignore_errors = true -[mypy-homeassistant.components.hue.*] -ignore_errors = true - [mypy-homeassistant.components.huisbaasje.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 1b49dd023f7..f5337d8e5ed 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -91,7 +91,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.homekit_controller.*", "homeassistant.components.homematicip_cloud.*", "homeassistant.components.honeywell.*", - "homeassistant.components.hue.*", "homeassistant.components.huisbaasje.*", "homeassistant.components.humidifier.*", "homeassistant.components.iaqualink.*", From 7ff14b47a83e7449c01eb81b0e28f183e80a57c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 23 May 2021 05:34:48 +0200 Subject: [PATCH 660/852] Use whoami for location lookup (#50934) --- homeassistant/components/ps4/__init__.py | 11 ++- homeassistant/components/ps4/config_flow.py | 10 ++- homeassistant/components/ps4/const.py | 68 ++++++++++++++++++ homeassistant/util/location.py | 52 +++----------- tests/components/config/test_core.py | 1 - tests/components/ps4/test_config_flow.py | 1 - tests/components/ps4/test_init.py | 1 - tests/fixtures/ip-api.com.json | 16 ----- tests/fixtures/ipapi.co.json | 20 ------ tests/fixtures/whoami.json | 14 ++++ tests/util/test_location.py | 79 ++++----------------- 11 files changed, 121 insertions(+), 152 deletions(-) delete mode 100644 tests/fixtures/ip-api.com.json delete mode 100644 tests/fixtures/ipapi.co.json create mode 100644 tests/fixtures/whoami.json diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 65940b9dc48..bf5eafa7bbb 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -25,7 +25,14 @@ from homeassistant.util import location from homeassistant.util.json import load_json, save_json from .config_flow import PlayStation4FlowHandler # noqa: F401 -from .const import ATTR_MEDIA_IMAGE_URL, COMMANDS, DOMAIN, GAMES_FILE, PS4_DATA +from .const import ( + ATTR_MEDIA_IMAGE_URL, + COMMANDS, + COUNTRYCODE_NAMES, + DOMAIN, + GAMES_FILE, + PS4_DATA, +) _LOGGER = logging.getLogger(__name__) @@ -91,7 +98,7 @@ async def async_migrate_entry(hass, entry): hass.helpers.aiohttp_client.async_get_clientsession() ) if loc: - country = loc.country_name + country = COUNTRYCODE_NAMES.get(loc.country_code) if country in COUNTRIES: for device in data["devices"]: device[CONF_REGION] = country diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index 1be879df58e..7424e0f5e1a 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -17,7 +17,13 @@ from homeassistant.const import ( ) from homeassistant.util import location -from .const import CONFIG_ENTRY_VERSION, DEFAULT_ALIAS, DEFAULT_NAME, DOMAIN +from .const import ( + CONFIG_ENTRY_VERSION, + COUNTRYCODE_NAMES, + DEFAULT_ALIAS, + DEFAULT_NAME, + DOMAIN, +) CONF_MODE = "Config Mode" CONF_AUTO = "Auto Discover" @@ -178,7 +184,7 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass.helpers.aiohttp_client.async_get_clientsession() ) if self.location: - country = self.location.country_name + country = COUNTRYCODE_NAMES.get(self.location.country_code) if country in COUNTRIES: default_region = country diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index 0974286ebe8..f2d284daa79 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -12,3 +12,71 @@ COMMANDS = ("up", "down", "right", "left", "enter", "back", "option", "ps", "ps_ # Deprecated used for logger/backwards compatibility from 0.89 REGIONS = ["R1", "R2", "R3", "R4", "R5"] + +COUNTRYCODE_NAMES = { + "AE": "United Arab Emirates", + "AR": "Argentina", + "AT": "Austria", + "AU": "Australia", + "BE": "Belgium", + "BG": "Bulgaria", + "BH": "Bahrain", + "BR": "Brazil", + "CA": "Canada", + "CH": "Switzerland", + "CL": "Chile", + "CO": "Columbia", + "CR": "Costa Rica", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DE": "Germany", + "DK": "Denmark", + "EC": "Ecuador", + "ES": "Spain", + "FI": "Finland", + "FR": "France", + "GB": "United Kingdom", + "GR": "Greece", + "GT": "Guatemala", + "HK": "Hong Kong", + "HN": "Honduras", + "HR": "Croatia", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IN": "India", + "IS": "Iceland", + "IT": "Italy", + "JP": "Japan", + "KW": "Kuwait", + "LB": "Lebanon", + "LU": "Luxembourg", + "MT": "Malta", + "MX": "Mexico", + "MY": "Maylasia", + "NI": "Nicaragua", + "NL": "Nederland", + "NO": "Norway", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PL": "Poland", + "PT": "Portugal", + "QA": "Qatar", + "RO": "Romania", + "RU": "Russia", + "SA": "Saudi Arabia", + "SE": "Sweden", + "SG": "Singapore", + "SI": "Slovenia", + "SK": "Slovakia", + "SV": "El Salvador", + "TH": "Thailand", + "TR": "Turkey", + "TW": "Taiwan", + "UA": "Ukraine", + "US": "United States", + "ZA": "South Africa", +} diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index c22f5213130..2a3a4ff0922 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -12,9 +12,7 @@ from typing import Any import aiohttp -ELEVATION_URL = "https://api.open-elevation.com/api/v1/lookup" -IP_API = "http://ip-api.com/json" -IPAPI = "https://ipapi.co/json/" +WHOAMI_URL = "https://whoami.home-assistant.io/v1" # Constants from https://github.com/maurycyp/vincenty # Earth ellipsoid according to WGS 84 @@ -34,7 +32,6 @@ LocationInfo = collections.namedtuple( [ "ip", "country_code", - "country_name", "region_code", "region_name", "city", @@ -51,10 +48,7 @@ async def async_detect_location_info( session: aiohttp.ClientSession, ) -> LocationInfo | None: """Detect location information.""" - data = await _get_ipapi(session) - - if data is None: - data = await _get_ip_api(session) + data = await _get_whoami(session) if data is None: return None @@ -164,10 +158,10 @@ def vincenty( return round(s, 6) -async def _get_ipapi(session: aiohttp.ClientSession) -> dict[str, Any] | None: - """Query ipapi.co for location data.""" +async def _get_whoami(session: aiohttp.ClientSession) -> dict[str, Any] | None: + """Query whoami.home-assistant.io for location data.""" try: - resp = await session.get(IPAPI, timeout=5) + resp = await session.get(WHOAMI_URL, timeout=30) except (aiohttp.ClientError, asyncio.TimeoutError): return None @@ -176,44 +170,14 @@ async def _get_ipapi(session: aiohttp.ClientSession) -> dict[str, Any] | None: except (aiohttp.ClientError, ValueError): return None - # ipapi allows 30k free requests/month. Some users exhaust those. - if raw_info.get("latitude") == "Sign up to access": - return None - return { "ip": raw_info.get("ip"), "country_code": raw_info.get("country"), - "country_name": raw_info.get("country_name"), "region_code": raw_info.get("region_code"), "region_name": raw_info.get("region"), "city": raw_info.get("city"), - "zip_code": raw_info.get("postal"), + "zip_code": raw_info.get("postal_code"), "time_zone": raw_info.get("timezone"), - "latitude": raw_info.get("latitude"), - "longitude": raw_info.get("longitude"), - } - - -async def _get_ip_api(session: aiohttp.ClientSession) -> dict[str, Any] | None: - """Query ip-api.com for location data.""" - try: - resp = await session.get(IP_API, timeout=5) - except (aiohttp.ClientError, asyncio.TimeoutError): - return None - - try: - raw_info = await resp.json() - except (aiohttp.ClientError, ValueError): - return None - return { - "ip": raw_info.get("query"), - "country_code": raw_info.get("countryCode"), - "country_name": raw_info.get("country"), - "region_code": raw_info.get("region"), - "region_name": raw_info.get("regionName"), - "city": raw_info.get("city"), - "zip_code": raw_info.get("zip"), - "time_zone": raw_info.get("timezone"), - "latitude": raw_info.get("lat"), - "longitude": raw_info.get("lon"), + "latitude": float(raw_info.get("latitude")), + "longitude": float(raw_info.get("longitude")), } diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index b58b572e230..738b1183c14 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -144,7 +144,6 @@ async def test_detect_config_fail(hass, client): return_value=location.LocationInfo( ip=None, country_code=None, - country_name=None, region_code=None, region_name=None, city=None, diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index f8c28d236be..36c38a62b4c 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -64,7 +64,6 @@ MOCK_MANUAL = {"Config Mode": "Manual Entry", CONF_IP_ADDRESS: MOCK_HOST} MOCK_LOCATION = location.LocationInfo( "0.0.0.0", "US", - "United States", "CA", "California", "San Diego", diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 94167528b21..f57fada8c37 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -56,7 +56,6 @@ MOCK_CONFIG = MockConfigEntry(domain=DOMAIN, data=MOCK_DATA, entry_id=MOCK_ENTRY MOCK_LOCATION = location.LocationInfo( "0.0.0.0", "US", - "United States", "CA", "California", "San Diego", diff --git a/tests/fixtures/ip-api.com.json b/tests/fixtures/ip-api.com.json deleted file mode 100644 index d31d4560589..00000000000 --- a/tests/fixtures/ip-api.com.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "as": "AS20001 Time Warner Cable Internet LLC", - "city": "San Diego", - "country": "United States", - "countryCode": "US", - "isp": "Time Warner Cable", - "lat": 32.8594, - "lon": -117.2073, - "org": "Time Warner Cable", - "query": "1.2.3.4", - "region": "CA", - "regionName": "California", - "status": "success", - "timezone": "America\/Los_Angeles", - "zip": "92122" -} diff --git a/tests/fixtures/ipapi.co.json b/tests/fixtures/ipapi.co.json deleted file mode 100644 index f1dc58a756b..00000000000 --- a/tests/fixtures/ipapi.co.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "ip": "1.2.3.4", - "city": "Bern", - "region": "Bern", - "region_code": "BE", - "country": "CH", - "country_name": "Switzerland", - "continent_code": "EU", - "in_eu": false, - "postal": "3000", - "latitude": 46.9480278, - "longitude": 7.4490812, - "timezone": "Europe/Zurich", - "utc_offset": "+0100", - "country_calling_code": "+41", - "currency": "CHF", - "languages": "de-CH,fr-CH,it-CH,rm", - "asn": "AS6830", - "org": "Liberty Global B.V." -} \ No newline at end of file diff --git a/tests/fixtures/whoami.json b/tests/fixtures/whoami.json new file mode 100644 index 00000000000..c805ef30558 --- /dev/null +++ b/tests/fixtures/whoami.json @@ -0,0 +1,14 @@ +{ + "ip": "1.2.3.4", + "city": "Gotham", + "continent": "Earth", + "country": "XX", + "latitude": "12.34567", + "longitude": "12.34567", + "postal_code": "12345", + "region_code": "00", + "region": "Gotham", + "timezone": "Earth/Gotham", + "iso_time": "2021-05-12T11:29:15.752Z", + "timestamp": 1620818956 +} \ No newline at end of file diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 9eb2dc70561..21531a59194 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,5 +1,5 @@ """Test Home Assistant location util methods.""" -from unittest.mock import Mock, patch +from unittest.mock import Mock import aiohttp import pytest @@ -72,76 +72,25 @@ def test_get_miles(): assert round(miles, 2) == DISTANCE_MILES -async def test_detect_location_info_ipapi(aioclient_mock, session): - """Test detect location info using ipapi.co.""" - aioclient_mock.get(location_util.IPAPI, text=load_fixture("ipapi.co.json")) +async def test_detect_location_info_whoami(aioclient_mock, session): + """Test detect location info using whoami.home-assistant.io.""" + aioclient_mock.get(location_util.WHOAMI_URL, text=load_fixture("whoami.json")) info = await location_util.async_detect_location_info(session, _test_real=True) assert info is not None assert info.ip == "1.2.3.4" - assert info.country_code == "CH" - assert info.country_name == "Switzerland" - assert info.region_code == "BE" - assert info.region_name == "Bern" - assert info.city == "Bern" - assert info.zip_code == "3000" - assert info.time_zone == "Europe/Zurich" - assert info.latitude == 46.9480278 - assert info.longitude == 7.4490812 + assert info.country_code == "XX" + assert info.region_code == "00" + assert info.city == "Gotham" + assert info.zip_code == "12345" + assert info.time_zone == "Earth/Gotham" + assert info.latitude == 12.34567 + assert info.longitude == 12.34567 assert info.use_metric -async def test_detect_location_info_ipapi_exhaust(aioclient_mock, session): - """Test detect location info using ipapi.co.""" - aioclient_mock.get(location_util.IPAPI, json={"latitude": "Sign up to access"}) - aioclient_mock.get(location_util.IP_API, text=load_fixture("ip-api.com.json")) - - info = await location_util.async_detect_location_info(session, _test_real=True) - - assert info is not None - # ip_api result because ipapi got skipped - assert info.country_code == "US" - assert len(aioclient_mock.mock_calls) == 2 - - -async def test_detect_location_info_ip_api(aioclient_mock, session): - """Test detect location info using ip-api.com.""" - aioclient_mock.get(location_util.IP_API, text=load_fixture("ip-api.com.json")) - - with patch("homeassistant.util.location._get_ipapi", return_value=None): - info = await location_util.async_detect_location_info(session, _test_real=True) - - assert info is not None - assert info.ip == "1.2.3.4" - assert info.country_code == "US" - assert info.country_name == "United States" - assert info.region_code == "CA" - assert info.region_name == "California" - assert info.city == "San Diego" - assert info.zip_code == "92122" - assert info.time_zone == "America/Los_Angeles" - assert info.latitude == 32.8594 - assert info.longitude == -117.2073 - assert not info.use_metric - - -async def test_detect_location_info_both_queries_fail(session): - """Ensure we return None if both queries fail.""" - with patch("homeassistant.util.location._get_ipapi", return_value=None), patch( - "homeassistant.util.location._get_ip_api", return_value=None - ): - info = await location_util.async_detect_location_info(session, _test_real=True) - assert info is None - - -async def test_freegeoip_query_raises(raising_session): - """Test ipapi.co query when the request to API fails.""" - info = await location_util._get_ipapi(raising_session) - assert info is None - - -async def test_ip_api_query_raises(raising_session): - """Test ip api query when the request to API fails.""" - info = await location_util._get_ip_api(raising_session) +async def test_whoami_query_raises(raising_session): + """Test whoami query when the request to API fails.""" + info = await location_util._get_whoami(raising_session) assert info is None From e4c77fd336ae3d61f2ec72a70069db57600fc04c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 May 2021 00:22:56 -0500 Subject: [PATCH 661/852] Bump aiohomekit to 0.2.62 (#50981) - Discovery is now instant when a ServiceBrowser is running Changelog: https://github.com/Jc2k/aiohomekit/compare/0.2.61...0.2.62 --- CODEOWNERS | 2 +- homeassistant/components/homekit_controller/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8049fec94b0..62518a59f15 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -206,7 +206,7 @@ homeassistant/components/home_connect/* @DavidMStraub homeassistant/components/home_plus_control/* @chemaaa homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homekit/* @bdraco -homeassistant/components/homekit_controller/* @Jc2k +homeassistant/components/homekit_controller/* @Jc2k @bdraco homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index cb248fcaa5f..1bfa39fab80 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,9 +3,9 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.2.61"], + "requirements": ["aiohomekit==0.2.62"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], - "codeowners": ["@Jc2k"], + "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 18365ca14cb..f05092ce918 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.61 +aiohomekit==0.2.62 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e90799028e8..e19eaac7d3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.61 +aiohomekit==0.2.62 # homeassistant.components.emulated_hue # homeassistant.components.http From 3141535d691629e3199b0035febda46cb038e5aa Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sun, 23 May 2021 16:55:02 +1000 Subject: [PATCH 662/852] Bump geojson_client to 0.6 (#50985) * bump geojson_client library to version 0.6 * add myself as codeowner --- CODEOWNERS | 1 + homeassistant/components/geo_json_events/manifest.json | 4 ++-- homeassistant/components/usgs_earthquakes_feed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 62518a59f15..a4eb7a07987 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -174,6 +174,7 @@ homeassistant/components/garages_amsterdam/* @klaasnicolaas homeassistant/components/garmin_connect/* @cyberjunky homeassistant/components/gdacs/* @exxamalte homeassistant/components/geniushub/* @zxdavb +homeassistant/components/geo_json_events/* @exxamalte homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/geonetnz_volcano/* @exxamalte diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index 5d898ee99d5..aba5abff67c 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -2,7 +2,7 @@ "domain": "geo_json_events", "name": "GeoJSON", "documentation": "https://www.home-assistant.io/integrations/geo_json_events", - "requirements": ["geojson_client==0.4"], - "codeowners": [], + "requirements": ["geojson_client==0.6"], + "codeowners": ["@exxamalte"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index ef6fa7a982f..d38a5c056b8 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -2,7 +2,7 @@ "domain": "usgs_earthquakes_feed", "name": "U.S. Geological Survey Earthquake Hazards (USGS)", "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", - "requirements": ["geojson_client==0.4"], + "requirements": ["geojson_client==0.6"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index f05092ce918..b3ca2e850b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -645,7 +645,7 @@ geniushub-client==0.6.30 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed -geojson_client==0.4 +geojson_client==0.6 # homeassistant.components.aprs geopy==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e19eaac7d3d..112777b2362 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -348,7 +348,7 @@ garminconnect==0.1.19 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed -geojson_client==0.4 +geojson_client==0.6 # homeassistant.components.aprs geopy==2.1.0 From 5ca5b9ac8916b05a49d4985653b53fd347d52247 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 23 May 2021 10:42:17 +0200 Subject: [PATCH 663/852] Improve KNX config validation (#50980) * remove dict repacking * check binary_sensor device_class * check cover device_class * check sensor_type --- homeassistant/components/knx/binary_sensor.py | 6 ++-- homeassistant/components/knx/cover.py | 3 +- homeassistant/components/knx/schema.py | 34 +++++++++++++------ 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 3b2f4b7a75a..a3271e605af 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -6,7 +6,7 @@ from typing import Any from xknx import XKNX from xknx.devices import BinarySensor as XknxBinarySensor -from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -64,9 +64,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity): @property def device_class(self) -> str | None: """Return the class of this sensor.""" - if self._device_class in DEVICE_CLASSES: - return self._device_class - return None + return self._device_class @property def is_on(self) -> bool: diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 86fba34a87a..58d627ecb5e 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -12,7 +12,6 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DEVICE_CLASS_BLIND, - DEVICE_CLASSES, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, SUPPORT_OPEN, @@ -127,7 +126,7 @@ class KNXCover(KnxEntity, CoverEntity): @property def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" - if self._device_class in DEVICE_CLASSES: + if self._device_class: return self._device_class if self._device.supports_angle: return DEVICE_CLASS_BLIND diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 4fd0aaaa3b9..3ac0ec84e1d 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -5,10 +5,15 @@ from typing import Any import voluptuous as vol from xknx.devices.climate import SetpointShiftMode +from xknx.dpt import DPTBase from xknx.exceptions import CouldNotParseAddress from xknx.io import DEFAULT_MCAST_PORT from xknx.telegram.address import IndividualAddress, parse_device_group_address +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES as BINARY_SENSOR_DEVICE_CLASSES, +) +from homeassistant.components.cover import DEVICE_CLASSES as COVER_DEVICE_CLASSES from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_ID, @@ -44,7 +49,8 @@ def ga_validator(value: Any) -> str | int: except CouldNotParseAddress: pass raise vol.Invalid( - f"value '{value}' is not a valid KNX group address '
//', '
/' or '' (eg.'1/2/3', '9/234', '123'), nor xknx internal address 'i-'." + f"value '{value}' is not a valid KNX group address '
//', '
/' " + "or '' (eg.'1/2/3', '9/234', '123'), nor xknx internal address 'i-'." ) @@ -56,15 +62,20 @@ ia_validator = vol.Any( msg="value does not match pattern for KNX individual address '..' (eg.'1.1.100')", ) + +def sensor_type_validator(value: Any) -> str | int: + """Validate that value is parsable as sensor type.""" + if isinstance(value, (str, int)) and DPTBase.parse_transcoder(value) is not None: + return value + raise vol.Invalid(f"value '{value}' is not a valid sensor type.") + + sync_state_validator = vol.Any( vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)), cv.boolean, cv.matches_regex(r"^(init|expire|every)( \d*)?$"), ) -sensor_type_validator = vol.Any(int, str) - - ############## # CONNECTION ############## @@ -119,7 +130,7 @@ class BinarySensorSchema: vol.Optional(CONF_CONTEXT_TIMEOUT): vol.All( vol.Coerce(float), vol.Range(min=0, max=10) ), - vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Optional(CONF_DEVICE_CLASS): vol.In(BINARY_SENSOR_DEVICE_CLASSES), vol.Optional(CONF_RESET_AFTER): cv.positive_float, } ), @@ -222,10 +233,10 @@ class ClimateSchema: CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT ): cv.boolean, vol.Optional(CONF_OPERATION_MODES): vol.All( - cv.ensure_list, [vol.In({**PRESET_MODES})] + cv.ensure_list, [vol.In(PRESET_MODES)] ), vol.Optional(CONF_CONTROLLER_MODES): vol.All( - cv.ensure_list, [vol.In({**CONTROLLER_MODES})] + cv.ensure_list, [vol.In(CONTROLLER_MODES)] ), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), @@ -280,7 +291,7 @@ class CoverSchema: ): cv.positive_float, vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, - vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Optional(CONF_DEVICE_CLASS): vol.In(COVER_DEVICE_CLASSES), } ), ) @@ -291,6 +302,7 @@ class ExposeSchema: CONF_KNX_EXPOSE_TYPE = CONF_TYPE CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" + CONF_KNX_EXPOSE_BINARY = "binary" CONF_KNX_EXPOSE_DEFAULT = "default" EXPOSE_TIME_TYPES = [ "time", @@ -308,14 +320,16 @@ class ExposeSchema: ) EXPOSE_SENSOR_SCHEMA = vol.Schema( { - vol.Required(CONF_KNX_EXPOSE_TYPE): sensor_type_validator, + vol.Required(CONF_KNX_EXPOSE_TYPE): vol.Any( + CONF_KNX_EXPOSE_BINARY, sensor_type_validator + ), vol.Required(KNX_ADDRESS): ga_validator, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string, vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, } ) - SCHEMA = vol.Any(EXPOSE_TIME_SCHEMA, EXPOSE_SENSOR_SCHEMA) + SCHEMA = vol.Any(EXPOSE_SENSOR_SCHEMA, EXPOSE_TIME_SCHEMA) class FanSchema: From ecb24f01a3e3d5219e6e7a84f3e1bdd7f2b0d38c Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Sun, 23 May 2021 12:48:33 +0200 Subject: [PATCH 664/852] Bump aiopvpc from 2.0.2 to 2.1.1 (#50989) * Remove pytz dependency and handle timezones with zoneinfo, and adapt to use input timezone as a time zone object or a string identifier * Fix prices being badly assigned in Canary Islands timezone * Fix sensor attributes in month changes --- homeassistant/components/pvpc_hourly_pricing/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 578dfc73619..c39d66163e0 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.0.2"], + "requirements": ["aiopvpc==2.1.1"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index b3ca2e850b9..e2cd1e763c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.0.2 +aiopvpc==2.1.1 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 112777b2362..e4abd4711fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.0.2 +aiopvpc==2.1.1 # homeassistant.components.webostv aiopylgtv==0.4.0 From 9b02fd86c5155399f9ad5f0cd7ff07058bbc7cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 23 May 2021 13:11:48 +0200 Subject: [PATCH 665/852] Update mill library, fix consumption data (#50992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 495ee960588..f41f03b66c0 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.4.0"], + "requirements": ["millheater==0.4.1"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index e2cd1e763c0..241e24234ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -951,7 +951,7 @@ mficlient==0.3.0 miflora==0.7.0 # homeassistant.components.mill -millheater==0.4.0 +millheater==0.4.1 # homeassistant.components.minio minio==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4abd4711fa..ddbce293011 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -520,7 +520,7 @@ meteofrance-api==1.0.2 mficlient==0.3.0 # homeassistant.components.mill -millheater==0.4.0 +millheater==0.4.1 # homeassistant.components.minio minio==4.0.9 From 44bbd9396db196752e7ade1ec9d14400303083e8 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Sun, 23 May 2021 13:08:21 +0100 Subject: [PATCH 666/852] Fix typing in config flow helper (#50994) --- homeassistant/helpers/config_entry_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 1a29f8a1752..05365b85645 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType -DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] +DiscoveryFunctionType = Callable[[HomeAssistant], Union[Awaitable[bool], bool]] _LOGGER = logging.getLogger(__name__) From 29205a923968dda85bf014f0a424041e9171cf6b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 23 May 2021 08:43:49 -0500 Subject: [PATCH 667/852] Sonos use common firmware version (#50861) --- homeassistant/components/sonos/speaker.py | 2 +- tests/components/sonos/conftest.py | 1 + tests/components/sonos/test_media_player.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index eb4e194403e..97fc8dcdbcc 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -147,7 +147,7 @@ class SonosSpeaker: self.mac_address = speaker_info["mac_address"] self.model_name = speaker_info["model_name"] - self.version = speaker_info["software_version"] + self.version = speaker_info["display_version"] self.zone_name = speaker_info["zone_name"] self.battery_info: dict[str, Any] | None = None diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f22c462f881..79e44720591 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -83,6 +83,7 @@ def speaker_info_fixture(): "model_name": "Model Name", "software_version": "49.2-64250", "mac_address": "00-11-22-33-44-55", + "display_version": "13.1", } diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 460c9012aeb..cba6967463a 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -63,7 +63,7 @@ async def test_device_registry(hass, config_entry, config, soco): identifiers={("sonos", "RINCON_test")} ) assert reg_device.model == "Model Name" - assert reg_device.sw_version == "49.2-64250" + assert reg_device.sw_version == "13.1" assert reg_device.connections == {(dr.CONNECTION_NETWORK_MAC, "00:11:22:33:44:55")} assert reg_device.manufacturer == "Sonos" assert reg_device.suggested_area == "Zone A" From e1b6385b4d2d9c8b03ff3598d922b45a818a2b18 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 May 2021 08:56:16 -0500 Subject: [PATCH 668/852] Add support for doorbell buttons to homekit_controller (#50983) --- .../homekit_controller/device_trigger.py | 1 + .../specific_devices/test_netamo_doorbell.py | 73 ++++ .../homekit_controller/netamo_doorbell.json | 341 ++++++++++++++++++ 3 files changed, 415 insertions(+) create mode 100644 tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py create mode 100644 tests/fixtures/homekit_controller/netamo_doorbell.json diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index c9cd771edf6..a04d7237cf1 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -16,6 +16,7 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS TRIGGER_TYPES = { + "doorbell", "button1", "button2", "button3", diff --git a/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py new file mode 100644 index 00000000000..58fe9df077f --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py @@ -0,0 +1,73 @@ +""" +Regression tests for Netamo Doorbell. + +https://github.com/home-assistant/core/issues/44596 +""" + +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import assert_lists_same, async_get_device_automations +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_netamo_doorbell_setup(hass): + """Test that a Netamo Doorbell can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "netamo_doorbell.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + + # Check that the camera is correctly found and set up + doorbell_id = "camera.netatmo_doorbell_g738658" + doorbell = entity_registry.async_get(doorbell_id) + assert doorbell.unique_id == "homekit-g738658-aid:1" + + camera_helper = Helper( + hass, + "camera.netatmo_doorbell_g738658", + pairing, + accessories[0], + config_entry, + ) + camera_helper = await camera_helper.poll_and_get_state() + assert camera_helper.attributes["friendly_name"] == "Netatmo-Doorbell-g738658" + + device_registry = dr.async_get(hass) + + device = device_registry.async_get(doorbell.device_id) + assert device.manufacturer == "Netatmo" + assert device.name == "Netatmo-Doorbell-g738658" + assert device.model == "Netatmo Doorbell" + assert device.sw_version == "80.0.0" + assert device.via_device_id is None + + # The fixture file has 1 button + expected = [] + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": doorbell.device_id, + "domain": "homekit_controller", + "platform": "device", + "type": "doorbell", + "subtype": subtype, + } + ) + + for type in ("no_motion", "motion"): + expected.append( + { + "device_id": doorbell.device_id, + "domain": "binary_sensor", + "entity_id": "binary_sensor.netatmo_doorbell_g738658", + "platform": "device", + "type": type, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", doorbell.device_id) + assert_lists_same(triggers, expected) diff --git a/tests/fixtures/homekit_controller/netamo_doorbell.json b/tests/fixtures/homekit_controller/netamo_doorbell.json new file mode 100644 index 00000000000..450b419f30d --- /dev/null +++ b/tests/fixtures/homekit_controller/netamo_doorbell.json @@ -0,0 +1,341 @@ +[ + { + "aid" : 1, + "services" : [ + { + "hidden" : true, + "iid" : 53, + "characteristics" : [ + { + "format" : "bool", + "iid" : 54, + "perms" : [ + "pw" + ], + "type" : "4D05AE82-5A22-5BD6-A730-B7F8B4F3218D" + }, + { + "value" : "g738658", + "format" : "string", + "type" : "00F44C18-042E-5C4E-9A4C-561D44DCD804", + "perms" : [ + "pr" + ], + "iid" : 55 + } + ], + "primary" : false, + "type" : "EA22EA53-6227-55EA-AC24-73ACF3EEA0E8" + }, + { + "type" : "0000003E-0000-1000-8000-0026BB765291", + "primary" : false, + "iid" : 1, + "characteristics" : [ + { + "format" : "string", + "value" : "Netatmo-Doorbell-g738658", + "iid" : 2, + "perms" : [ + "pr" + ], + "type" : "00000023-0000-1000-8000-0026BB765291" + }, + { + "iid" : 3, + "type" : "00000020-0000-1000-8000-0026BB765291", + "perms" : [ + "pr" + ], + "value" : "Netatmo", + "format" : "string" + }, + { + "format" : "string", + "value" : "Netatmo Doorbell", + "perms" : [ + "pr" + ], + "type" : "00000021-0000-1000-8000-0026BB765291", + "iid" : 4 + }, + { + "format" : "string", + "value" : "g738658", + "perms" : [ + "pr" + ], + "type" : "00000030-0000-1000-8000-0026BB765291", + "iid" : 5 + }, + { + "iid" : 6, + "perms" : [ + "pr" + ], + "type" : "00000052-0000-1000-8000-0026BB765291", + "format" : "string", + "value" : "80.0.0" + }, + { + "type" : "00000014-0000-1000-8000-0026BB765291", + "perms" : [ + "pw" + ], + "iid" : 7, + "format" : "bool" + }, + { + "value" : "+nvrOo7/HvM=", + "format" : "data", + "iid" : 56, + "type" : "220", + "perms" : [ + "pr" + ] + } + ], + "hidden" : false + }, + { + "hidden" : false, + "iid" : 29, + "characteristics" : [ + { + "format" : "string", + "value" : "1.1.0", + "perms" : [ + "pr" + ], + "type" : "00000037-0000-1000-8000-0026BB765291", + "iid" : 30 + } + ], + "type" : "000000A2-0000-1000-8000-0026BB765291", + "primary" : false + }, + { + "type" : "00000121-0000-1000-8000-0026BB765291", + "primary" : true, + "characteristics" : [ + { + "value" : null, + "format" : "uint8", + "type" : "00000073-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "ev" + ], + "iid" : 50 + }, + { + "value" : "Doorbell", + "format" : "string", + "type" : "00000023-0000-1000-8000-0026BB765291", + "perms" : [ + "pr" + ], + "iid" : 57 + } + ], + "iid" : 49, + "hidden" : false + }, + { + "hidden" : false, + "iid" : 51, + "characteristics" : [ + { + "value" : false, + "format" : "bool", + "type" : "0000011A-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "pw", + "ev" + ], + "iid" : 52 + } + ], + "type" : "00000113-0000-1000-8000-0026BB765291", + "primary" : false + }, + { + "hidden" : false, + "characteristics" : [ + { + "value" : false, + "format" : "bool", + "iid" : 9, + "type" : "0000011A-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "pw", + "ev" + ] + } + ], + "iid" : 8, + "type" : "00000112-0000-1000-8000-0026BB765291", + "primary" : false + }, + { + "hidden" : false, + "iid" : 10, + "characteristics" : [ + { + "iid" : 11, + "type" : "00000022-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "ev" + ], + "value" : false, + "format" : "bool" + }, + { + "perms" : [ + "pr" + ], + "type" : "00000023-0000-1000-8000-0026BB765291", + "iid" : 12, + "format" : "string", + "value" : "Motion Sensor" + } + ], + "type" : "00000085-0000-1000-8000-0026BB765291", + "primary" : false + }, + { + "primary" : false, + "type" : "00000110-0000-1000-8000-0026BB765291", + "characteristics" : [ + { + "format" : "tlv8", + "value" : "AQEA", + "perms" : [ + "pr", + "ev" + ], + "type" : "00000120-0000-1000-8000-0026BB765291", + "iid" : 14 + }, + { + "iid" : 15, + "type" : "00000114-0000-1000-8000-0026BB765291", + "perms" : [ + "pr" + ], + "value" : "AVUBAQACFgEBAQAAAQECAgEAAAACAQIDAQAEAQADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECgAICAmgBAwEeAAADCwECQAECAvAAAwEe", + "format" : "tlv8" + }, + { + "iid" : 16, + "type" : "00000115-0000-1000-8000-0026BB765291", + "perms" : [ + "pr" + ], + "value" : "ARMBAQMCDgEBAQIBAQAAAgEAAwEBAgEA", + "format" : "tlv8" + }, + { + "value" : "AgECAAACAQAAAAIBAQ==", + "format" : "tlv8", + "type" : "00000116-0000-1000-8000-0026BB765291", + "perms" : [ + "pr" + ], + "iid" : 17 + }, + { + "iid" : 19, + "type" : "00000117-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "pw" + ], + "value" : "AQA=", + "format" : "tlv8" + }, + { + "iid" : 18, + "perms" : [ + "pr", + "pw" + ], + "type" : "00000118-0000-1000-8000-0026BB765291", + "format" : "tlv8", + "value" : "ARDpyds+onxNHb4xI0H6deS3AgEAAxgBAQACCzEwLjEwLjYwLjExAwJRwwQCUsMENQEBAQIgyWEU3zQuPNAsFAm1DM3ZSdp0Vh7kGuVQ+vqtS5Qa09YDDr5ebeow7eweCsu3FYh/BTUBAQECIFPPdRRI86ozZNB/WU/e8Em4N1lSsJhttOWoJly3XNEMAw7Zm8TgFdAof+wvoCQTYgYE/r0D9wcEC1Pwjg==" + } + ], + "iid" : 13, + "hidden" : false + }, + { + "iid" : 20, + "characteristics" : [ + { + "iid" : 21, + "type" : "00000120-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "ev" + ], + "value" : "AQEA", + "format" : "tlv8" + }, + { + "format" : "tlv8", + "value" : "AVUBAQACFgEBAQAAAQECAgEAAAACAQIDAQAEAQADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECgAICAmgBAwEeAAADCwECQAECAvAAAwEe", + "perms" : [ + "pr" + ], + "type" : "00000114-0000-1000-8000-0026BB765291", + "iid" : 22 + }, + { + "format" : "tlv8", + "value" : "ARMBAQMCDgEBAQIBAQAAAgEAAwEBAgEA", + "iid" : 23, + "perms" : [ + "pr" + ], + "type" : "00000115-0000-1000-8000-0026BB765291" + }, + { + "format" : "tlv8", + "value" : "AgECAAACAQAAAAIBAQ==", + "perms" : [ + "pr" + ], + "type" : "00000116-0000-1000-8000-0026BB765291", + "iid" : 24 + }, + { + "format" : "tlv8", + "value" : "AQA=", + "perms" : [ + "pr", + "pw" + ], + "type" : "00000117-0000-1000-8000-0026BB765291", + "iid" : 26 + }, + { + "iid" : 25, + "type" : "00000118-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "pw" + ], + "value" : "ARDu4+F49fZMSatQjcfR8FGVAgEAAxgBAQACCzEwLjEwLjYwLjExAwJbwwQCXMMENQEBAQIg9nqVm+80ccYh/S3vKKfbcUGH7VgggHRwp1e1x63+kpkDDnAxnJxfEz8KDp6xKoPhBTUBAQECILLYad+aKdzVbhGz55ywh0RYX9DTyY7HdSRf8y8tUi1kAw4DRngrGhYBdnrjELUzGgYEf+ysuwcESU05wg==", + "format" : "tlv8" + } + ], + "primary" : false, + "type" : "00000110-0000-1000-8000-0026BB765291", + "hidden" : false + } + ] + } +] From 87438dd401ec84a7f53d75b4755a5cc8bda343d0 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 23 May 2021 07:00:24 -0700 Subject: [PATCH 669/852] Add services to SmartTub for changing filtration settings (#46980) Co-authored-by: J. Nick Koston Co-authored-by: Franck Nijhof --- homeassistant/components/smarttub/entity.py | 2 +- homeassistant/components/smarttub/sensor.py | 79 +++++++++++-- .../components/smarttub/services.yaml | 47 ++++++++ tests/components/smarttub/conftest.py | 104 ++++++++++-------- tests/components/smarttub/test_climate.py | 6 +- tests/components/smarttub/test_sensor.py | 26 ++++- 6 files changed, 204 insertions(+), 60 deletions(-) create mode 100644 homeassistant/components/smarttub/services.yaml diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index 58f400597b3..4bf20868ee0 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -1,4 +1,4 @@ -"""SmartTub integration.""" +"""Base classes for SmartTub entities.""" import logging import smarttub diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index ea803c9862b..07866d0b7a4 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -2,7 +2,11 @@ from enum import Enum import logging +import smarttub +import voluptuous as vol + from homeassistant.components.sensor import SensorEntity +from homeassistant.helpers import config_validation as cv, entity_platform from .const import DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubSensorBase @@ -16,6 +20,25 @@ ATTR_MODE = "mode" # the hour of the day at which to start the cycle (0-23) ATTR_START_HOUR = "start_hour" +SET_PRIMARY_FILTRATION_SCHEMA = vol.All( + cv.has_at_least_one_key(ATTR_DURATION, ATTR_START_HOUR), + cv.make_entity_service_schema( + { + vol.Optional(ATTR_DURATION): vol.All(int, vol.Range(min=1, max=24)), + vol.Optional(ATTR_START_HOUR): vol.All(int, vol.Range(min=0, max=23)), + }, + ), +) + +SET_SECONDARY_FILTRATION_SCHEMA = { + vol.Required(ATTR_MODE): vol.In( + { + mode.name.lower() + for mode in smarttub.SpaSecondaryFiltrationCycle.SecondaryFiltrationMode + } + ), +} + async def async_setup_entry(hass, entry, async_add_entities): """Set up sensor entities for the sensors in the tub.""" @@ -45,6 +68,20 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + "set_primary_filtration", + SET_PRIMARY_FILTRATION_SCHEMA, + "async_set_primary_filtration", + ) + + platform.async_register_entity_service( + "set_secondary_filtration", + SET_SECONDARY_FILTRATION_SCHEMA, + "async_set_secondary_filtration", + ) + class SmartTubSensor(SmartTubSensorBase, SensorEntity): """Generic class for SmartTub status sensors.""" @@ -66,22 +103,33 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor): coordinator, spa, "Primary Filtration Cycle", "primary_filtration" ) + @property + def cycle(self) -> smarttub.SpaPrimaryFiltrationCycle: + """Return the underlying smarttub.SpaPrimaryFiltrationCycle object.""" + return self._state + @property def state(self) -> str: """Return the current state of the sensor.""" - return self._state.status.name.lower() + return self.cycle.status.name.lower() @property def extra_state_attributes(self): """Return the state attributes.""" - state = self._state return { - ATTR_DURATION: state.duration, - ATTR_CYCLE_LAST_UPDATED: state.last_updated.isoformat(), - ATTR_MODE: state.mode.name.lower(), - ATTR_START_HOUR: state.start_hour, + ATTR_DURATION: self.cycle.duration, + ATTR_CYCLE_LAST_UPDATED: self.cycle.last_updated.isoformat(), + ATTR_MODE: self.cycle.mode.name.lower(), + ATTR_START_HOUR: self.cycle.start_hour, } + async def async_set_primary_filtration(self, **kwargs): + """Update primary filtration settings.""" + await self.cycle.set( + duration=kwargs.get(ATTR_DURATION), + start_hour=kwargs.get(ATTR_START_HOUR), + ) + class SmartTubSecondaryFiltrationCycle(SmartTubSensor): """The secondary filtration cycle.""" @@ -92,16 +140,27 @@ class SmartTubSecondaryFiltrationCycle(SmartTubSensor): coordinator, spa, "Secondary Filtration Cycle", "secondary_filtration" ) + @property + def cycle(self) -> smarttub.SpaSecondaryFiltrationCycle: + """Return the underlying smarttub.SpaSecondaryFiltrationCycle object.""" + return self._state + @property def state(self) -> str: """Return the current state of the sensor.""" - return self._state.status.name.lower() + return self.cycle.status.name.lower() @property def extra_state_attributes(self): """Return the state attributes.""" - state = self._state return { - ATTR_CYCLE_LAST_UPDATED: state.last_updated.isoformat(), - ATTR_MODE: state.mode.name.lower(), + ATTR_CYCLE_LAST_UPDATED: self.cycle.last_updated.isoformat(), + ATTR_MODE: self.cycle.mode.name.lower(), } + + async def async_set_secondary_filtration(self, **kwargs): + """Update primary filtration settings.""" + mode = smarttub.SpaSecondaryFiltrationCycle.SecondaryFiltrationMode[ + kwargs[ATTR_MODE].upper() + ] + await self.cycle.set_mode(mode) diff --git a/homeassistant/components/smarttub/services.yaml b/homeassistant/components/smarttub/services.yaml new file mode 100644 index 00000000000..30bd225113e --- /dev/null +++ b/homeassistant/components/smarttub/services.yaml @@ -0,0 +1,47 @@ +set_primary_filtration: + name: Update primary filtration settings + description: Updates the primary filtration settings + target: + entity: + integration: smarttub + domain: sensor + fields: + duration: + name: Duration + description: The desired duration of the primary filtration cycle + default: 8 + selector: + number: + min: 1 + max: 24 + unit_of_measurement: "hours" + mode: slider + example: 8 + start_hour: + description: The hour of the day at which to begin the primary filtration cycle + default: 0 + example: 2 + selector: + number: + min: 0 + max: 23 + unit_of_measurement: "hour" + +set_secondary_filtration: + name: Update secondary filtration settings + description: Updates the secondary filtration settings + target: + entity: + integration: smarttub + domain: sensor + fields: + mode: + description: The secondary filtration mode. + selector: + select: + options: + - "frequent" + - "infrequent" + - "away" + required: true + example: "frequent" diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index 84566fcccc5..2b6991fbbe0 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -35,13 +35,65 @@ async def setup_component(hass): @pytest.fixture(name="spa") -def mock_spa(): +def mock_spa(spa_state): """Mock a smarttub.Spa.""" mock_spa = create_autospec(smarttub.Spa, instance=True) mock_spa.id = "mockspa1" mock_spa.brand = "mockbrand1" mock_spa.model = "mockmodel1" + + mock_spa.get_status_full.return_value = spa_state + + mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True) + mock_circulation_pump.id = "CP" + mock_circulation_pump.spa = mock_spa + mock_circulation_pump.state = smarttub.SpaPump.PumpState.OFF + mock_circulation_pump.type = smarttub.SpaPump.PumpType.CIRCULATION + + mock_jet_off = create_autospec(smarttub.SpaPump, instance=True) + mock_jet_off.id = "P1" + mock_jet_off.spa = mock_spa + mock_jet_off.state = smarttub.SpaPump.PumpState.OFF + mock_jet_off.type = smarttub.SpaPump.PumpType.JET + + mock_jet_on = create_autospec(smarttub.SpaPump, instance=True) + mock_jet_on.id = "P2" + mock_jet_on.spa = mock_spa + mock_jet_on.state = smarttub.SpaPump.PumpState.HIGH + mock_jet_on.type = smarttub.SpaPump.PumpType.JET + + spa_state.pumps = [mock_circulation_pump, mock_jet_off, mock_jet_on] + + mock_light_off = create_autospec(smarttub.SpaLight, instance=True) + mock_light_off.spa = mock_spa + mock_light_off.zone = 1 + mock_light_off.intensity = 0 + mock_light_off.mode = smarttub.SpaLight.LightMode.OFF + + mock_light_on = create_autospec(smarttub.SpaLight, instance=True) + mock_light_on.spa = mock_spa + mock_light_on.zone = 2 + mock_light_on.intensity = 50 + mock_light_on.mode = smarttub.SpaLight.LightMode.PURPLE + + spa_state.lights = [mock_light_off, mock_light_on] + + mock_filter_reminder = create_autospec(smarttub.SpaReminder, instance=True) + mock_filter_reminder.id = "FILTER01" + mock_filter_reminder.name = "MyFilter" + mock_filter_reminder.remaining_days = 2 + mock_filter_reminder.snoozed = False + + mock_spa.get_reminders.return_value = [mock_filter_reminder] + + return mock_spa + + +@pytest.fixture(name="spa_state") +def mock_spa_state(): + """Create a smarttub.SpaStateFull with mocks.""" + full_status = smarttub.SpaStateFull( mock_spa, { @@ -73,51 +125,15 @@ def mock_spa(): "pumps": [], }, ) - mock_spa.get_status_full.return_value = full_status - mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True) - mock_circulation_pump.id = "CP" - mock_circulation_pump.spa = mock_spa - mock_circulation_pump.state = smarttub.SpaPump.PumpState.OFF - mock_circulation_pump.type = smarttub.SpaPump.PumpType.CIRCULATION + full_status.primary_filtration.set = create_autospec( + smarttub.SpaPrimaryFiltrationCycle, instance=True + ).set + full_status.secondary_filtration.set_mode = create_autospec( + smarttub.SpaSecondaryFiltrationCycle, instance=True + ).set_mode - mock_jet_off = create_autospec(smarttub.SpaPump, instance=True) - mock_jet_off.id = "P1" - mock_jet_off.spa = mock_spa - mock_jet_off.state = smarttub.SpaPump.PumpState.OFF - mock_jet_off.type = smarttub.SpaPump.PumpType.JET - - mock_jet_on = create_autospec(smarttub.SpaPump, instance=True) - mock_jet_on.id = "P2" - mock_jet_on.spa = mock_spa - mock_jet_on.state = smarttub.SpaPump.PumpState.HIGH - mock_jet_on.type = smarttub.SpaPump.PumpType.JET - - full_status.pumps = [mock_circulation_pump, mock_jet_off, mock_jet_on] - - mock_light_off = create_autospec(smarttub.SpaLight, instance=True) - mock_light_off.spa = mock_spa - mock_light_off.zone = 1 - mock_light_off.intensity = 0 - mock_light_off.mode = smarttub.SpaLight.LightMode.OFF - - mock_light_on = create_autospec(smarttub.SpaLight, instance=True) - mock_light_on.spa = mock_spa - mock_light_on.zone = 2 - mock_light_on.intensity = 50 - mock_light_on.mode = smarttub.SpaLight.LightMode.PURPLE - - full_status.lights = [mock_light_off, mock_light_on] - - mock_filter_reminder = create_autospec(smarttub.SpaReminder, instance=True) - mock_filter_reminder.id = "FILTER01" - mock_filter_reminder.name = "MyFilter" - mock_filter_reminder.remaining_days = 2 - mock_filter_reminder.snoozed = False - - mock_spa.get_reminders.return_value = [mock_filter_reminder] - - return mock_spa + return full_status @pytest.fixture(name="account") diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index a9c2de4e6e2..83a223cee98 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -33,7 +33,7 @@ from homeassistant.const import ( from . import trigger_update -async def test_thermostat_update(spa, setup_entry, hass): +async def test_thermostat_update(spa, spa_state, setup_entry, hass): """Test the thermostat entity.""" entity_id = f"climate.{spa.brand}_{spa.model}_thermostat" @@ -42,7 +42,7 @@ async def test_thermostat_update(spa, setup_entry, hass): assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT - spa.get_status_full.return_value.heater = "OFF" + spa_state.heater = "OFF" await trigger_update(hass) state = hass.states.get(entity_id) @@ -85,7 +85,7 @@ async def test_thermostat_update(spa, setup_entry, hass): ) spa.set_heat_mode.assert_called_with(smarttub.Spa.HeatMode.ECONOMY) - spa.get_status_full.return_value.heat_mode = smarttub.Spa.HeatMode.ECONOMY + spa_state.heat_mode = smarttub.Spa.HeatMode.ECONOMY await trigger_update(hass) state = hass.states.get(entity_id) assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py index 4f179b24910..025e0b6e13e 100644 --- a/tests/components/smarttub/test_sensor.py +++ b/tests/components/smarttub/test_sensor.py @@ -1,6 +1,7 @@ """Test the SmartTub sensor platform.""" import pytest +import smarttub @pytest.mark.parametrize( @@ -23,7 +24,7 @@ async def test_sensor(spa, setup_entry, hass, entity_suffix, expected_state): assert state.state == expected_state -async def test_primary_filtration(spa, setup_entry, hass): +async def test_primary_filtration(spa, spa_state, setup_entry, hass): """Test the primary filtration cycle sensor.""" entity_id = f"sensor.{spa.brand}_{spa.model}_primary_filtration_cycle" @@ -35,8 +36,16 @@ async def test_primary_filtration(spa, setup_entry, hass): assert state.attributes["mode"] == "normal" assert state.attributes["start_hour"] == 2 + await hass.services.async_call( + "smarttub", + "set_primary_filtration", + {"entity_id": entity_id, "duration": 8, "start_hour": 1}, + blocking=True, + ) + spa_state.primary_filtration.set.assert_called_with(duration=8, start_hour=1) -async def test_secondary_filtration(spa, setup_entry, hass): + +async def test_secondary_filtration(spa, spa_state, setup_entry, hass): """Test the secondary filtration cycle sensor.""" entity_id = f"sensor.{spa.brand}_{spa.model}_secondary_filtration_cycle" @@ -45,3 +54,16 @@ async def test_secondary_filtration(spa, setup_entry, hass): assert state.state == "inactive" assert state.attributes["cycle_last_updated"] is not None assert state.attributes["mode"] == "away" + + await hass.services.async_call( + "smarttub", + "set_secondary_filtration", + { + "entity_id": entity_id, + "mode": "frequent", + }, + blocking=True, + ) + spa_state.secondary_filtration.set_mode.assert_called_with( + mode=smarttub.SpaSecondaryFiltrationCycle.SecondaryFiltrationMode.FREQUENT + ) From 4b0b0f5db76058139fe8cf2e7227c3ea216d07fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 May 2021 10:15:38 -0500 Subject: [PATCH 670/852] Allow user to override insecure setup codes and pair with homekit_controller (#50986) * Allow user to override invalid setup codes and pair with homekit_controller * adjust from manual testing * invalid -> insecure --- .../homekit_controller/config_flow.py | 29 ++++++--- .../homekit_controller/strings.json | 4 +- .../homekit_controller/translations/en.json | 2 + .../homekit_controller/test_config_flow.py | 65 ++++++++++++++++++- 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 654bd56c755..5fbd3f3b4cb 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -37,7 +37,7 @@ PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") _LOGGER = logging.getLogger(__name__) -DISALLOWED_CODES = { +INSECURE_CODES = { "00000000", "11111111", "22222222", @@ -66,7 +66,7 @@ def find_existing_host(hass, serial): return entry -def ensure_pin_format(pin): +def ensure_pin_format(pin, allow_insecure_setup_codes=None): """ Ensure a pin code is correctly formatted. @@ -78,8 +78,8 @@ def ensure_pin_format(pin): if not match: raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}") pin_without_dashes = "".join(match.groups()) - if pin_without_dashes in DISALLOWED_CODES: - raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}") + if not allow_insecure_setup_codes and pin_without_dashes in INSECURE_CODES: + raise InsecureSetupCode(f"Invalid PIN code f{pin}") return "-".join(match.groups()) @@ -310,7 +310,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if pair_info and self.finish_pairing: code = pair_info["pairing_code"] try: - code = ensure_pin_format(code) + code = ensure_pin_format( + code, + allow_insecure_setup_codes=pair_info.get( + "allow_insecure_setup_codes" + ), + ) pairing = await self.finish_pairing(code) return await self._entry_from_accessory(pairing) except aiohomekit.exceptions.MalformedPinError: @@ -336,6 +341,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except aiohomekit.AccessoryNotFoundError: # Can no longer find the device on the network return self.async_abort(reason="accessory_not_found_error") + except InsecureSetupCode: + errors["pairing_code"] = "insecure_setup_code" except Exception: # pylint: disable=broad-except _LOGGER.exception("Pairing attempt failed with an unhandled exception") self.finish_pairing = None @@ -399,13 +406,15 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders = {"name": self.name} self.context["title_placeholders"] = {"name": self.name} + schema = {vol.Required("pairing_code"): vol.All(str, vol.Strip)} + if errors and errors.get("pairing_code") == "insecure_setup_code": + schema[vol.Optional("allow_insecure_setup_codes")] = bool + return self.async_show_form( step_id="pair", errors=errors or {}, description_placeholders=placeholders, - data_schema=vol.Schema( - {vol.Required("pairing_code"): vol.All(str, vol.Strip)} - ), + data_schema=vol.Schema(schema), ) async def _entry_from_accessory(self, pairing): @@ -428,3 +437,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): name = get_accessory_name(bridge_info) return self.async_create_entry(title=name, data=pairing_data) + + +class InsecureSetupCode(Exception): + """An exception for insecure trivial setup codes.""" diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index d170693bb6f..7ad868db3fc 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -14,7 +14,8 @@ "title": "Pair with a device via HomeKit Accessory Protocol", "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", "data": { - "pairing_code": "Pairing Code" + "pairing_code": "Pairing Code", + "allow_insecure_setup_codes": "Allow pairing with insecure setup codes." } }, "protocol_error": { @@ -31,6 +32,7 @@ } }, "error": { + "insecure_setup_code": "The requested setup code is insecure because of its trivial nature. This accessory fails to meet basic security requirements.", "unable_to_pair": "Unable to pair, please try again.", "unknown_error": "Device reported an unknown error. Pairing failed.", "authentication_error": "Incorrect HomeKit code. Please check it and try again.", diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index 67409ed02cf..5de3a6c5334 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Incorrect HomeKit code. Please check it and try again.", + "insecure_setup_code": "The requested setup code is insecure because of its trivial nature. This accessory fails to meet basic security requirements.", "max_peers_error": "Device refused to add pairing as it has no free pairing storage.", "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.", "unable_to_pair": "Unable to pair, please try again.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Allow pairing with insecure setup codes.", "pairing_code": "Pairing Code" }, "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 12381614a83..99c6966e827 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -42,6 +42,16 @@ PAIRING_FINISH_ABORT_ERRORS = [ (aiohomekit.AccessoryNotFoundError, "accessory_not_found_error") ] + +INSECURE_PAIRING_CODES = [ + "111-11-111", + "123-45-678", + "22222222", + "111-11-111 ", + " 111-11-111", +] + + INVALID_PAIRING_CODES = [ "aaa-aa-aaa", "aaa-11-aaa", @@ -49,11 +59,8 @@ INVALID_PAIRING_CODES = [ "aaa-aa-111", "1111-1-111", "a111-11-111", - " 111-11-111", - "111-11-111 ", "111-11-111a", "1111111", - "22222222", ] @@ -94,6 +101,15 @@ def test_invalid_pairing_codes(pairing_code): config_flow.ensure_pin_format(pairing_code) +@pytest.mark.parametrize("pairing_code", INSECURE_PAIRING_CODES) +def test_insecure_pairing_codes(pairing_code): + """Test ensure_pin_format raises for an invalid setup code.""" + with pytest.raises(config_flow.InsecureSetupCode): + config_flow.ensure_pin_format(pairing_code) + + config_flow.ensure_pin_format(pairing_code, allow_insecure_setup_codes=True) + + @pytest.mark.parametrize("pairing_code", VALID_PAIRING_CODES) def test_valid_pairing_codes(pairing_code): """Test ensure_pin_format corrects format for a valid pin in an alternative format.""" @@ -624,6 +640,49 @@ async def test_user_works(hass, controller): assert result["title"] == "Koogeek-LS1-20833F" +async def test_user_pairing_with_insecure_setup_code(hass, controller): + """Test user initiated disovers devices.""" + device = setup_mock_accessory(controller) + device.pairing_code = "123-45-678" + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert get_flow_context(hass, result) == { + "source": config_entries.SOURCE_USER, + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"device": "TestDevice"} + ) + assert result["type"] == "form" + assert result["step_id"] == "pair" + + assert get_flow_context(hass, result) == { + "source": config_entries.SOURCE_USER, + "unique_id": "00:00:00:00:00:00", + "title_placeholders": {"name": "TestDevice"}, + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pairing_code": "123-45-678"} + ) + assert result["type"] == "form" + assert result["step_id"] == "pair" + assert result["errors"] == {"pairing_code": "insecure_setup_code"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"pairing_code": "123-45-678", "allow_insecure_setup_codes": True}, + ) + assert result["type"] == "create_entry" + assert result["title"] == "Koogeek-LS1-20833F" + + async def test_user_no_devices(hass, controller): """Test user initiated pairing where no devices discovered.""" result = await hass.config_entries.flow.async_init( From f55213d8b16e169552d1ac8c0afe36d4e7bfe734 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 23 May 2021 17:18:35 +0200 Subject: [PATCH 671/852] Update modbus cover to 100% coverage (#50996) --- .coveragerc | 1 - homeassistant/components/modbus/cover.py | 4 +- .../{test_modbus_cover.py => test_cover.py} | 49 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) rename tests/components/modbus/{test_modbus_cover.py => test_cover.py} (77%) diff --git a/.coveragerc b/.coveragerc index e56773678c2..c24b190f660 100644 --- a/.coveragerc +++ b/.coveragerc @@ -630,7 +630,6 @@ omit = homeassistant/components/mjpeg/camera.py homeassistant/components/mochad/* homeassistant/components/modbus/climate.py - homeassistant/components/modbus/cover.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index f6e1b21b0cf..ca00576770e 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -147,7 +147,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): self._slave, self._register, self._state_open, self._write_type ) self._available = result is not None - self.async_update() + await self.async_update() async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" @@ -155,7 +155,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): self._slave, self._register, self._state_closed, self._write_type ) self._available = result is not None - self.async_update() + await self.async_update() async def async_update(self, now=None): """Update the state of the cover.""" diff --git a/tests/components/modbus/test_modbus_cover.py b/tests/components/modbus/test_cover.py similarity index 77% rename from tests/components/modbus/test_modbus_cover.py rename to tests/components/modbus/test_cover.py index f30ee79bd52..8fbb45fde8e 100644 --- a/tests/components/modbus/test_modbus_cover.py +++ b/tests/components/modbus/test_cover.py @@ -1,6 +1,7 @@ """The tests for the Modbus cover component.""" import logging +from pymodbus.exceptions import ModbusException import pytest from homeassistant.components.cover import DOMAIN as COVER_DOMAIN @@ -24,6 +25,7 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, + STATE_UNAVAILABLE, ) from homeassistant.core import State @@ -246,3 +248,50 @@ async def test_restore_state_cover(hass, state): method_discovery=True, ) assert hass.states.get(entity_id).state == state + + +async def test_service_cover_move(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "cover.test" + entity_id2 = "cover.test2" + config = { + CONF_COVERS: [ + { + CONF_NAME: "test", + CONF_REGISTER: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + { + CONF_NAME: "test2", + CALL_TYPE_COIL: 1234, + }, + ] + } + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OPEN + + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_CLOSED + + mock_pymodbus.read_holding_registers.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + mock_pymodbus.read_coils.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id2}, blocking=True + ) + assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE From dbefa8fac00c942adf2fbcc8a9788243980b45c4 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 23 May 2021 17:51:40 +0200 Subject: [PATCH 672/852] Add strict type annotations to alarm_control_panel (#50945) * Add strict type annotations * Apply suggestions * Type code as optional string --- .strict-typing | 1 + .../alarm_control_panel/__init__.py | 84 ++++++++++--------- .../components/alarm_control_panel/const.py | 24 +++--- .../alarm_control_panel/device_action.py | 23 +++-- .../alarm_control_panel/device_condition.py | 6 +- .../alarm_control_panel/device_trigger.py | 22 +++-- .../alarm_control_panel/reproduce_state.py | 6 +- .../components/arlo/alarm_control_panel.py | 4 +- .../concord232/alarm_control_panel.py | 6 +- .../components/ifttt/alarm_control_panel.py | 4 +- .../components/nx584/alarm_control_panel.py | 6 +- .../template/alarm_control_panel.py | 4 +- .../yale_smart_alarm/alarm_control_panel.py | 4 +- mypy.ini | 11 +++ 14 files changed, 128 insertions(+), 77 deletions(-) diff --git a/.strict-typing b/.strict-typing index 9e02dad19d2..3c409c85448 100644 --- a/.strict-typing +++ b/.strict-typing @@ -10,6 +10,7 @@ homeassistant.components.aftership.* homeassistant.components.air_quality.* homeassistant.components.airly.* homeassistant.components.aladdin_connect.* +homeassistant.components.alarm_control_panel.* homeassistant.components.amazon_polly.* homeassistant.components.ampio.* homeassistant.components.automation.* diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 7d9e47fbcbe..2d6d1f4d5b1 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -1,11 +1,14 @@ """Component to interface with an alarm control panel.""" +from __future__ import annotations + from abc import abstractmethod from datetime import timedelta import logging -from typing import final +from typing import Any, Final, final import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, @@ -16,14 +19,12 @@ from homeassistant.const import ( SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType from .const import ( SUPPORT_ALARM_ARM_AWAY, @@ -33,21 +34,26 @@ from .const import ( SUPPORT_ALARM_TRIGGER, ) -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -DOMAIN = "alarm_control_panel" -SCAN_INTERVAL = timedelta(seconds=30) -ATTR_CHANGED_BY = "changed_by" -FORMAT_TEXT = "text" -FORMAT_NUMBER = "number" -ATTR_CODE_ARM_REQUIRED = "code_arm_required" +DOMAIN: Final = "alarm_control_panel" +SCAN_INTERVAL: Final = timedelta(seconds=30) +ATTR_CHANGED_BY: Final = "changed_by" +FORMAT_TEXT: Final = "text" +FORMAT_NUMBER: Final = "number" +ATTR_CODE_ARM_REQUIRED: Final = "code_arm_required" -ENTITY_ID_FORMAT = DOMAIN + ".{}" +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" -ALARM_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) +ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema( + {vol.Optional(ATTR_CODE): cv.string} +) + +PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for sensors.""" component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL @@ -92,79 +98,81 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class AlarmControlPanelEntity(Entity): """An abstract class for alarm control entities.""" @property - def code_format(self): + def code_format(self) -> str | None: """Regex for code format or None if no code is required.""" return None @property - def changed_by(self): + def changed_by(self) -> str | None: """Last change triggered by.""" return None @property - def code_arm_required(self): + def code_arm_required(self) -> bool: """Whether the code is required for arm actions.""" return True - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" raise NotImplementedError() - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" await self.hass.async_add_executor_job(self.alarm_disarm, code) - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" raise NotImplementedError() - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" await self.hass.async_add_executor_job(self.alarm_arm_home, code) - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" raise NotImplementedError() - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" await self.hass.async_add_executor_job(self.alarm_arm_away, code) - def alarm_arm_night(self, code=None): + def alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" raise NotImplementedError() - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" await self.hass.async_add_executor_job(self.alarm_arm_night, code) - def alarm_trigger(self, code=None): + def alarm_trigger(self, code: str | None = None) -> None: """Send alarm trigger command.""" raise NotImplementedError() - async def async_alarm_trigger(self, code=None): + async def async_alarm_trigger(self, code: str | None = None) -> None: """Send alarm trigger command.""" await self.hass.async_add_executor_job(self.alarm_trigger, code) - def alarm_arm_custom_bypass(self, code=None): + def alarm_arm_custom_bypass(self, code: str | None = None) -> None: """Send arm custom bypass command.""" raise NotImplementedError() - async def async_alarm_arm_custom_bypass(self, code=None): + async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: """Send arm custom bypass command.""" await self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code) @@ -175,7 +183,7 @@ class AlarmControlPanelEntity(Entity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return { ATTR_CODE_FORMAT: self.code_format, @@ -187,9 +195,9 @@ class AlarmControlPanelEntity(Entity): class AlarmControlPanel(AlarmControlPanelEntity): """An abstract class for alarm control entities (for backwards compatibility).""" - def __init_subclass__(cls, **kwargs): + def __init_subclass__(cls, **kwargs: Any) -> None: """Print deprecation warning.""" - super().__init_subclass__(**kwargs) + super().__init_subclass__(**kwargs) # type: ignore[call-arg] _LOGGER.warning( "AlarmControlPanel is deprecated, modify %s to extend AlarmControlPanelEntity", cls.__name__, diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py index 2844cb286ab..36e3b6a13eb 100644 --- a/homeassistant/components/alarm_control_panel/const.py +++ b/homeassistant/components/alarm_control_panel/const.py @@ -1,14 +1,16 @@ """Provides the constants needed for component.""" -SUPPORT_ALARM_ARM_HOME = 1 -SUPPORT_ALARM_ARM_AWAY = 2 -SUPPORT_ALARM_ARM_NIGHT = 4 -SUPPORT_ALARM_TRIGGER = 8 -SUPPORT_ALARM_ARM_CUSTOM_BYPASS = 16 +from typing import Final -CONDITION_TRIGGERED = "is_triggered" -CONDITION_DISARMED = "is_disarmed" -CONDITION_ARMED_HOME = "is_armed_home" -CONDITION_ARMED_AWAY = "is_armed_away" -CONDITION_ARMED_NIGHT = "is_armed_night" -CONDITION_ARMED_CUSTOM_BYPASS = "is_armed_custom_bypass" +SUPPORT_ALARM_ARM_HOME: Final = 1 +SUPPORT_ALARM_ARM_AWAY: Final = 2 +SUPPORT_ALARM_ARM_NIGHT: Final = 4 +SUPPORT_ALARM_TRIGGER: Final = 8 +SUPPORT_ALARM_ARM_CUSTOM_BYPASS: Final = 16 + +CONDITION_TRIGGERED: Final = "is_triggered" +CONDITION_DISARMED: Final = "is_disarmed" +CONDITION_ARMED_HOME: Final = "is_armed_home" +CONDITION_ARMED_AWAY: Final = "is_armed_away" +CONDITION_ARMED_NIGHT: Final = "is_armed_night" +CONDITION_ARMED_CUSTOM_BYPASS: Final = "is_armed_custom_bypass" diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index 9a55998e929..506552f8a50 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -1,6 +1,8 @@ """Provides device automations for Alarm control panel.""" from __future__ import annotations +from typing import Final + import voluptuous as vol from homeassistant.const import ( @@ -21,6 +23,7 @@ from homeassistant.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from . import ATTR_CODE_ARM_REQUIRED, DOMAIN from .const import ( @@ -30,9 +33,15 @@ from .const import ( SUPPORT_ALARM_TRIGGER, ) -ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"} +ACTION_TYPES: Final[set[str]] = { + "arm_away", + "arm_home", + "arm_night", + "disarm", + "trigger", +} -ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( +ACTION_SCHEMA: Final = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), @@ -41,7 +50,9 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: +async def async_get_actions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: """List device actions for Alarm control panel devices.""" registry = await entity_registry.async_get_registry(hass) actions = [] @@ -109,7 +120,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, config: ConfigType, variables: dict, context: Context | None ) -> None: """Execute a device action.""" service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} @@ -132,7 +143,9 @@ async def async_call_action_from_config( ) -async def async_get_action_capabilities(hass, config): +async def async_get_action_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List action capabilities.""" state = hass.states.get(config[CONF_ENTITY_ID]) code_required = state.attributes.get(ATTR_CODE_ARM_REQUIRED) if state else False diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index 3817cf37b45..fa4f903f2e5 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -1,6 +1,8 @@ """Provide the device automations for Alarm control panel.""" from __future__ import annotations +from typing import Final + import voluptuous as vol from homeassistant.components.alarm_control_panel.const import ( @@ -39,7 +41,7 @@ from .const import ( CONDITION_TRIGGERED, ) -CONDITION_TYPES = { +CONDITION_TYPES: Final[set[str]] = { CONDITION_TRIGGERED, CONDITION_DISARMED, CONDITION_ARMED_HOME, @@ -48,7 +50,7 @@ CONDITION_TYPES = { CONDITION_ARMED_CUSTOM_BYPASS, } -CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( +CONDITION_SCHEMA: Final = DEVICE_CONDITION_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index b24716bb43e..477a0c0fe6d 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -1,6 +1,8 @@ """Provides device automations for Alarm control panel.""" from __future__ import annotations +from typing import Final + import voluptuous as vol from homeassistant.components.alarm_control_panel.const import ( @@ -32,10 +34,14 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -BASIC_TRIGGER_TYPES = {"triggered", "disarmed", "arming"} -TRIGGER_TYPES = BASIC_TRIGGER_TYPES | {"armed_home", "armed_away", "armed_night"} +BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"} +TRIGGER_TYPES: Final[set[str]] = BASIC_TRIGGER_TYPES | { + "armed_home", + "armed_away", + "armed_night", +} -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA: Final = TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), @@ -44,10 +50,12 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: """List device triggers for Alarm control panel devices.""" registry = await entity_registry.async_get_registry(hass) - triggers = [] + triggers: list[dict[str, str]] = [] # Get all the integrations entities for this device for entry in entity_registry.async_entries_for_device(registry, device_id): @@ -102,7 +110,9 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: return triggers -async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: +async def async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List trigger capabilities.""" return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py index e7e4c07b8ad..019ba35c013 100644 --- a/homeassistant/components/alarm_control_panel/reproduce_state.py +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable import logging -from typing import Any +from typing import Any, Final from homeassistant.const import ( ATTR_ENTITY_ID, @@ -25,9 +25,9 @@ from homeassistant.core import Context, HomeAssistant, State from . import DOMAIN -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -VALID_STATES = { +VALID_STATES: Final[set[str]] = { STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index dd899cbd04f..91fb2a6a33e 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( @@ -37,7 +37,7 @@ DISARMED = "disarmed" ICON = "mdi:security" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string, diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index f502e805c85..a936c199e61 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -7,7 +7,9 @@ import requests import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, +) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -33,7 +35,7 @@ DEFAULT_MODE = "audible" SCAN_INTERVAL = datetime.timedelta(seconds=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index 519c2e42764..a4240dba177 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, FORMAT_TEXT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( @@ -54,7 +54,7 @@ DEFAULT_EVENT_DISARM = "alarm_disarm" CONF_CODE_ARM_REQUIRED = "code_arm_required" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index d5cdce5d64b..12f47de7060 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -7,7 +7,9 @@ import requests import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, +) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -35,7 +37,7 @@ SERVICE_BYPASS_ZONE = "bypass_zone" SERVICE_UNBYPASS_ZONE = "unbypass_zone" ATTR_ZONE = "zone" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 4c72c5094ef..2706b2d433d 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( ENTITY_ID_FORMAT, FORMAT_NUMBER, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( @@ -68,7 +68,7 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys( ALARM_CONTROL_PANEL_SCHEMA diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index d3504bcc6da..13433086879 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -11,7 +11,7 @@ from yalesmartalarmclient.client import ( ) from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( @@ -36,7 +36,7 @@ DEFAULT_AREA_ID = "1" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/mypy.ini b/mypy.ini index 2cb4d318962..cda4ff75ac3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -121,6 +121,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.alarm_control_panel.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.amazon_polly.*] check_untyped_defs = true disallow_incomplete_defs = true From c1a1a38ffc5711d5a2fddda11a18b570f2268c3f Mon Sep 17 00:00:00 2001 From: HighOnMikey <3639519+HighOnMikey@users.noreply.github.com> Date: Sun, 23 May 2021 12:47:19 -0500 Subject: [PATCH 673/852] Improve legacy support for Hunter Douglas PowerView (#50918) Co-authored-by: J. Nick Koston --- .../hunterdouglas_powerview/__init__.py | 42 +++++------ .../hunterdouglas_powerview/const.py | 21 ++++-- .../hunterdouglas_powerview/entity.py | 21 +++--- .../test_config_flow.py | 75 ++++++++++++++++++- .../hunterdouglas_powerview/fwversion.json | 10 +++ 5 files changed, 126 insertions(+), 43 deletions(-) create mode 100644 tests/fixtures/hunterdouglas_powerview/fwversion.json diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index a25d24fef81..d9a52446028 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from aiopvapi.helpers.aiorequest import AioRequest +from aiopvapi.helpers.api_base import ApiEntryPoint from aiopvapi.helpers.constants import ATTR_ID from aiopvapi.helpers.tools import base64_to_unicode from aiopvapi.rooms import Rooms @@ -20,7 +21,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( + API_PATH_FWVERSION, COORDINATOR, + DEFAULT_LEGACY_MAINPROCESSOR, DEVICE_FIRMWARE, DEVICE_INFO, DEVICE_MAC_ADDRESS, @@ -29,24 +32,18 @@ from .const import ( DEVICE_REVISION, DEVICE_SERIAL_NUMBER, DOMAIN, - FIRMWARE_BUILD, - FIRMWARE_IN_USERDATA, - FIRMWARE_SUB_REVISION, + FIRMWARE, + FIRMWARE_MAINPROCESSOR, + FIRMWARE_NAME, + FIRMWARE_REVISION, HUB_EXCEPTIONS, HUB_NAME, - LEGACY_DEVICE_BUILD, - LEGACY_DEVICE_MODEL, - LEGACY_DEVICE_REVISION, - LEGACY_DEVICE_SUB_REVISION, MAC_ADDRESS_IN_USERDATA, - MAINPROCESSOR_IN_USERDATA_FIRMWARE, - MODEL_IN_MAINPROCESSOR, PV_API, PV_ROOM_DATA, PV_SCENE_DATA, PV_SHADE_DATA, PV_SHADES, - REVISION_IN_MAINPROCESSOR, ROOM_DATA, SCENE_DATA, SERIAL_NUMBER_IN_USERDATA, @@ -137,26 +134,25 @@ async def async_get_device_info(pv_request): resources = await userdata.get_resources() userdata_data = resources[USER_DATA] - if FIRMWARE_IN_USERDATA in userdata_data: - main_processor_info = userdata_data[FIRMWARE_IN_USERDATA][ - MAINPROCESSOR_IN_USERDATA_FIRMWARE - ] - else: + if FIRMWARE in userdata_data: + main_processor_info = userdata_data[FIRMWARE][FIRMWARE_MAINPROCESSOR] + elif userdata_data: # Legacy devices - main_processor_info = { - REVISION_IN_MAINPROCESSOR: LEGACY_DEVICE_REVISION, - FIRMWARE_SUB_REVISION: LEGACY_DEVICE_SUB_REVISION, - FIRMWARE_BUILD: LEGACY_DEVICE_BUILD, - MODEL_IN_MAINPROCESSOR: LEGACY_DEVICE_MODEL, - } + fwversion = ApiEntryPoint(pv_request, API_PATH_FWVERSION) + resources = await fwversion.get_resources() + + if FIRMWARE in resources: + main_processor_info = resources[FIRMWARE][FIRMWARE_MAINPROCESSOR] + else: + main_processor_info = DEFAULT_LEGACY_MAINPROCESSOR return { DEVICE_NAME: base64_to_unicode(userdata_data[HUB_NAME]), DEVICE_MAC_ADDRESS: userdata_data[MAC_ADDRESS_IN_USERDATA], DEVICE_SERIAL_NUMBER: userdata_data[SERIAL_NUMBER_IN_USERDATA], - DEVICE_REVISION: main_processor_info[REVISION_IN_MAINPROCESSOR], + DEVICE_REVISION: main_processor_info[FIRMWARE_REVISION], DEVICE_FIRMWARE: main_processor_info, - DEVICE_MODEL: main_processor_info[MODEL_IN_MAINPROCESSOR], + DEVICE_MODEL: main_processor_info[FIRMWARE_NAME], } diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index e83a9d8945b..e827b055995 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -19,14 +19,11 @@ USER_DATA = "userData" MAC_ADDRESS_IN_USERDATA = "macAddress" SERIAL_NUMBER_IN_USERDATA = "serialNumber" -FIRMWARE_IN_USERDATA = "firmware" -MAINPROCESSOR_IN_USERDATA_FIRMWARE = "mainProcessor" -REVISION_IN_MAINPROCESSOR = "revision" -MODEL_IN_MAINPROCESSOR = "name" HUB_NAME = "hubName" -FIRMWARE_IN_SHADE = "firmware" - +FIRMWARE = "firmware" +FIRMWARE_MAINPROCESSOR = "mainProcessor" +FIRMWARE_NAME = "name" FIRMWARE_REVISION = "revision" FIRMWARE_SUB_REVISION = "subRevision" FIRMWARE_BUILD = "build" @@ -70,4 +67,14 @@ HUB_EXCEPTIONS = (ServerDisconnectedError, asyncio.TimeoutError, PvApiConnection LEGACY_DEVICE_SUB_REVISION = 1 LEGACY_DEVICE_REVISION = 0 LEGACY_DEVICE_BUILD = 0 -LEGACY_DEVICE_MODEL = "PV Hub1.0" +LEGACY_DEVICE_MODEL = "PowerView Hub" + +DEFAULT_LEGACY_MAINPROCESSOR = { + FIRMWARE_REVISION: LEGACY_DEVICE_REVISION, + FIRMWARE_SUB_REVISION: LEGACY_DEVICE_SUB_REVISION, + FIRMWARE_BUILD: LEGACY_DEVICE_BUILD, + FIRMWARE_NAME: LEGACY_DEVICE_MODEL, +} + + +API_PATH_FWVERSION = "api/fwversion" diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 679e55e806c..bf0d5d564ff 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -12,8 +12,8 @@ from .const import ( DEVICE_NAME, DEVICE_SERIAL_NUMBER, DOMAIN, + FIRMWARE, FIRMWARE_BUILD, - FIRMWARE_IN_SHADE, FIRMWARE_REVISION, FIRMWARE_SUB_REVISION, MANUFACTURER, @@ -71,20 +71,21 @@ class ShadeEntity(HDEntity): "name": self._shade_name, "suggested_area": self._room_name, "manufacturer": MANUFACTURER, + "model": self._shade.raw_data[ATTR_TYPE], "via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), } - if FIRMWARE_IN_SHADE not in self._shade.raw_data: - return device_info - - firmware = self._shade.raw_data[FIRMWARE_IN_SHADE] - sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" - model = self._shade.raw_data[ATTR_TYPE] for shade in self._shade.shade_types: - if shade.shade_type == model: - model = shade.description + if shade.shade_type == device_info["model"]: + device_info["model"] = shade.description break + if FIRMWARE not in self._shade.raw_data: + return device_info + + firmware = self._shade.raw_data[FIRMWARE] + sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" + device_info["sw_version"] = sw_version - device_info["model"] = model + return device_info diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 712ebea64a9..6423daff3f8 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -41,12 +41,34 @@ def _get_mock_powerview_userdata(userdata=None, get_resources=None): if not userdata: userdata = json.loads(load_fixture("hunterdouglas_powerview/userdata.json")) if get_resources: - type(mock_powerview_userdata).get_resources = AsyncMock( + mock_powerview_userdata.get_resources = AsyncMock(side_effect=get_resources) + else: + mock_powerview_userdata.get_resources = AsyncMock(return_value=userdata) + return mock_powerview_userdata + + +def _get_mock_powerview_legacy_userdata(userdata=None, get_resources=None): + mock_powerview_userdata_legacy = MagicMock() + if not userdata: + userdata = json.loads(load_fixture("hunterdouglas_powerview/userdata_v1.json")) + if get_resources: + mock_powerview_userdata_legacy.get_resources = AsyncMock( side_effect=get_resources ) else: - type(mock_powerview_userdata).get_resources = AsyncMock(return_value=userdata) - return mock_powerview_userdata + mock_powerview_userdata_legacy.get_resources = AsyncMock(return_value=userdata) + return mock_powerview_userdata_legacy + + +def _get_mock_powerview_fwversion(fwversion=None, get_resources=None): + mock_powerview_fwversion = MagicMock() + if not fwversion: + fwversion = json.loads(load_fixture("hunterdouglas_powerview/fwversion.json")) + if get_resources: + mock_powerview_fwversion.get_resources = AsyncMock(side_effect=get_resources) + else: + mock_powerview_fwversion.get_resources = AsyncMock(return_value=fwversion) + return mock_powerview_fwversion async def test_user_form(hass): @@ -92,6 +114,53 @@ async def test_user_form(hass): assert result4["type"] == "abort" +async def test_user_form_legacy(hass): + """Test we get the user form with a legacy device.""" + 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"] == {} + + mock_powerview_userdata = _get_mock_powerview_legacy_userdata() + mock_powerview_fwversion = _get_mock_powerview_fwversion() + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ), patch( + "homeassistant.components.hunterdouglas_powerview.ApiEntryPoint", + return_value=mock_powerview_fwversion, + ), patch( + "homeassistant.components.hunterdouglas_powerview.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.2.3.4"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "PowerView Hub Gen 1" + assert result2["data"] == { + "host": "1.2.3.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + result3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result3["type"] == "form" + assert result3["errors"] == {} + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {"host": "1.2.3.4"}, + ) + assert result4["type"] == "abort" + + @pytest.mark.parametrize("source, discovery_info", DISCOVERY_DATA) async def test_form_homekit_and_dhcp_cannot_connect(hass, source, discovery_info): """Test we get the form with homekit and dhcp source.""" diff --git a/tests/fixtures/hunterdouglas_powerview/fwversion.json b/tests/fixtures/hunterdouglas_powerview/fwversion.json new file mode 100644 index 00000000000..96d301802ff --- /dev/null +++ b/tests/fixtures/hunterdouglas_powerview/fwversion.json @@ -0,0 +1,10 @@ +{ + "firmware": { + "mainProcessor": { + "name": "PowerView Hub", + "revision": 1, + "subRevision": 1, + "build": 857 + } + } + } \ No newline at end of file From 4d527c5cd200d643ba2c4094d1290c778a31a771 Mon Sep 17 00:00:00 2001 From: Andrew Hayworth Date: Sun, 23 May 2021 12:51:51 -0500 Subject: [PATCH 674/852] Update pylutron-caseta to 0.10.0 (#51005) This update adds support for: - PD-15OUT outdoor switch - RA2 Select fan controller --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index de32b839153..36de8cab15b 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.9.0", "aiolip==1.1.4"], + "requirements": ["pylutron-caseta==0.10.0", "aiolip==1.1.4"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 241e24234ed..a57d3ad43e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1539,7 +1539,7 @@ pylitterbot==2021.3.1 pyloopenergy==0.2.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.9.0 +pylutron-caseta==0.10.0 # homeassistant.components.lutron pylutron==0.2.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddbce293011..5a867997453 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -856,7 +856,7 @@ pylitejet==0.3.0 pylitterbot==2021.3.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.9.0 +pylutron-caseta==0.10.0 # homeassistant.components.mailgun pymailgunner==1.4 From 3cef96e78aeeba7764e54c55361ab9491deb7e0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 May 2021 13:39:22 -0500 Subject: [PATCH 675/852] Update aiohomekit to subscribe more like iOS (#50997) --- homeassistant/components/homekit_controller/connection.py | 2 +- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index eaaab390136..b8713972334 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -267,7 +267,7 @@ class HKDevice: if self._polling_interval_remover: self._polling_interval_remover() - await self.pairing.unsubscribe(self.watchable_characteristics) + await self.pairing.close() return await self.hass.config_entries.async_unload_platforms( self.config_entry, self.platforms diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 1bfa39fab80..ac12c5dc123 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.2.62"], + "requirements": ["aiohomekit==0.2.64"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index a57d3ad43e3..7be73a10d03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.62 +aiohomekit==0.2.64 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a867997453..6578f66c62e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.62 +aiohomekit==0.2.64 # homeassistant.components.emulated_hue # homeassistant.components.http From f0cd87e0315a399390bc671c2d40f891d33cb30f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sun, 23 May 2021 21:06:58 +0200 Subject: [PATCH 676/852] Reduce precision in returned values to meaningful digits (#49382) --- homeassistant/components/fronius/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index a908f2605f8..ac006638912 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -290,6 +290,8 @@ class FroniusTemplateSensor(SensorEntity): """Update the internal state.""" state = self.parent.data.get(self._name) self._state = state.get("value") + if isinstance(self._state, float): + self._state = round(self._state, 2) self._unit = state.get("unit") async def async_added_to_hass(self): From c91f89260e25d5a8cd136b30fa05112862cdd7b1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 23 May 2021 22:10:22 +0200 Subject: [PATCH 677/852] Add `state_class` to entities coming from battery powered devices in Shelly integration (#51009) * Fix sensor state_class * Remove state class from total work time sensor * Add state_class restore mechanism * Remove commented code * Remove unnecessary code --- homeassistant/components/shelly/entity.py | 3 +++ homeassistant/components/shelly/sensor.py | 32 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index bcaa6385100..744272ccf91 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -9,6 +9,7 @@ from typing import Any, Callable import aioshelly import async_timeout +from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.core import callback from homeassistant.helpers import ( device_registry, @@ -168,6 +169,7 @@ class RestAttributeDescription: unit: str | None = None value: Callable[[dict, Any], Any] | None = None device_class: str | None = None + state_class: str | None = None default_enabled: bool = True extra_state_attributes: Callable[[dict], dict | None] | None = None @@ -429,6 +431,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti if last_state is not None: self.last_state = last_state.state + self.description.state_class = last_state.attributes.get(ATTR_STATE_CLASS) @callback def _update_callback(self): diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 006ee166423..a42f38f8a1b 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -30,6 +30,7 @@ SENSORS = { name="Battery", unit=PERCENTAGE, device_class=sensor.DEVICE_CLASS_BATTERY, + state_class=sensor.STATE_CLASS_MEASUREMENT, removal_condition=lambda settings, _: settings.get("external_power") == 1, ), ("device", "deviceTemp"): BlockAttributeDescription( @@ -37,6 +38,7 @@ SENSORS = { unit=temperature_unit, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_TEMPERATURE, + state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, ), ("emeter", "current"): BlockAttributeDescription( @@ -44,12 +46,14 @@ SENSORS = { unit=ELECTRICAL_CURRENT_AMPERE, value=lambda value: value, device_class=sensor.DEVICE_CLASS_CURRENT, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("light", "power"): BlockAttributeDescription( name="Power", unit=POWER_WATT, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_POWER, + state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, ), ("device", "power"): BlockAttributeDescription( @@ -57,60 +61,70 @@ SENSORS = { unit=POWER_WATT, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_POWER, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("emeter", "power"): BlockAttributeDescription( name="Power", unit=POWER_WATT, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_POWER, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("emeter", "voltage"): BlockAttributeDescription( name="Voltage", unit=VOLT, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_VOLTAGE, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("emeter", "powerFactor"): BlockAttributeDescription( name="Power Factor", unit=PERCENTAGE, value=lambda value: round(value * 100, 1), device_class=sensor.DEVICE_CLASS_POWER_FACTOR, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("relay", "power"): BlockAttributeDescription( name="Power", unit=POWER_WATT, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_POWER, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("roller", "rollerPower"): BlockAttributeDescription( name="Power", unit=POWER_WATT, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_POWER, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("device", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("emeter", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("emeter", "energyReturned"): BlockAttributeDescription( name="Energy Returned", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("light", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, + state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, ), ("relay", "energy"): BlockAttributeDescription( @@ -118,17 +132,20 @@ SENSORS = { unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("roller", "rollerEnergy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("sensor", "concentration"): BlockAttributeDescription( name="Gas Concentration", unit=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:gauge", + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("sensor", "extTemp"): BlockAttributeDescription( name="Temperature", @@ -143,17 +160,20 @@ SENSORS = { unit=PERCENTAGE, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_HUMIDITY, + state_class=sensor.STATE_CLASS_MEASUREMENT, available=lambda block: block.extTemp != 999, ), ("sensor", "luminosity"): BlockAttributeDescription( name="Luminosity", unit=LIGHT_LUX, device_class=sensor.DEVICE_CLASS_ILLUMINANCE, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("sensor", "tilt"): BlockAttributeDescription( name="Tilt", unit=DEGREE, icon="mdi:angle-acute", + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("relay", "totalWorkTime"): BlockAttributeDescription( name="Lamp Life", @@ -169,6 +189,7 @@ SENSORS = { unit=VOLT, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_VOLTAGE, + state_class=sensor.STATE_CLASS_MEASUREMENT, ), ("sensor", "sensorOp"): BlockAttributeDescription( name="Operation", @@ -184,6 +205,7 @@ REST_SENSORS = { unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, value=lambda status, _: status["wifi_sta"]["rssi"], device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, ), "uptime": RestAttributeDescription( @@ -218,6 +240,11 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """Return value of sensor.""" return self.attribute_value + @property + def state_class(self): + """State class of sensor.""" + return self.description.state_class + @property def unit_of_measurement(self): """Return unit of sensor.""" @@ -254,6 +281,11 @@ class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): return self.last_state + @property + def state_class(self): + """State class of sensor.""" + return self.description.state_class + @property def unit_of_measurement(self): """Return unit of sensor.""" From e920afd4d871be03e34d72d48bc3bf449d9e91dd Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 24 May 2021 00:12:23 +0000 Subject: [PATCH 678/852] [ci skip] Translation update --- .../homekit_controller/translations/et.json | 2 ++ .../homekit_controller/translations/it.json | 2 ++ .../homekit_controller/translations/ru.json | 2 ++ .../translations/zh-Hant.json | 2 ++ .../translations/it.json | 1 + .../components/isy994/translations/it.json | 8 ++++++++ .../components/samsungtv/translations/ca.json | 9 +++++++-- .../components/samsungtv/translations/it.json | 17 +++++++++++++---- .../components/samsungtv/translations/ru.json | 17 +++++++++++++---- 9 files changed, 50 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homekit_controller/translations/et.json b/homeassistant/components/homekit_controller/translations/et.json index 6a35fd22943..c658213eea0 100644 --- a/homeassistant/components/homekit_controller/translations/et.json +++ b/homeassistant/components/homekit_controller/translations/et.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Vale HomeKiti kood. Kontrolli seda ja proovi uuesti.", + "insecure_setup_code": "Taotletud salas\u00f5na on ebaturvaline, sest see on liiga lihtne ning ei vasta p\u00f5hilistele turvan\u00f5uetele.", "max_peers_error": "Seade keeldus sidumist lisamast kuna puudub piisav salvestusruum.", "pairing_failed": "Selle seadmega sidumise katsel ilmnes tundmatu t\u00f5rge. See v\u00f5ib olla ajutine t\u00f5rge v\u00f5i seadet ei toetata praegu.", "unable_to_pair": "Ei saa siduda, proovi uuesti.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Luba sidumist ebaturvalise salas\u00f5naga.", "pairing_code": "Sidumiskood" }, "description": "HomeKiti kontroller suhtleb seadmega {name} kohtv\u00f5rgu kaudu, kasutades turvalist kr\u00fcpteeritud \u00fchendust ilma eraldi HomeKiti kontrolleri v\u00f5i iCloudita. Selle lisaseadme kasutamiseks sisesta oma HomeKiti sidumiskood (vormingus XXX-XX-XXX). See kood on tavaliselt seadmel v\u00f5i pakendil.", diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index f42a92a8c1a..e5c54a6297e 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Codice HomeKit errato. Per favore, controllate e riprovate.", + "insecure_setup_code": "Il codice di installazione richiesto non \u00e8 sicuro a causa della sua natura banale. Questo accessorio non soddisfa i requisiti di sicurezza di base.", "max_peers_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento in quanto non dispone di una memoria libera per esso.", "pairing_failed": "Si \u00e8 verificato un errore non gestito durante il tentativo di abbinamento con questo dispositivo. Potrebbe trattarsi di un errore temporaneo o il dispositivo potrebbe non essere attualmente supportato.", "unable_to_pair": "Impossibile abbinare, per favore riprova.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Consenti l'associazione con codici di installazione non sicuri.", "pairing_code": "Codice di abbinamento" }, "description": "Il controller HomeKit comunica con {name} sulla rete locale utilizzando una connessione crittografata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.", diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json index 2bb6eefbe7b..d4ab6771ee5 100644 --- a/homeassistant/components/homekit_controller/translations/ru.json +++ b/homeassistant/components/homekit_controller/translations/ru.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 HomeKit. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u0434 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "insecure_setup_code": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d \u0438\u0437-\u0437\u0430 \u0441\u0432\u043e\u0435\u0439 \u0442\u0440\u0438\u0432\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438. \u042d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u043d\u0435 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u043c \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f\u043c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438.", "max_peers_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043a\u043b\u043e\u043d\u0438\u043b\u043e \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0438\u0437-\u0437\u0430 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u044f \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u0430.", "pairing_failed": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0441\u0431\u043e\u0439 \u0438\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0435\u0449\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "unable_to_pair": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\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.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u043c\u0438 \u043a\u043e\u0434\u0430\u043c\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", "pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441 {name} \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440. \u042d\u0442\u043e\u0442 \u043a\u043e\u0434 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u043b\u0438 \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u043a\u0435.", diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index dd0082d8d11..e13b69bd0fd 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Homekit \u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u5b9a\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", + "insecure_setup_code": "\u7531\u65bc\u5176\u7463\u788e\u7279\u6027\u3001\u6240\u8acb\u6c42\u7684\u8a2d\u5b9a\u4ee3\u78bc\u4e0d\u5b89\u5168\u3002\u6b64\u914d\u4ef6\u7121\u6cd5\u9054\u5230\u6700\u4f4e\u5b89\u5168\u9700\u6c42\u3002", "max_peers_error": "\u88dd\u7f6e\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\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", "unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "\u5141\u8a31\u8207\u4e0d\u5b89\u5168\u8a2d\u5b9a\u4ee3\u78bc\u9032\u884c\u914d\u5c0d\u3002", "pairing_code": "\u8a2d\u5b9a\u4ee3\u78bc" }, "description": "\u4f7f\u7528 {name} \u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u88dd\u7f6e\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/it.json b/homeassistant/components/hunterdouglas_powerview/translations/it.json index f8ff617f008..df027146b12 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/it.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/it.json @@ -7,6 +7,7 @@ "cannot_connect": "Impossibile connettersi", "unknown": "Errore imprevisto" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Vuoi impostare {name} ({host})?", diff --git a/homeassistant/components/isy994/translations/it.json b/homeassistant/components/isy994/translations/it.json index 448ecf6d570..1522642e62f 100644 --- a/homeassistant/components/isy994/translations/it.json +++ b/homeassistant/components/isy994/translations/it.json @@ -36,5 +36,13 @@ "title": "Opzioni ISY994" } } + }, + "system_health": { + "info": { + "device_connected": "ISY Connesso", + "host_reachable": "Host raggiungibile", + "last_heartbeat": "Tempo dell'ultimo battito cardiaco", + "websocket_status": "Stato socket evento" + } } } \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json index ee30b508798..9ccff13ae3d 100644 --- a/homeassistant/components/samsungtv/translations/ca.json +++ b/homeassistant/components/samsungtv/translations/ca.json @@ -5,9 +5,14 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 del televisor per autoritzar a Home Assistant.", "cannot_connect": "Ha fallat la connexi\u00f3", - "not_supported": "Actualment aquest televisor Samsung no \u00e9s compatible." + "not_supported": "Actualment aquest televisor Samsung no \u00e9s compatible.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "unknown": "Error inesperat" }, - "flow_title": "{model}", + "error": { + "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 del televisor per autoritzar a Home Assistant." + }, + "flow_title": "{device}", "step": { "confirm": { "description": "Vols configurar el televisior Samsung {model}? Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3. Les configuracions manuals d'aquest televisor es sobreescriuran.", diff --git a/homeassistant/components/samsungtv/translations/it.json b/homeassistant/components/samsungtv/translations/it.json index 46cc5df4edd..ee1219305d7 100644 --- a/homeassistant/components/samsungtv/translations/it.json +++ b/homeassistant/components/samsungtv/translations/it.json @@ -3,16 +3,25 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo Samsung TV. Controlla le impostazioni del tuo TV per autorizzare Home Assistant.", + "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo televisore Samsung. Controlla le impostazioni di Gestione dispositivi esterni della tua TV per autorizzare Home Assistant.", "cannot_connect": "Impossibile connettersi", - "not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato." + "id_missing": "Questo dispositivo Samsung non ha un SerialNumber.", + "not_supported": "Questo dispositivo Samsung non \u00e8 attualmente supportato.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unknown": "Errore imprevisto" }, - "flow_title": "{model}", + "error": { + "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo televisore Samsung. Controlla le impostazioni di Gestione dispositivi esterni della tua TV per autorizzare Home Assistant." + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "Vuoi configurare Samsung TV {model}? Se non hai mai connesso Home Assistant in precedenza, dovresti vedere un messaggio sul tuo TV in cui \u00e8 richiesta l'autorizzazione. Le configurazioni manuali per questo TV verranno sovrascritte.", + "description": "Vuoi configurare {device}? Se non hai mai collegato Home Assistant, dovresti vedere un popup sulla tua TV che chiede l'autorizzazione.", "title": "Samsung TV" }, + "reauth_confirm": { + "description": "Dopo l'invio, accetta il popup su {device} richiedendo l'autorizzazione entro 30 secondi." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/samsungtv/translations/ru.json b/homeassistant/components/samsungtv/translations/ru.json index 48bb435ae61..7d4c24aba45 100644 --- a/homeassistant/components/samsungtv/translations/ru.json +++ b/homeassistant/components/samsungtv/translations/ru.json @@ -3,16 +3,25 @@ "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", - "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430.", + "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Samsung TV. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 External Device Manager \u0412\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "not_supported": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + "id_missing": "\u0423 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Samsung \u043d\u0435\u0442 \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430.", + "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Samsung \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "{model}", + "error": { + "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Samsung TV. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 External Device Manager \u0412\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430." + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung {model}? \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e, \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u044b.", + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {device}? \u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u0440\u0430\u043d\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u043b\u0438 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" }, + "reauth_confirm": { + "description": "\u041f\u043e\u0441\u043b\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u044d\u0442\u043e\u0439 \u0444\u043e\u0440\u043c\u044b, \u043f\u0440\u0438\u043c\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u043e \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u043c \u043e\u043a\u043d\u0435 \u043d\u0430 {device} \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 30 \u0441\u0435\u043a\u0443\u043d\u0434." + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", From 121349f866a31bce17bb6f4c7d3f0fdf2f35609d Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 23 May 2021 18:27:25 -0700 Subject: [PATCH 679/852] Bump python-smarttub to 0.0.25 (#51015) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 00b79681b4f..42858f69b39 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": ["python-smarttub==0.0.24"], + "requirements": ["python-smarttub==0.0.25"], "quality_scale": "platinum", "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7be73a10d03..6b72c9f6c6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1858,7 +1858,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.24 +python-smarttub==0.0.25 # homeassistant.components.sochain python-sochain-api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6578f66c62e..5a06d15465e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1016,7 +1016,7 @@ python-openzwave-mqtt[mqtt-client]==1.4.0 python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.24 +python-smarttub==0.0.25 # homeassistant.components.songpal python-songpal==0.12 From d7da32cbb93dfb5707e190a56f672957747c77d9 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 23 May 2021 18:27:54 -0700 Subject: [PATCH 680/852] Add refresh when changing SmartTub filtration settings (#51014) --- homeassistant/components/smarttub/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index 07866d0b7a4..95a862502cd 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -129,6 +129,7 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor): duration=kwargs.get(ATTR_DURATION), start_hour=kwargs.get(ATTR_START_HOUR), ) + await self.coordinator.async_request_refresh() class SmartTubSecondaryFiltrationCycle(SmartTubSensor): @@ -164,3 +165,4 @@ class SmartTubSecondaryFiltrationCycle(SmartTubSensor): kwargs[ATTR_MODE].upper() ] await self.cycle.set_mode(mode) + await self.coordinator.async_request_refresh() From 0bba0f07acbb6901d37e3db4d026bb6c5006a446 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Mon, 24 May 2021 08:48:28 +0200 Subject: [PATCH 681/852] Add SIA Alarm systems (#36625) * initial commit of SIA integration * translations * moved reactions to file, typed everything * fixed no-else-return 3 times * refactored config and fix coverage of test * fix requirements_test * elimated another platform * forgot some mentions of sensor * updated config flow steps, fixed restore and small edits * fixed pylint * updated config_flow with better schema, small fixes from review * final comment and small legibility enhancements * small fix for pylint * fixed init * fixes for botched rebase * fixed port string * updated common strings * rebuild component with eventbus * fixed pylint and tests * updates based on review by @bdraco * updates based on new version of package and reviews * small updates with latest package * added raise from * deleted async_setup from test * fixed tests * removed unused code from addititional account step * fixed typo in strings * clarification and update to update_data func * added iot_class to manifest * fixed entity and unique id setup * small fix in tests * improved unique_id semantics and load/unload functions * added typing in order to fix mypy * further fixes for typing * final fixes for mypy * adding None return types * fix hub DR identifier * rebased, added DeviceInfo * rewrite to clean up and make it easier to read * replaced functions with format for id and name * renamed tracker remover small fix in state.setter * improved readibility of state.setter * no more state.setter and small updates * mypy fix * fixed and improved config flow * added fixtures to test and other cleaner test code * removed timeband from config, will reintro in a options flow * removed timeband from tests * added options flow for zones and timestamps * removed type ignore * replaced mapping with collections.abc --- .coveragerc | 5 + .pre-commit-config.yaml | 2 +- CODEOWNERS | 1 + homeassistant/components/sia/__init__.py | 34 ++ .../components/sia/alarm_control_panel.py | 253 ++++++++++++++ homeassistant/components/sia/config_flow.py | 232 +++++++++++++ homeassistant/components/sia/const.py | 38 +++ homeassistant/components/sia/hub.py | 138 ++++++++ homeassistant/components/sia/manifest.json | 9 + homeassistant/components/sia/strings.json | 50 +++ homeassistant/components/sia/utils.py | 57 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/sia/__init__.py | 1 + tests/components/sia/test_config_flow.py | 314 ++++++++++++++++++ 16 files changed, 1140 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sia/__init__.py create mode 100644 homeassistant/components/sia/alarm_control_panel.py create mode 100644 homeassistant/components/sia/config_flow.py create mode 100644 homeassistant/components/sia/const.py create mode 100644 homeassistant/components/sia/hub.py create mode 100644 homeassistant/components/sia/manifest.json create mode 100644 homeassistant/components/sia/strings.json create mode 100644 homeassistant/components/sia/utils.py create mode 100644 tests/components/sia/__init__.py create mode 100644 tests/components/sia/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index c24b190f660..5fc54f1dbea 100644 --- a/.coveragerc +++ b/.coveragerc @@ -916,6 +916,11 @@ omit = homeassistant/components/skybeacon/sensor.py homeassistant/components/skybell/* homeassistant/components/slack/notify.py + homeassistant/components/sia/__init__.py + homeassistant/components/sia/alarm_control_panel.py + homeassistant/components/sia/const.py + homeassistant/components/sia/hub.py + homeassistant/components/sia/utils.py homeassistant/components/sinch/* homeassistant/components/slide/* homeassistant/components/sma/__init__.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66e10b18767..9ead1fd09bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort + - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] diff --git a/CODEOWNERS b/CODEOWNERS index a4eb7a07987..e7f20d27e18 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -431,6 +431,7 @@ homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74 homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff +homeassistant/components/sia/* @eavanvalkenburg homeassistant/components/sighthound/* @robmarkcole homeassistant/components/signal_messenger/* @bbernhard homeassistant/components/simplisafe/* @bachya diff --git a/homeassistant/components/sia/__init__.py b/homeassistant/components/sia/__init__.py new file mode 100644 index 00000000000..9bca9a5f5b2 --- /dev/null +++ b/homeassistant/components/sia/__init__.py @@ -0,0 +1,34 @@ +"""The sia integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN, PLATFORMS +from .hub import SIAHub + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up sia from a config entry.""" + hub: SIAHub = SIAHub(hass, entry) + await hub.async_setup_hub() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = hub + try: + await hub.sia_client.start(reuse_port=True) + except OSError as exc: + raise ConfigEntryNotReady( + f"SIA Server at port {entry.data[CONF_PORT]} could not start." + ) from exc + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hub: SIAHub = hass.data[DOMAIN].pop(entry.entry_id) + await hub.async_shutdown() + return unload_ok diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py new file mode 100644 index 00000000000..74bb48be940 --- /dev/null +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -0,0 +1,253 @@ +"""Module for SIA Alarm Control Panels.""" +from __future__ import annotations + +import logging +from typing import Any, Callable + +from pysiaalarm import SIAEvent + +from homeassistant.components.alarm_control_panel import ( + ENTITY_ID_FORMAT as ALARM_ENTITY_ID_FORMAT, + AlarmControlPanelEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_PORT, + CONF_ZONE, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_UNAVAILABLE, +) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import StateType + +from .const import ( + CONF_ACCOUNT, + CONF_ACCOUNTS, + CONF_PING_INTERVAL, + CONF_ZONES, + DOMAIN, + SIA_ENTITY_ID_FORMAT, + SIA_EVENT, + SIA_NAME_FORMAT, + SIA_UNIQUE_ID_FORMAT_ALARM, +) +from .utils import get_attr_from_sia_event, get_unavailability_interval + +_LOGGER = logging.getLogger(__name__) + +DEVICE_CLASS_ALARM = "alarm" +PREVIOUS_STATE = "previous_state" + +CODE_CONSEQUENCES: dict[str, StateType] = { + "PA": STATE_ALARM_TRIGGERED, + "JA": STATE_ALARM_TRIGGERED, + "TA": STATE_ALARM_TRIGGERED, + "BA": STATE_ALARM_TRIGGERED, + "CA": STATE_ALARM_ARMED_AWAY, + "CB": STATE_ALARM_ARMED_AWAY, + "CG": STATE_ALARM_ARMED_AWAY, + "CL": STATE_ALARM_ARMED_AWAY, + "CP": STATE_ALARM_ARMED_AWAY, + "CQ": STATE_ALARM_ARMED_AWAY, + "CS": STATE_ALARM_ARMED_AWAY, + "CF": STATE_ALARM_ARMED_CUSTOM_BYPASS, + "OA": STATE_ALARM_DISARMED, + "OB": STATE_ALARM_DISARMED, + "OG": STATE_ALARM_DISARMED, + "OP": STATE_ALARM_DISARMED, + "OQ": STATE_ALARM_DISARMED, + "OR": STATE_ALARM_DISARMED, + "OS": STATE_ALARM_DISARMED, + "NC": STATE_ALARM_ARMED_NIGHT, + "NL": STATE_ALARM_ARMED_NIGHT, + "BR": PREVIOUS_STATE, + "NP": PREVIOUS_STATE, + "NO": PREVIOUS_STATE, +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[..., None], +) -> bool: + """Set up SIA alarm_control_panel(s) from a config entry.""" + async_add_entities( + [ + SIAAlarmControlPanel(entry, account_data, zone) + for account_data in entry.data[CONF_ACCOUNTS] + for zone in range( + 1, + entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES] + + 1, + ) + ] + ) + return True + + +class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): + """Class for SIA Alarm Control Panels.""" + + def __init__( + self, + entry: ConfigEntry, + account_data: dict[str, Any], + zone: int, + ): + """Create SIAAlarmControlPanel object.""" + self._entry: ConfigEntry = entry + self._account_data: dict[str, Any] = account_data + self._zone: int = zone + + self._port: int = self._entry.data[CONF_PORT] + self._account: str = self._account_data[CONF_ACCOUNT] + self._ping_interval: int = self._account_data[CONF_PING_INTERVAL] + + self.entity_id: str = ALARM_ENTITY_ID_FORMAT.format( + SIA_ENTITY_ID_FORMAT.format( + self._port, self._account, self._zone, DEVICE_CLASS_ALARM + ) + ) + + self._attr: dict[str, Any] = { + CONF_PORT: self._port, + CONF_ACCOUNT: self._account, + CONF_ZONE: self._zone, + CONF_PING_INTERVAL: f"{self._ping_interval} minute(s)", + } + + self._available: bool = True + self._state: StateType = None + self._old_state: StateType = None + self._cancel_availability_cb: CALLBACK_TYPE | None = None + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass. + + Overridden from Entity. + + 1. start the event listener and add the callback to on_remove + 2. get previous state from storage + 3. if previous state: restore + 4. if previous state is unavailable: set _available to False and return + 5. if available: create availability cb + """ + self.async_on_remove( + self.hass.bus.async_listen( + event_type=SIA_EVENT.format(self._port, self._account), + listener=self.async_handle_event, + ) + ) + last_state = await self.async_get_last_state() + if last_state is not None: + self._state = last_state.state + if self.state == STATE_UNAVAILABLE: + self._available = False + return + self._cancel_availability_cb = self.async_create_availability_cb() + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass. + + Overridden from Entity. + """ + if self._cancel_availability_cb: + self._cancel_availability_cb() + + async def async_handle_event(self, event: Event) -> None: + """Listen to events for this port and account and update state and attributes. + + If the port and account combo receives any message it means it is online and can therefore be set to available. + """ + sia_event: SIAEvent = SIAEvent.from_dict( # pylint: disable=no-member + event.data + ) + _LOGGER.debug("Received event: %s", sia_event) + if int(sia_event.ri) == self._zone: + self._attr.update(get_attr_from_sia_event(sia_event)) + new_state = CODE_CONSEQUENCES.get(sia_event.code, None) + if new_state is not None: + if new_state == PREVIOUS_STATE: + new_state = self._old_state + self._state, self._old_state = new_state, self._state + self._available = True + self.async_write_ha_state() + self.async_reset_availability_cb() + + @callback + def async_reset_availability_cb(self) -> None: + """Reset availability cb by cancelling the current and creating a new one.""" + if self._cancel_availability_cb: + self._cancel_availability_cb() + self._cancel_availability_cb = self.async_create_availability_cb() + + @callback + def async_create_availability_cb(self) -> CALLBACK_TYPE: + """Create a availability cb and return the callback.""" + return async_call_later( + self.hass, + get_unavailability_interval(self._ping_interval), + self.async_set_unavailable, + ) + + @callback + def async_set_unavailable(self, _) -> None: + """Set unavailable.""" + self._available = False + self.async_write_ha_state() + + @property + def state(self) -> StateType: + """Get state.""" + return self._state + + @property + def name(self) -> str: + """Get Name.""" + return SIA_NAME_FORMAT.format( + self._port, self._account, self._zone, DEVICE_CLASS_ALARM + ) + + @property + def unique_id(self) -> str: + """Get unique_id.""" + return SIA_UNIQUE_ID_FORMAT_ALARM.format( + self._entry.entry_id, self._account, self._zone + ) + + @property + def available(self) -> bool: + """Get availability.""" + return self._available + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return device attributes.""" + return self._attr + + @property + def should_poll(self) -> bool: + """Return False if entity pushes its state to HA.""" + return False + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return 0 + + @property + def device_info(self) -> DeviceInfo: + """Return the device_info.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "via_device": (DOMAIN, f"{self._port}_{self._account}"), + } diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py new file mode 100644 index 00000000000..fe49ec65777 --- /dev/null +++ b/homeassistant/components/sia/config_flow.py @@ -0,0 +1,232 @@ +"""Config flow for sia integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from copy import deepcopy +import logging +from typing import Any + +from pysiaalarm import ( + InvalidAccountFormatError, + InvalidAccountLengthError, + InvalidKeyFormatError, + InvalidKeyLengthError, + SIAAccount, +) +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PORT, CONF_PROTOCOL +from homeassistant.core import callback +from homeassistant.helpers.typing import ConfigType + +from .const import ( + CONF_ACCOUNT, + CONF_ACCOUNTS, + CONF_ADDITIONAL_ACCOUNTS, + CONF_ENCRYPTION_KEY, + CONF_IGNORE_TIMESTAMPS, + CONF_PING_INTERVAL, + CONF_ZONES, + DOMAIN, + TITLE, +) +from .hub import SIAHub + +_LOGGER = logging.getLogger(__name__) + + +HUB_SCHEMA = vol.Schema( + { + vol.Required(CONF_PORT): int, + vol.Optional(CONF_PROTOCOL, default="TCP"): vol.In(["TCP", "UDP"]), + vol.Required(CONF_ACCOUNT): str, + vol.Optional(CONF_ENCRYPTION_KEY): str, + vol.Required(CONF_PING_INTERVAL, default=1): int, + vol.Required(CONF_ZONES, default=1): int, + vol.Optional(CONF_ADDITIONAL_ACCOUNTS, default=False): bool, + } +) + +ACCOUNT_SCHEMA = vol.Schema( + { + vol.Required(CONF_ACCOUNT): str, + vol.Optional(CONF_ENCRYPTION_KEY): str, + vol.Required(CONF_PING_INTERVAL, default=1): int, + vol.Required(CONF_ZONES, default=1): int, + vol.Optional(CONF_ADDITIONAL_ACCOUNTS, default=False): bool, + } +) + +DEFAULT_OPTIONS = {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: None} + + +def validate_input(data: ConfigType) -> dict[str, str] | None: + """Validate the input by the user.""" + try: + SIAAccount.validate_account(data[CONF_ACCOUNT], data.get(CONF_ENCRYPTION_KEY)) + except InvalidKeyFormatError: + return {"base": "invalid_key_format"} + except InvalidKeyLengthError: + return {"base": "invalid_key_length"} + except InvalidAccountFormatError: + return {"base": "invalid_account_format"} + except InvalidAccountLengthError: + return {"base": "invalid_account_length"} + except Exception as exc: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception from SIAAccount: %s", exc) + return {"base": "unknown"} + if not 1 <= data[CONF_PING_INTERVAL] <= 1440: + return {"base": "invalid_ping"} + return validate_zones(data) + + +def validate_zones(data: ConfigType) -> dict[str, str] | None: + """Validate the zones field.""" + if data[CONF_ZONES] == 0: + return {"base": "invalid_zones"} + return None + + +class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for sia.""" + + VERSION: int = 1 + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return SIAOptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize the config flow.""" + self._data: ConfigType = {} + self._options: Mapping[str, Any] = {CONF_ACCOUNTS: {}} + + async def async_step_user(self, user_input: ConfigType = None): + """Handle the initial user step.""" + errors: dict[str, str] | None = None + if user_input is not None: + errors = validate_input(user_input) + if user_input is None or errors is not None: + return self.async_show_form( + step_id="user", data_schema=HUB_SCHEMA, errors=errors + ) + return await self.async_handle_data_and_route(user_input) + + async def async_step_add_account(self, user_input: ConfigType = None): + """Handle the additional accounts steps.""" + errors: dict[str, str] | None = None + if user_input is not None: + errors = validate_input(user_input) + if user_input is None or errors is not None: + return self.async_show_form( + step_id="add_account", data_schema=ACCOUNT_SCHEMA, errors=errors + ) + return await self.async_handle_data_and_route(user_input) + + async def async_handle_data_and_route(self, user_input: ConfigType): + """Handle the user_input, check if configured and route to the right next step or create entry.""" + self._update_data(user_input) + if self._data and self._port_already_configured(): + return self.async_abort(reason="already_configured") + + if user_input[CONF_ADDITIONAL_ACCOUNTS]: + return await self.async_step_add_account() + return self.async_create_entry( + title=TITLE.format(self._data[CONF_PORT]), + data=self._data, + options=self._options, + ) + + def _update_data(self, user_input: ConfigType) -> None: + """Parse the user_input and store in data and options attributes. + + If there is a port in the input or no data, assume it is fully new and overwrite. + Add the default options and overwrite the zones in options. + """ + if not self._data or user_input.get(CONF_PORT): + self._data = { + CONF_PORT: user_input[CONF_PORT], + CONF_PROTOCOL: user_input[CONF_PROTOCOL], + CONF_ACCOUNTS: [], + } + account = user_input[CONF_ACCOUNT] + self._data[CONF_ACCOUNTS].append( + { + CONF_ACCOUNT: account, + CONF_ENCRYPTION_KEY: user_input.get(CONF_ENCRYPTION_KEY), + CONF_PING_INTERVAL: user_input[CONF_PING_INTERVAL], + } + ) + self._options[CONF_ACCOUNTS].setdefault(account, deepcopy(DEFAULT_OPTIONS)) + self._options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] + + def _port_already_configured(self): + """See if we already have a SIA entry matching the port.""" + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_PORT] == self._data[CONF_PORT]: + return True + return False + + +class SIAOptionsFlowHandler(config_entries.OptionsFlow): + """Handle SIA options.""" + + def __init__(self, config_entry): + """Initialize SIA options flow.""" + self.config_entry = config_entry + self.options = deepcopy(dict(config_entry.options)) + self.hub: SIAHub | None = None + self.accounts_todo: list = [] + + async def async_step_init(self, user_input: ConfigType = None): + """Manage the SIA options.""" + self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id] + if self.hub is not None and self.hub.sia_accounts is not None: + self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] + return await self.async_step_options() + + async def async_step_options(self, user_input: ConfigType = None): + """Create the options step for a account.""" + errors: dict[str, str] | None = None + if user_input is not None: + errors = validate_zones(user_input) + if user_input is None or errors is not None: + account = self.accounts_todo[0] + return self.async_show_form( + step_id="options", + description_placeholders={"account": account}, + data_schema=vol.Schema( + { + vol.Optional( + CONF_ZONES, + default=self.options[CONF_ACCOUNTS][account][CONF_ZONES], + ): int, + vol.Optional( + CONF_IGNORE_TIMESTAMPS, + default=self.options[CONF_ACCOUNTS][account][ + CONF_IGNORE_TIMESTAMPS + ], + ): bool, + } + ), + errors=errors, + last_step=self.last_step, + ) + + account = self.accounts_todo.pop(0) + self.options[CONF_ACCOUNTS][account][CONF_IGNORE_TIMESTAMPS] = user_input[ + CONF_IGNORE_TIMESTAMPS + ] + self.options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] + if self.accounts_todo: + return await self.async_step_options() + _LOGGER.warning("Updating SIA Options with %s", self.options) + return self.async_create_entry(title="", data=self.options) + + @property + def last_step(self) -> bool: + """Return if this is the last step.""" + return len(self.accounts_todo) <= 1 diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py new file mode 100644 index 00000000000..ceeaac75923 --- /dev/null +++ b/homeassistant/components/sia/const.py @@ -0,0 +1,38 @@ +"""Constants for the sia integration.""" +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) + +PLATFORMS = [ALARM_CONTROL_PANEL_DOMAIN] + +CONF_ACCOUNT = "account" +CONF_ACCOUNTS = "accounts" +CONF_ADDITIONAL_ACCOUNTS = "additional_account" +CONF_PING_INTERVAL = "ping_interval" +CONF_ENCRYPTION_KEY = "encryption_key" +CONF_ZONES = "zones" +CONF_IGNORE_TIMESTAMPS = "ignore_timestamps" + +DOMAIN = "sia" +TITLE = "SIA Alarm on port {}" +SIA_EVENT = "sia_event_{}_{}" +SIA_NAME_FORMAT = "{} - {} - zone {} - {}" +SIA_NAME_FORMAT_HUB = "{} - {} - {}" +SIA_ENTITY_ID_FORMAT = "{}_{}_{}_{}" +SIA_ENTITY_ID_FORMAT_HUB = "{}_{}_{}" +SIA_UNIQUE_ID_FORMAT_ALARM = "{}_{}_{}" +SIA_UNIQUE_ID_FORMAT = "{}_{}_{}_{}" +HUB_SENSOR_NAME = "last_heartbeat" +HUB_ZONE = 0 +PING_INTERVAL_MARGIN = 30 + +DEFAULT_TIMEBAND = (80, 40) +IGNORED_TIMEBAND = (3600, 1800) + +EVENT_CODE = "last_code" +EVENT_ACCOUNT = "account" +EVENT_ZONE = "zone" +EVENT_PORT = "port" +EVENT_MESSAGE = "last_message" +EVENT_ID = "last_id" +EVENT_TIMESTAMP = "last_timestamp" diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py new file mode 100644 index 00000000000..f5d48a3282c --- /dev/null +++ b/homeassistant/components/sia/hub.py @@ -0,0 +1,138 @@ +"""The sia hub.""" +from __future__ import annotations + +from copy import deepcopy +import logging +from typing import Any + +from pysiaalarm.aio import CommunicationsProtocol, SIAAccount, SIAClient, SIAEvent + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PORT, CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, EventOrigin, HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .const import ( + CONF_ACCOUNT, + CONF_ACCOUNTS, + CONF_ENCRYPTION_KEY, + CONF_IGNORE_TIMESTAMPS, + CONF_ZONES, + DEFAULT_TIMEBAND, + DOMAIN, + IGNORED_TIMEBAND, + PLATFORMS, + SIA_EVENT, +) + +_LOGGER = logging.getLogger(__name__) + + +class SIAHub: + """Class for SIA Hubs.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + ): + """Create the SIAHub.""" + self._hass: HomeAssistant = hass + self._entry: ConfigEntry = entry + self._port: int = int(entry.data[CONF_PORT]) + self._title: str = entry.title + self._accounts: list[dict[str, Any]] = deepcopy(entry.data[CONF_ACCOUNTS]) + self._protocol: str = entry.data[CONF_PROTOCOL] + self.sia_accounts: list[SIAAccount] | None = None + self.sia_client: SIAClient = None + + async def async_setup_hub(self) -> None: + """Add a device to the device_registry, register shutdown listener, load reactions.""" + self.update_accounts() + device_registry = await dr.async_get_registry(self._hass) + for acc in self._accounts: + account = acc[CONF_ACCOUNT] + device_registry.async_get_or_create( + config_entry_id=self._entry.entry_id, + identifiers={(DOMAIN, f"{self._port}_{account}")}, + name=f"{self._port} - {account}", + ) + self._entry.async_on_unload( + self._entry.add_update_listener(self.async_config_entry_updated) + ) + self._entry.async_on_unload( + self._hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.async_shutdown) + ) + + async def async_shutdown(self, _: Event = None) -> None: + """Shutdown the SIA server.""" + await self.sia_client.stop() + + async def async_create_and_fire_event(self, event: SIAEvent) -> None: + """Create a event on HA's bus, with the data from the SIAEvent. + + The created event is handled by default for only a small subset for each platform (there are about 320 SIA Codes defined, only 22 of those are used in the alarm_control_panel), a user can choose to build other automation or even entities on the same event for SIA codes not handled by the built-in platforms. + + """ + _LOGGER.debug( + "Adding event to bus for code %s for port %s and account %s", + event.code, + self._port, + event.account, + ) + self._hass.bus.async_fire( + event_type=SIA_EVENT.format(self._port, event.account), + event_data=event.to_dict(encode_json=True), + origin=EventOrigin.remote, + ) + + def update_accounts(self): + """Update the SIA_Accounts variable.""" + self._load_options() + self.sia_accounts = [ + SIAAccount( + account_id=a[CONF_ACCOUNT], + key=a.get(CONF_ENCRYPTION_KEY), + allowed_timeband=IGNORED_TIMEBAND + if a[CONF_IGNORE_TIMESTAMPS] + else DEFAULT_TIMEBAND, + ) + for a in self._accounts + ] + if self.sia_client is not None: + self.sia_client.accounts = self.sia_accounts + return + self.sia_client = SIAClient( + host="", + port=self._port, + accounts=self.sia_accounts, + function=self.async_create_and_fire_event, + protocol=CommunicationsProtocol(self._protocol), + ) + + def _load_options(self) -> None: + """Store attributes to avoid property call overhead since they are called frequently.""" + options = dict(self._entry.options) + for acc in self._accounts: + acc_id = acc[CONF_ACCOUNT] + if acc_id in options[CONF_ACCOUNTS].keys(): + acc[CONF_IGNORE_TIMESTAMPS] = options[CONF_ACCOUNTS][acc_id][ + CONF_IGNORE_TIMESTAMPS + ] + acc[CONF_ZONES] = options[CONF_ACCOUNTS][acc_id][CONF_ZONES] + + @staticmethod + async def async_config_entry_updated( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> None: + """Handle signals of config entry being updated. + + First, update the accounts, this will reflect any changes with ignore_timestamps. + Second, unload underlying platforms, and then setup platforms, this reflects any changes in number of zones. + + """ + if not (hub := hass.data[DOMAIN].get(config_entry.entry_id)): + return + hub.update_accounts() + await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/sia/manifest.json b/homeassistant/components/sia/manifest.json new file mode 100644 index 00000000000..67c2a0e91a1 --- /dev/null +++ b/homeassistant/components/sia/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "sia", + "name": "SIA Alarm Systems", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sia", + "requirements": ["pysiaalarm==3.0.0b12"], + "codeowners": ["@eavanvalkenburg"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/sia/strings.json b/homeassistant/components/sia/strings.json new file mode 100644 index 00000000000..b091fdd341d --- /dev/null +++ b/homeassistant/components/sia/strings.json @@ -0,0 +1,50 @@ +{ + "title": "SIA Alarm Systems", + "config": { + "step": { + "user": { + "data": { + "port": "[%key:common::config_flow::data::port%]", + "protocol": "Protocol", + "account": "Account ID", + "encryption_key": "Encryption Key", + "ping_interval": "Ping Interval (min)", + "zones": "Number of zones for the account", + "additional_account": "Additional accounts" + }, + "title": "Create a connection for SIA based alarm systems." + }, + "additional_account": { + "data": { + "account": "[%key:component::sia::config::step::user::data::account%]", + "encryption_key": "[%key:component::sia::config::step::user::data::encryption_key%]", + "ping_interval": "[%key:component::sia::config::step::user::data::ping_interval%]", + "zones": "[%key:component::sia::config::step::user::data::zones%]", + "additional_account": "[%key:component::sia::config::step::user::data::additional_account%]" + }, + "title": "Add another account to the current port." + } + }, + "error": { + "invalid_key_format": "The key is not a hex value, please use only 0-9 and A-F.", + "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 characters hex characters.", + "invalid_account_format": "The account is not a hex value, please use only 0-9 and A-F.", + "invalid_account_length": "The account is not the right length, it has to be between 3 and 16 characters.", + "invalid_ping": "The ping interval needs to be between 1 and 1440 minutes.", + "invalid_zones": "There needs to be at least 1 zone.", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignore the timestamp check of the SIA events", + "zones": "[%key:component::sia::config::step::user::data::zones%]" + }, + "description": "Set the options for account: {account}", + "title": "Options for the SIA Setup." + } + } + } +} diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py new file mode 100644 index 00000000000..9b02025aa8d --- /dev/null +++ b/homeassistant/components/sia/utils.py @@ -0,0 +1,57 @@ +"""Helper functions for the SIA integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from pysiaalarm import SIAEvent + +from homeassistant.const import DEVICE_CLASS_TIMESTAMP + +from .const import ( + EVENT_ACCOUNT, + EVENT_CODE, + EVENT_ID, + EVENT_MESSAGE, + EVENT_TIMESTAMP, + EVENT_ZONE, + HUB_SENSOR_NAME, + HUB_ZONE, + PING_INTERVAL_MARGIN, +) + + +def get_unavailability_interval(ping: int) -> float: + """Return the interval to the next unavailability check.""" + return timedelta(minutes=ping, seconds=PING_INTERVAL_MARGIN).total_seconds() + + +def get_name(port: int, account: str, zone: int, entity_type: str) -> str: + """Give back a entity_id and name according to the variables.""" + if zone == HUB_ZONE: + return f"{port} - {account} - {'Last Heartbeat' if entity_type == DEVICE_CLASS_TIMESTAMP else 'Power'}" + return f"{port} - {account} - zone {zone} - {entity_type}" + + +def get_entity_id(port: int, account: str, zone: int, entity_type: str) -> str: + """Give back a entity_id according to the variables.""" + if zone == HUB_ZONE: + return f"{port}_{account}_{HUB_SENSOR_NAME if entity_type == DEVICE_CLASS_TIMESTAMP else entity_type}" + return f"{port}_{account}_{zone}_{entity_type}" + + +def get_unique_id(entry_id: str, account: str, zone: int, domain: str) -> str: + """Return the unique id.""" + return f"{entry_id}_{account}_{zone}_{domain}" + + +def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]: + """Create the attributes dict from a SIAEvent.""" + return { + EVENT_ACCOUNT: event.account, + EVENT_ZONE: event.ri, + EVENT_CODE: event.code, + EVENT_MESSAGE: event.message, + EVENT_ID: event.id, + EVENT_TIMESTAMP: event.timestamp, + } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a7d0153d8a1..3a28a24315b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -219,6 +219,7 @@ FLOWS = [ "sharkiq", "shelly", "shopping_list", + "sia", "simplisafe", "sma", "smappee", diff --git a/requirements_all.txt b/requirements_all.txt index 6b72c9f6c6e..156b75bb8a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1722,6 +1722,9 @@ pysesame2==1.0.1 # homeassistant.components.goalfeed pysher==1.0.1 +# homeassistant.components.sia +pysiaalarm==3.0.0b12 + # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a06d15465e..ac6bcea193f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -961,6 +961,9 @@ pyserial-asyncio==0.5 # homeassistant.components.zha pyserial==3.5 +# homeassistant.components.sia +pysiaalarm==3.0.0b12 + # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 diff --git a/tests/components/sia/__init__.py b/tests/components/sia/__init__.py new file mode 100644 index 00000000000..198b6bc4bb8 --- /dev/null +++ b/tests/components/sia/__init__.py @@ -0,0 +1 @@ +"""Tests for the sia integration.""" diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py new file mode 100644 index 00000000000..204518c1e5a --- /dev/null +++ b/tests/components/sia/test_config_flow.py @@ -0,0 +1,314 @@ +"""Test the sia config flow.""" +import logging +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.sia.config_flow import ACCOUNT_SCHEMA, HUB_SCHEMA +from homeassistant.components.sia.const import ( + CONF_ACCOUNT, + CONF_ACCOUNTS, + CONF_ADDITIONAL_ACCOUNTS, + CONF_ENCRYPTION_KEY, + CONF_IGNORE_TIMESTAMPS, + CONF_PING_INTERVAL, + CONF_ZONES, + DOMAIN, +) +from homeassistant.const import CONF_PORT, CONF_PROTOCOL +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +BASIS_CONFIG_ENTRY_ID = 1 +BASIC_CONFIG = { + CONF_PORT: 7777, + CONF_PROTOCOL: "TCP", + CONF_ACCOUNT: "ABCDEF", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 10, + CONF_ZONES: 1, + CONF_ADDITIONAL_ACCOUNTS: False, +} + +BASIC_OPTIONS = {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 2} + +BASE_OUT = { + "data": { + CONF_PORT: 7777, + CONF_PROTOCOL: "TCP", + CONF_ACCOUNTS: [ + { + CONF_ACCOUNT: "ABCDEF", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 10, + }, + ], + }, + "options": { + CONF_ACCOUNTS: {"ABCDEF": {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 1}} + }, +} + +ADDITIONAL_CONFIG_ENTRY_ID = 2 +BASIC_CONFIG_ADDITIONAL = { + CONF_PORT: 7777, + CONF_PROTOCOL: "TCP", + CONF_ACCOUNT: "ABCDEF", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 10, + CONF_ZONES: 1, + CONF_ADDITIONAL_ACCOUNTS: True, +} + +ADDITIONAL_ACCOUNT = { + CONF_ACCOUNT: "ACC2", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 2, + CONF_ZONES: 2, + CONF_ADDITIONAL_ACCOUNTS: False, +} +ADDITIONAL_OUT = { + "data": { + CONF_PORT: 7777, + CONF_PROTOCOL: "TCP", + CONF_ACCOUNTS: [ + { + CONF_ACCOUNT: "ABCDEF", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 10, + }, + { + CONF_ACCOUNT: "ACC2", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 2, + }, + ], + }, + "options": { + CONF_ACCOUNTS: { + "ABCDEF": {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 1}, + "ACC2": {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 2}, + } + }, +} + +ADDITIONAL_OPTIONS = { + CONF_ACCOUNTS: { + "ABCDEF": {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 2}, + "ACC2": {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 2}, + } +} + +BASIC_CONFIG_ENTRY = MockConfigEntry( + domain=DOMAIN, + data=BASE_OUT["data"], + options=BASE_OUT["options"], + title="SIA Alarm on port 7777", + entry_id=BASIS_CONFIG_ENTRY_ID, + version=1, +) +ADDITIONAL_CONFIG_ENTRY = MockConfigEntry( + domain=DOMAIN, + data=ADDITIONAL_OUT["data"], + options=ADDITIONAL_OUT["options"], + title="SIA Alarm on port 7777", + entry_id=ADDITIONAL_CONFIG_ENTRY_ID, + version=1, +) + + +@pytest.fixture(params=[False, True], ids=["user", "add_account"]) +def additional(request) -> bool: + """Return True or False for the additional or base test.""" + return request.param + + +@pytest.fixture +async def flow_at_user_step(hass): + """Return a initialized flow.""" + return await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + + +@pytest.fixture +async def entry_with_basic_config(hass, flow_at_user_step): + """Return a entry with a basic config.""" + with patch("pysiaalarm.aio.SIAClient.start", return_value=True): + return await hass.config_entries.flow.async_configure( + flow_at_user_step["flow_id"], BASIC_CONFIG + ) + + +@pytest.fixture +async def flow_at_add_account_step(hass, flow_at_user_step): + """Return a initialized flow at the additional account step.""" + return await hass.config_entries.flow.async_configure( + flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL + ) + + +@pytest.fixture +async def entry_with_additional_account_config(hass, flow_at_add_account_step): + """Return a entry with a two account config.""" + with patch("pysiaalarm.aio.SIAClient.start", return_value=True): + return await hass.config_entries.flow.async_configure( + flow_at_add_account_step["flow_id"], ADDITIONAL_ACCOUNT + ) + + +async def setup_sia(hass, config_entry: MockConfigEntry): + """Add mock config to HASS.""" + assert await async_setup_component(hass, DOMAIN, {}) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + +async def test_form_start( + hass, flow_at_user_step, flow_at_add_account_step, additional +): + """Start the form and check if you get the right id and schema.""" + if additional: + assert flow_at_add_account_step["step_id"] == "add_account" + assert flow_at_add_account_step["errors"] is None + assert flow_at_add_account_step["data_schema"] == ACCOUNT_SCHEMA + return + assert flow_at_user_step["step_id"] == "user" + assert flow_at_user_step["errors"] is None + assert flow_at_user_step["data_schema"] == HUB_SCHEMA + + +async def test_create(hass, entry_with_basic_config): + """Test we create a entry through the form.""" + assert entry_with_basic_config["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + entry_with_basic_config["title"] + == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" + ) + assert entry_with_basic_config["data"] == BASE_OUT["data"] + assert entry_with_basic_config["options"] == BASE_OUT["options"] + + +async def test_create_additional_account(hass, entry_with_additional_account_config): + """Test we create a config with two accounts.""" + assert ( + entry_with_additional_account_config["type"] + == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + ) + assert ( + entry_with_additional_account_config["title"] + == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" + ) + + assert entry_with_additional_account_config["data"] == ADDITIONAL_OUT["data"] + assert entry_with_additional_account_config["options"] == ADDITIONAL_OUT["options"] + + +async def test_abort_form(hass, entry_with_basic_config): + """Test aborting a config that already exists.""" + assert entry_with_basic_config["data"][CONF_PORT] == BASIC_CONFIG[CONF_PORT] + start_another_flow = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + get_abort = await hass.config_entries.flow.async_configure( + start_another_flow["flow_id"], BASIC_CONFIG + ) + assert get_abort["type"] == "abort" + assert get_abort["reason"] == "already_configured" + + +@pytest.mark.parametrize( + "field, value, error", + [ + ("encryption_key", "AAAAAAAAAAAAAZZZ", "invalid_key_format"), + ("encryption_key", "AAAAAAAAAAAAA", "invalid_key_length"), + ("account", "ZZZ", "invalid_account_format"), + ("account", "A", "invalid_account_length"), + ("ping_interval", 1500, "invalid_ping"), + ("zones", 0, "invalid_zones"), + ], +) +async def test_validation_errors( + hass, + flow_at_user_step, + additional, + field, + value, + error, +): + """Test we handle the different invalid inputs, both in the user and add_account flow.""" + config = BASIC_CONFIG.copy() + flow_id = flow_at_user_step["flow_id"] + if additional: + flow_at_add_account_step = await hass.config_entries.flow.async_configure( + flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL + ) + config = ADDITIONAL_ACCOUNT.copy() + flow_id = flow_at_add_account_step["flow_id"] + + config[field] = value + result_err = await hass.config_entries.flow.async_configure(flow_id, config) + assert result_err["type"] == "form" + assert result_err["errors"] == {"base": error} + + +async def test_unknown(hass, flow_at_user_step, additional): + """Test unknown exceptions.""" + flow_id = flow_at_user_step["flow_id"] + if additional: + flow_at_add_account_step = await hass.config_entries.flow.async_configure( + flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL + ) + flow_id = flow_at_add_account_step["flow_id"] + with patch( + "pysiaalarm.SIAAccount.validate_account", + side_effect=Exception, + ): + config = ADDITIONAL_ACCOUNT if additional else BASIC_CONFIG + result_err = await hass.config_entries.flow.async_configure(flow_id, config) + assert result_err + assert result_err["step_id"] == "add_account" if additional else "user" + assert result_err["errors"] == {"base": "unknown"} + assert result_err["data_schema"] == ACCOUNT_SCHEMA if additional else HUB_SCHEMA + + +async def test_options_basic(hass): + """Test options flow for single account.""" + await setup_sia(hass, BASIC_CONFIG_ENTRY) + result = await hass.config_entries.options.async_init(BASIC_CONFIG_ENTRY.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "options" + assert result["last_step"] + + updated = await hass.config_entries.options.async_configure( + result["flow_id"], BASIC_OPTIONS + ) + assert updated["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert updated["data"] == { + CONF_ACCOUNTS: {BASIC_CONFIG[CONF_ACCOUNT]: BASIC_OPTIONS} + } + + +async def test_options_additional(hass): + """Test options flow for single account.""" + await setup_sia(hass, ADDITIONAL_CONFIG_ENTRY) + result = await hass.config_entries.options.async_init( + ADDITIONAL_CONFIG_ENTRY.entry_id + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "options" + assert not result["last_step"] + + updated = await hass.config_entries.options.async_configure( + result["flow_id"], BASIC_OPTIONS + ) + assert updated["type"] == data_entry_flow.RESULT_TYPE_FORM + assert updated["step_id"] == "options" + assert updated["last_step"] From 331cb3b74da1caa6d6d6a86b17b20b2c6a9fbeb7 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 24 May 2021 09:51:33 +0200 Subject: [PATCH 682/852] Fix KNX light: turn on color light with only brightness (#50979) * fix turn on color light with only brightness * fix comment * fix individual_color address assignment * python 3.8 compatibility --- homeassistant/components/knx/light.py | 48 ++++++++++++++++++--------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 1c20c68b145..ed4abac63b5 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -1,7 +1,7 @@ """Support for KNX/IP lights.""" from __future__ import annotations -from typing import Any, cast +from typing import Any, Tuple, cast from xknx import XKNX from xknx.devices import Light as XknxLight @@ -176,40 +176,40 @@ def _create_light(xknx: XKNX, config: ConfigType) -> XknxLight: LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS ), group_address_switch_green=individual_color_addresses( - LightSchema.CONF_RED, KNX_ADDRESS + LightSchema.CONF_GREEN, KNX_ADDRESS ), group_address_switch_green_state=individual_color_addresses( - LightSchema.CONF_RED, LightSchema.CONF_STATE_ADDRESS + LightSchema.CONF_GREEN, LightSchema.CONF_STATE_ADDRESS ), group_address_brightness_green=individual_color_addresses( - LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_ADDRESS + LightSchema.CONF_GREEN, LightSchema.CONF_BRIGHTNESS_ADDRESS ), group_address_brightness_green_state=individual_color_addresses( - LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + LightSchema.CONF_GREEN, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS ), group_address_switch_blue=individual_color_addresses( - LightSchema.CONF_RED, KNX_ADDRESS + LightSchema.CONF_BLUE, KNX_ADDRESS ), group_address_switch_blue_state=individual_color_addresses( - LightSchema.CONF_RED, LightSchema.CONF_STATE_ADDRESS + LightSchema.CONF_BLUE, LightSchema.CONF_STATE_ADDRESS ), group_address_brightness_blue=individual_color_addresses( - LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_ADDRESS + LightSchema.CONF_BLUE, LightSchema.CONF_BRIGHTNESS_ADDRESS ), group_address_brightness_blue_state=individual_color_addresses( - LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + LightSchema.CONF_BLUE, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS ), group_address_switch_white=individual_color_addresses( - LightSchema.CONF_RED, KNX_ADDRESS + LightSchema.CONF_WHITE, KNX_ADDRESS ), group_address_switch_white_state=individual_color_addresses( - LightSchema.CONF_RED, LightSchema.CONF_STATE_ADDRESS + LightSchema.CONF_WHITE, LightSchema.CONF_STATE_ADDRESS ), group_address_brightness_white=individual_color_addresses( - LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_ADDRESS + LightSchema.CONF_WHITE, LightSchema.CONF_BRIGHTNESS_ADDRESS ), group_address_brightness_white_state=individual_color_addresses( - LightSchema.CONF_RED, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + LightSchema.CONF_WHITE, LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS ), min_kelvin=config[LightSchema.CONF_MIN_KELVIN], max_kelvin=config[LightSchema.CONF_MAX_KELVIN], @@ -366,7 +366,7 @@ class KNXLight(KnxEntity, LightEntity): await self._device.set_brightness(brightness) return rgb = cast( - tuple[int, int, int], + Tuple[int, int, int], tuple(color * brightness // 255 for color in rgb), ) white = white * brightness // 255 if white is not None else None @@ -395,7 +395,25 @@ class KNXLight(KnxEntity, LightEntity): await self._device.set_tunable_white(relative_ct) if brightness is not None: - await self._device.set_brightness(brightness) + # brightness: 1..255; 0 brightness will call async_turn_off() + if self._device.brightness.writable: + await self._device.set_brightness(brightness) + return + # brightness without color in kwargs; set via color - default to white + if self.color_mode == COLOR_MODE_RGBW: + rgbw = self.rgbw_color + if not rgbw or not any(rgbw): + await self._device.set_color((0, 0, 0), brightness) + return + await set_color(rgbw[:3], rgbw[3], brightness) + return + if self.color_mode == COLOR_MODE_RGB: + rgb = self.rgb_color + if not rgb or not any(rgb): + await self._device.set_color((brightness, brightness, brightness)) + return + await set_color(rgb, None, brightness) + return async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" From ec4a47d1db95e2bc04b11ed777baf140d379f569 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 May 2021 11:36:04 +0200 Subject: [PATCH 683/852] Fix sia pylint errors (#51022) --- homeassistant/components/sia/alarm_control_panel.py | 2 +- homeassistant/components/sia/hub.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 74bb48be940..9d5f62b02de 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -101,7 +101,7 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): entry: ConfigEntry, account_data: dict[str, Any], zone: int, - ): + ) -> None: """Create SIAAlarmControlPanel object.""" self._entry: ConfigEntry = entry self._account_data: dict[str, Any] = account_data diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index f5d48a3282c..e5dc7b85ed8 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -35,7 +35,7 @@ class SIAHub: self, hass: HomeAssistant, entry: ConfigEntry, - ): + ) -> None: """Create the SIAHub.""" self._hass: HomeAssistant = hass self._entry: ConfigEntry = entry From b169a8dbda2e61e0de315e92b2493726dc88874d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 May 2021 11:36:42 +0200 Subject: [PATCH 684/852] Mark battery, humidity and pressure sensors as STATE_CLASS_MEASUREMENT (#50924) * Mark battery, humidity and pressure sensors as STATE_CLASS_MEASUREMENT * Fix deconz battery sensor --- homeassistant/components/broadlink/sensor.py | 7 ++++++- homeassistant/components/deconz/sensor.py | 7 +++++++ homeassistant/components/hue/sensor.py | 1 + homeassistant/components/tasmota/sensor.py | 20 +++++++++++++++---- .../components/xiaomi_miio/sensor.py | 10 ++++++++-- homeassistant/components/zha/sensor.py | 3 +++ homeassistant/components/zwave_js/sensor.py | 4 +++- 7 files changed, 44 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index aa0aa6c5f0b..92708583c43 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -28,7 +28,12 @@ SENSOR_TYPES = { STATE_CLASS_MEASUREMENT, ), "air_quality": ("Air Quality", None, None, None), - "humidity": ("Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY, None), + "humidity": ( + "Humidity", + PERCENTAGE, + DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, + ), "light": ("Light", None, DEVICE_CLASS_ILLUMINANCE, None), "noise": ("Noise", None, None, None), } diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 96963302139..bbc49f786ea 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -65,6 +65,8 @@ ICON = { } STATE_CLASS = { + Humidity: STATE_CLASS_MEASUREMENT, + Pressure: STATE_CLASS_MEASUREMENT, Temperature: STATE_CLASS_MEASUREMENT, } @@ -300,6 +302,11 @@ class DeconzBattery(DeconzDevice, SensorEntity): """Return the class of the sensor.""" return DEVICE_CLASS_BATTERY + @property + def state_class(self): + """Return the state class of the sensor.""" + return STATE_CLASS_MEASUREMENT + @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 0f1fa418287..a512012bc68 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -93,6 +93,7 @@ class HueBattery(GenericHueSensor, SensorEntity): """Battery class for when a batt-powered device is only represented as an event.""" _attr_device_class = DEVICE_CLASS_BATTERY + _attr_state_class = STATE_CLASS_MEASUREMENT _attr_unit_of_measurement = PERCENTAGE @property diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 5faea128594..19c3e37fa57 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -53,7 +53,10 @@ ICON = "icon" SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_AMBIENT: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, hc.SENSOR_APPARENT_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - hc.SENSOR_BATTERY: {DEVICE_CLASS: DEVICE_CLASS_BATTERY}, + hc.SENSOR_BATTERY: { + DEVICE_CLASS: DEVICE_CLASS_BATTERY, + STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, hc.SENSOR_CCT: {ICON: "mdi:temperature-kelvin"}, hc.SENSOR_CO2: {DEVICE_CLASS: DEVICE_CLASS_CO2}, hc.SENSOR_COLOR_BLUE: {ICON: "mdi:palette"}, @@ -64,7 +67,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_DISTANCE: {ICON: "mdi:leak"}, hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"}, hc.SENSOR_FREQUENCY: {ICON: "mdi:current-ac"}, - hc.SENSOR_HUMIDITY: {DEVICE_CLASS: DEVICE_CLASS_HUMIDITY}, + hc.SENSOR_HUMIDITY: { + DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, hc.SENSOR_ILLUMINANCE: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, @@ -81,8 +87,14 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_PM2_5: {ICON: "mdi:air-filter"}, hc.SENSOR_POWERFACTOR: {ICON: "mdi:alpha-f-circle-outline"}, hc.SENSOR_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - hc.SENSOR_PRESSURE: {DEVICE_CLASS: DEVICE_CLASS_PRESSURE}, - hc.SENSOR_PRESSUREATSEALEVEL: {DEVICE_CLASS: DEVICE_CLASS_PRESSURE}, + hc.SENSOR_PRESSURE: { + DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + hc.SENSOR_PRESSUREATSEALEVEL: { + DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, hc.SENSOR_REACTIVE_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index ed551a6dd49..16ca4d3e7ec 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -81,10 +81,16 @@ GATEWAY_SENSOR_TYPES = { state_class=STATE_CLASS_MEASUREMENT, ), "humidity": SensorType( - unit=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_HUMIDITY + unit=PERCENTAGE, + icon=None, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), "pressure": SensorType( - unit=PRESSURE_HPA, icon=None, device_class=DEVICE_CLASS_PRESSURE + unit=PRESSURE_HPA, + icon=None, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), "load_power": SensorType( unit=POWER_WATT, icon=None, device_class=DEVICE_CLASS_POWER diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index a6d7ae4ce9e..714df80eebb 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -179,6 +179,7 @@ class Battery(Sensor): SENSOR_ATTR = "battery_percentage_remaining" _device_class = DEVICE_CLASS_BATTERY + _state_class = STATE_CLASS_MEASUREMENT _unit = PERCENTAGE @staticmethod @@ -241,6 +242,7 @@ class Humidity(Sensor): SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_HUMIDITY _divisor = 100 + _state_class = STATE_CLASS_MEASUREMENT _unit = PERCENTAGE @@ -282,6 +284,7 @@ class Pressure(Sensor): SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_PRESSURE _decimals = 0 + _state_class = STATE_CLASS_MEASUREMENT _unit = PRESSURE_HPA diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index dcc21b236e8..40e28999a1a 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -122,9 +122,11 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): This should be run once during initialization so we don't have to calculate this value on every state update. """ + if self.info.primary_value.command_class == CommandClass.BATTERY: + return STATE_CLASS_MEASUREMENT if isinstance(self.info.primary_value.property_, str): property_lower = self.info.primary_value.property_.lower() - if "temperature" in property_lower: + if "humidity" in property_lower or "temperature" in property_lower: return STATE_CLASS_MEASUREMENT return None From 870c61a62262af07534fa41f70f651f3a680c5fd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 May 2021 11:37:02 +0200 Subject: [PATCH 685/852] Add color_mode support to MQTT light with basic schema (#50464) * Add color_mode support to MQTT light with basic schema * Update abbreviations * Silence pylint * Improve test coverage * Apply suggestions from code review --- .../components/mqtt/abbreviations.py | 10 + .../components/mqtt/light/schema_basic.py | 345 ++++- tests/components/mqtt/test_light.py | 1281 ++++++++++++++++- 3 files changed, 1602 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 23c94ada4c0..a16d721ba7c 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -25,6 +25,8 @@ ABBREVIATIONS = { "chrg_t": "charging_topic", "chrg_tpl": "charging_template", "clrm": "color_mode", + "clrm_stat_t": "color_mode_state_topic", + "clrm_val_tpl": "color_mode_value_template", "clr_temp_cmd_t": "color_temp_command_topic", "clr_temp_stat_t": "color_temp_state_topic", "clr_temp_tpl": "color_temp_template", @@ -146,6 +148,14 @@ ABBREVIATIONS = { "rgb_cmd_t": "rgb_command_topic", "rgb_stat_t": "rgb_state_topic", "rgb_val_tpl": "rgb_value_template", + "rgbw_cmd_tpl": "rgbw_command_template", + "rgbw_cmd_t": "rgbw_command_topic", + "rgbw_stat_t": "rgbw_state_topic", + "rgbw_val_tpl": "rgbw_value_template", + "rgbww_cmd_tpl": "rgbww_command_template", + "rgbww_cmd_t": "rgbww_command_topic", + "rgbww_stat_t": "rgbww_state_topic", + "rgbww_val_tpl": "rgbww_value_template", "send_cmd_t": "send_command_topic", "send_if_off": "send_if_off", "set_fan_spd_t": "set_fan_speed_topic", diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index f7c1648e96b..3e347363428 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -5,13 +5,24 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_UNKNOWN, + COLOR_MODE_XY, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -44,6 +55,8 @@ CONF_BRIGHTNESS_COMMAND_TOPIC = "brightness_command_topic" CONF_BRIGHTNESS_SCALE = "brightness_scale" CONF_BRIGHTNESS_STATE_TOPIC = "brightness_state_topic" CONF_BRIGHTNESS_VALUE_TEMPLATE = "brightness_value_template" +CONF_COLOR_MODE_STATE_TOPIC = "color_mode_state_topic" +CONF_COLOR_MODE_VALUE_TEMPLATE = "color_mode_value_template" CONF_COLOR_TEMP_COMMAND_TEMPLATE = "color_temp_command_template" CONF_COLOR_TEMP_COMMAND_TOPIC = "color_temp_command_topic" CONF_COLOR_TEMP_STATE_TOPIC = "color_temp_state_topic" @@ -61,6 +74,14 @@ CONF_RGB_COMMAND_TEMPLATE = "rgb_command_template" CONF_RGB_COMMAND_TOPIC = "rgb_command_topic" CONF_RGB_STATE_TOPIC = "rgb_state_topic" CONF_RGB_VALUE_TEMPLATE = "rgb_value_template" +CONF_RGBW_COMMAND_TEMPLATE = "rgbw_command_template" +CONF_RGBW_COMMAND_TOPIC = "rgbw_command_topic" +CONF_RGBW_STATE_TOPIC = "rgbw_state_topic" +CONF_RGBW_VALUE_TEMPLATE = "rgbw_value_template" +CONF_RGBWW_COMMAND_TEMPLATE = "rgbww_command_template" +CONF_RGBWW_COMMAND_TOPIC = "rgbww_command_topic" +CONF_RGBWW_STATE_TOPIC = "rgbww_state_topic" +CONF_RGBWW_VALUE_TEMPLATE = "rgbww_value_template" CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_XY_COMMAND_TOPIC = "xy_command_topic" CONF_XY_STATE_TOPIC = "xy_state_topic" @@ -81,13 +102,21 @@ DEFAULT_ON_COMMAND_TYPE = "last" VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"] -COMMAND_TEMPLATE_KEYS = [CONF_COLOR_TEMP_COMMAND_TEMPLATE, CONF_RGB_COMMAND_TEMPLATE] +COMMAND_TEMPLATE_KEYS = [ + CONF_COLOR_TEMP_COMMAND_TEMPLATE, + CONF_RGB_COMMAND_TEMPLATE, + CONF_RGBW_COMMAND_TEMPLATE, + CONF_RGBWW_COMMAND_TEMPLATE, +] VALUE_TEMPLATE_KEYS = [ CONF_BRIGHTNESS_VALUE_TEMPLATE, + CONF_COLOR_MODE_VALUE_TEMPLATE, CONF_COLOR_TEMP_VALUE_TEMPLATE, CONF_EFFECT_VALUE_TEMPLATE, CONF_HS_VALUE_TEMPLATE, CONF_RGB_VALUE_TEMPLATE, + CONF_RGBW_VALUE_TEMPLATE, + CONF_RGBWW_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE, CONF_WHITE_VALUE_TEMPLATE, CONF_XY_VALUE_TEMPLATE, @@ -102,6 +131,8 @@ PLATFORM_SCHEMA_BASIC = ( ): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_COLOR_MODE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COLOR_TEMP_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -126,6 +157,14 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_RGBW_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_RGBW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RGBW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBW_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_RGBWW_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_RGBWW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( @@ -159,22 +198,32 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT light.""" self._brightness = None + self._color_mode = None self._color_temp = None self._effect = None self._hs_color = None + self._legacy_mode = False + self._rgb_color = None + self._rgbw_color = None + self._rgbww_color = None self._state = False + self._supported_color_modes = None self._white_value = None + self._xy_color = None self._topic = None self._payload = None self._command_templates = None self._value_templates = None self._optimistic = False - self._optimistic_rgb_color = False self._optimistic_brightness = False + self._optimistic_color_mode = False self._optimistic_color_temp = False self._optimistic_effect = False self._optimistic_hs_color = False + self._optimistic_rgb_color = False + self._optimistic_rgbw_color = False + self._optimistic_rgbww_color = False self._optimistic_white_value = False self._optimistic_xy_color = False @@ -192,6 +241,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): for key in ( CONF_BRIGHTNESS_COMMAND_TOPIC, CONF_BRIGHTNESS_STATE_TOPIC, + CONF_COLOR_MODE_STATE_TOPIC, CONF_COLOR_TEMP_COMMAND_TOPIC, CONF_COLOR_TEMP_STATE_TOPIC, CONF_COMMAND_TOPIC, @@ -201,6 +251,10 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): CONF_HS_STATE_TOPIC, CONF_RGB_COMMAND_TOPIC, CONF_RGB_STATE_TOPIC, + CONF_RGBW_COMMAND_TOPIC, + CONF_RGBW_STATE_TOPIC, + CONF_RGBWW_COMMAND_TOPIC, + CONF_RGBWW_STATE_TOPIC, CONF_STATE_TOPIC, CONF_WHITE_VALUE_COMMAND_TOPIC, CONF_WHITE_VALUE_STATE_TOPIC, @@ -230,8 +284,15 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._command_templates = command_templates optimistic = config[CONF_OPTIMISTIC] + self._optimistic_color_mode = ( + optimistic or topic[CONF_COLOR_MODE_STATE_TOPIC] is None + ) self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None self._optimistic_rgb_color = optimistic or topic[CONF_RGB_STATE_TOPIC] is None + self._optimistic_rgbw_color = optimistic or topic[CONF_RGBW_STATE_TOPIC] is None + self._optimistic_rgbww_color = ( + optimistic or topic[CONF_RGBWW_STATE_TOPIC] is None + ) self._optimistic_brightness = ( optimistic or ( @@ -252,6 +313,38 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None ) self._optimistic_xy_color = optimistic or topic[CONF_XY_STATE_TOPIC] is None + self._supported_color_modes = set() + if topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + self._color_mode = COLOR_MODE_COLOR_TEMP + if topic[CONF_HS_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_HS) + self._color_mode = COLOR_MODE_HS + if topic[CONF_RGB_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_RGB) + self._color_mode = COLOR_MODE_RGB + if topic[CONF_RGBW_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_RGBW) + self._color_mode = COLOR_MODE_RGBW + if topic[CONF_RGBWW_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_RGBWW) + self._color_mode = COLOR_MODE_RGBWW + if topic[CONF_XY_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_XY) + self._color_mode = COLOR_MODE_XY + if len(self._supported_color_modes) > 1: + self._color_mode = COLOR_MODE_UNKNOWN + + if not self._supported_color_modes: + if topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: + self._color_mode = COLOR_MODE_BRIGHTNESS + self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + else: + self._color_mode = COLOR_MODE_ONOFF + self._supported_color_modes.add(COLOR_MODE_ONOFF) + + if topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: + self._legacy_mode = True def _is_optimistic(self, attribute): """Return True if the attribute is optimistically updated.""" @@ -334,6 +427,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) return None color = tuple(int(val) for val in payload.split(",")) + if self._optimistic_color_mode: + self._color_mode = color_mode if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: rgb = convert_color(*color) percent_bright = float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0 @@ -349,12 +444,69 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) if not rgb: return - self._hs_color = color_util.color_RGB_to_hs(*rgb) + if self._legacy_mode: + self._hs_color = color_util.color_RGB_to_hs(*rgb) + else: + self._rgb_color = rgb self.async_write_ha_state() add_topic(CONF_RGB_STATE_TOPIC, rgb_received) + restore_state(ATTR_RGB_COLOR) restore_state(ATTR_HS_COLOR, ATTR_RGB_COLOR) + @callback + @log_messages(self.hass, self.entity_id) + def rgbw_received(msg): + """Handle new MQTT messages for RGBW.""" + rgbw = _rgbx_received( + msg, + CONF_RGBW_VALUE_TEMPLATE, + COLOR_MODE_RGBW, + color_util.color_rgbw_to_rgb, + ) + if not rgbw: + return + self._rgbw_color = rgbw + self.async_write_ha_state() + + add_topic(CONF_RGBW_STATE_TOPIC, rgbw_received) + restore_state(ATTR_RGBW_COLOR) + + @callback + @log_messages(self.hass, self.entity_id) + def rgbww_received(msg): + """Handle new MQTT messages for RGBWW.""" + rgbww = _rgbx_received( + msg, + CONF_RGBWW_VALUE_TEMPLATE, + COLOR_MODE_RGBWW, + color_util.color_rgbww_to_rgb, + ) + if not rgbww: + return + self._rgbww_color = rgbww + self.async_write_ha_state() + + add_topic(CONF_RGBWW_STATE_TOPIC, rgbww_received) + restore_state(ATTR_RGBWW_COLOR) + + @callback + @log_messages(self.hass, self.entity_id) + def color_mode_received(msg): + """Handle new MQTT messages for color mode.""" + payload = self._value_templates[CONF_COLOR_MODE_VALUE_TEMPLATE]( + msg.payload, None + ) + if not payload: + _LOGGER.debug("Ignoring empty color mode message from '%s'", msg.topic) + return + + self._color_mode = payload + self.async_write_ha_state() + + add_topic(CONF_COLOR_MODE_STATE_TOPIC, color_mode_received) + restore_state(ATTR_COLOR_MODE) + @callback @log_messages(self.hass, self.entity_id) def color_temp_received(msg): @@ -366,6 +518,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): _LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic) return + if self._optimistic_color_mode: + self._color_mode = COLOR_MODE_COLOR_TEMP self._color_temp = int(payload) self.async_write_ha_state() @@ -399,6 +553,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return try: hs_color = tuple(float(val) for val in payload.split(",", 2)) + if self._optimistic_color_mode: + self._color_mode = COLOR_MODE_HS self._hs_color = hs_color self.async_write_ha_state() except ValueError: @@ -436,10 +592,16 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return xy_color = tuple(float(val) for val in payload.split(",")) - self._hs_color = color_util.color_xy_to_hs(*xy_color) + if self._optimistic_color_mode: + self._color_mode = COLOR_MODE_XY + if self._legacy_mode: + self._hs_color = color_util.color_xy_to_hs(*xy_color) + else: + self._xy_color = xy_color self.async_write_ha_state() add_topic(CONF_XY_STATE_TOPIC, xy_received) + restore_state(ATTR_XY_COLOR) restore_state(ATTR_HS_COLOR, ATTR_XY_COLOR) self._sub_state = await subscription.async_subscribe_topics( @@ -454,16 +616,51 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): brightness = min(round(brightness), 255) return brightness + @property + def color_mode(self): + """Return current color mode.""" + if self._legacy_mode: + return None + return self._color_mode + @property def hs_color(self): """Return the hs color value.""" + if not self._legacy_mode: + return self._hs_color + + # Legacy mode, gate color_temp with white_value == 0 if self._white_value: return None return self._hs_color + @property + def rgb_color(self): + """Return the rgb color value.""" + return self._rgb_color + + @property + def rgbw_color(self): + """Return the rgbw color value.""" + return self._rgbw_color + + @property + def rgbww_color(self): + """Return the rgbww color value.""" + return self._rgbww_color + + @property + def xy_color(self): + """Return the xy color value.""" + return self._xy_color + @property def color_temp(self): """Return the color temperature in mired.""" + if not self._legacy_mode: + return self._color_temp + + # Legacy mode, gate color_temp with white_value > 0 supports_color = ( self._topic[CONF_RGB_COMMAND_TOPIC] or self._topic[CONF_HS_COMMAND_TOPIC] @@ -512,10 +709,24 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): """Return the current effect.""" return self._effect + @property + def supported_color_modes(self): + """Flag supported color modes.""" + if self._legacy_mode: + return None + return self._supported_color_modes + @property def supported_features(self): """Flag supported features.""" supported_features = 0 + supported_features |= ( + self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None and SUPPORT_EFFECT + ) + if not self._legacy_mode: + return supported_features + + # Legacy mode supported_features |= self._topic[CONF_RGB_COMMAND_TOPIC] is not None and ( SUPPORT_COLOR | SUPPORT_BRIGHTNESS ) @@ -527,9 +738,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and SUPPORT_COLOR_TEMP ) - supported_features |= ( - self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None and SUPPORT_EFFECT - ) supported_features |= ( self._topic[CONF_HS_COMMAND_TOPIC] is not None and SUPPORT_COLOR ) @@ -543,7 +751,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return supported_features - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. This method is a coroutine. @@ -574,22 +782,28 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) return tuple(int(channel * brightness / 255) for channel in color) - def render_rgbx(color, template): + def render_rgbx(color, template, color_mode): """Render RGBx payload.""" tpl = self._command_templates[template] if tpl: keys = ["red", "green", "blue"] + if color_mode == COLOR_MODE_RGBW: + keys.append("white") + elif color_mode == COLOR_MODE_RGBWW: + keys.extend(["cold_white", "warm_white"]) rgb_color_str = tpl(zip(keys, color)) else: rgb_color_str = ",".join(str(channel) for channel in color) return rgb_color_str - def set_optimistic(attribute, value, condition_attribute=None): + def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): """Optimistically update a state attribute.""" if condition_attribute is None: condition_attribute = attribute if not self._is_optimistic(condition_attribute): return False + if color_mode and self._optimistic_color_mode: + self._color_mode = color_mode setattr(self, f"_{attribute}", value) return True @@ -604,21 +818,72 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 hs_color = kwargs.get(ATTR_HS_COLOR) - if hs_color and self._topic[CONF_RGB_COMMAND_TOPIC] is not None: - # Convert HS to RGB + if ( + hs_color + and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and self._legacy_mode + ): + # Legacy mode: Convert HS to RGB rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100)) - rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) publish(CONF_RGB_COMMAND_TOPIC, rgb_s) - should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ATTR_RGB_COLOR) + should_update |= set_optimistic( + ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_RGB_COLOR + ) if hs_color and self._topic[CONF_HS_COMMAND_TOPIC] is not None: publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") - should_update |= set_optimistic(ATTR_HS_COLOR, hs_color) + should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, COLOR_MODE_HS) - if hs_color and self._topic[CONF_XY_COMMAND_TOPIC] is not None: + if ( + hs_color + and self._topic[CONF_XY_COMMAND_TOPIC] is not None + and self._legacy_mode + ): + # Legacy mode: Convert HS to XY xy_color = color_util.color_hs_to_xy(*hs_color) publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") - should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ATTR_XY_COLOR) + should_update |= set_optimistic( + ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_XY_COLOR + ) + + if ( + (rgb := kwargs.get(ATTR_RGB_COLOR)) + and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + scaled = scale_rgbx(rgb) + rgb_s = render_rgbx(scaled, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) + publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + should_update |= set_optimistic(ATTR_RGB_COLOR, rgb, COLOR_MODE_RGB) + + if ( + (rgbw := kwargs.get(ATTR_RGBW_COLOR)) + and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + scaled = scale_rgbx(rgbw) + rgbw_s = render_rgbx(scaled, CONF_RGBW_COMMAND_TEMPLATE, COLOR_MODE_RGBW) + publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) + should_update |= set_optimistic(ATTR_RGBW_COLOR, rgbw, COLOR_MODE_RGBW) + + if ( + (rgbww := kwargs.get(ATTR_RGBWW_COLOR)) + and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + scaled = scale_rgbx(rgbww) + rgbww_s = render_rgbx(scaled, CONF_RGBWW_COMMAND_TEMPLATE, COLOR_MODE_RGBWW) + publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) + should_update |= set_optimistic(ATTR_RGBWW_COLOR, rgbww, COLOR_MODE_RGBWW) + + if ( + (xy_color := kwargs.get(ATTR_XY_COLOR)) + and self._topic[CONF_XY_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") + should_update |= set_optimistic(ATTR_XY_COLOR, xy_color, COLOR_MODE_XY) if ( ATTR_BRIGHTNESS in kwargs @@ -637,14 +902,52 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR not in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and self._legacy_mode ): + # Legacy mode hs_color = self._hs_color if self._hs_color is not None else (0, 0) brightness = kwargs[ATTR_BRIGHTNESS] rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100), brightness) - rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) - + elif ( + ATTR_BRIGHTNESS in kwargs + and ATTR_RGB_COLOR not in kwargs + and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + rgb_color = self._rgb_color if self._rgb_color is not None else (255,) * 3 + rgb = scale_rgbx(rgb_color, kwargs[ATTR_BRIGHTNESS]) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) + publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) + elif ( + ATTR_BRIGHTNESS in kwargs + and ATTR_RGBW_COLOR not in kwargs + and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + rgbw_color = ( + self._rgbw_color if self._rgbw_color is not None else (255,) * 4 + ) + rgbw = scale_rgbx(rgbw_color, kwargs[ATTR_BRIGHTNESS]) + rgbw_s = render_rgbx(rgbw, CONF_RGBW_COMMAND_TEMPLATE, COLOR_MODE_RGBW) + publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) + elif ( + ATTR_BRIGHTNESS in kwargs + and ATTR_RGBWW_COLOR not in kwargs + and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + rgbww_color = ( + self._rgbww_color if self._rgbww_color is not None else (255,) * 5 + ) + rgbww = scale_rgbx(rgbww_color, kwargs[ATTR_BRIGHTNESS]) + rgbww_s = render_rgbx(rgbww, CONF_RGBWW_COMMAND_TEMPLATE, COLOR_MODE_RGBWW) + publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) if ( ATTR_COLOR_TEMP in kwargs and self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None @@ -655,7 +958,9 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): color_temp = tpl({"value": color_temp}) publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp) - should_update |= set_optimistic(ATTR_COLOR_TEMP, kwargs[ATTR_COLOR_TEMP]) + should_update |= set_optimistic( + ATTR_COLOR_TEMP, kwargs[ATTR_COLOR_TEMP], COLOR_MODE_COLOR_TEMP + ) if ATTR_EFFECT in kwargs and self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: effect = kwargs[ATTR_EFFECT] diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index e995b373d03..e419743bd87 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -212,8 +212,8 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): assert hass.states.get("light.test") is None -async def test_rgb_light(hass, mqtt_mock): - """Test RGB light flags brightness support.""" +async def test_legacy_rgb_white_light(hass, mqtt_mock): + """Test legacy RGB + white light flags brightness support.""" assert await async_setup_component( hass, light.DOMAIN, @@ -223,14 +223,19 @@ async def test_rgb_light(hass, mqtt_mock): "name": "test", "command_topic": "test_light_rgb/set", "rgb_command_topic": "test_light_rgb/rgb/set", + "white_value_command_topic": "test_light_rgb/white/set", } }, ) await hass.async_block_till_done() state = hass.states.get("light.test") - expected_features = light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS + expected_features = ( + light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS | light.SUPPORT_WHITE_VALUE + ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["hs", "rgbw"] async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock): @@ -255,8 +260,13 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] async_fire_mqtt_message(hass, "test_light_rgb/status", "ON") @@ -266,12 +276,17 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "onoff" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] -async def test_controlling_state_via_topic(hass, mqtt_mock): - """Test the controlling of the state via topic.""" +async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): + """Test the controlling of the state via topic for legacy light (white_value).""" config = { light.DOMAIN: { "platform": "mqtt", @@ -297,6 +312,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): "payload_off": 0, } } + color_modes = ["color_temp", "hs", "rgbw"] assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() @@ -308,8 +324,13 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "test_light_rgb/status", "1") @@ -321,8 +342,13 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/status", "0") @@ -335,20 +361,28 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): light_state = hass.states.get("light.test") assert light_state.attributes["brightness"] == 100 + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") light_state = hass.states.get("light.test") assert light_state.attributes.get("color_temp") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "100") light_state = hass.states.get("light.test") assert light_state.attributes["white_value"] == 100 assert light_state.attributes["color_temp"] == 300 + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") light_state = hass.states.get("light.test") assert light_state.attributes["effect"] == "rainbow" + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/status", "1") @@ -356,23 +390,152 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): light_state = hass.states.get("light.test") assert light_state.attributes.get("rgb_color") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "0") light_state = hass.states.get("light.test") assert light_state.attributes.get("rgb_color") == (255, 255, 255) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") light_state = hass.states.get("light.test") assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") light_state = hass.states.get("light.test") assert light_state.attributes.get("xy_color") == (0.672, 0.324) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic(hass, mqtt_mock): + """Test the controlling of the state via topic.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_state_topic": "test_light_rgb/effect/status", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_state_topic": "test_light_rgb/xy/status", + "xy_command_topic": "test_light_rgb/xy/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/status", "0") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") == 100 + assert light_state.attributes["color_temp"] == 300 + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "rainbow" + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "125,125,125") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgb_color") == (125, 125, 125) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "80,40,20,10") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbw_color") == (80, 40, 20, 10) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "80,40,20,10,8") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbww_color") == (80, 40, 20, 10, 8) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("xy_color") == (0.675, 0.322) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + +async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): """Test handling of empty data via topic.""" config = { light.DOMAIN: { @@ -488,6 +651,140 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): assert light_state.attributes["white_value"] == 255 +async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): + """Test handling of empty data via topic.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "color_mode_state_topic": "test_light_rgb/color_mode/status", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_state_topic": "test_light_rgb/effect/status", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_state_topic": "test_light_rgb/xy/status", + "xy_command_topic": "test_light_rgb/xy/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("xy_color") is None + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgb") + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "255,255,255") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "255") + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "none") + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 255, 255) + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") == "none" + assert state.attributes.get("hs_color") == (0, 0) + assert state.attributes.get("xy_color") == (0.323, 0.329) + assert state.attributes.get("color_mode") == "rgb" + + async_fire_mqtt_message(hass, "test_light_rgb/status", "") + assert "Ignoring empty state message" in caplog.text + light_state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "") + assert "Ignoring empty brightness message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["brightness"] == 255 + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "") + assert "Ignoring empty color mode message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "none" + + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "") + assert "Ignoring empty effect message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "none" + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "") + assert "Ignoring empty rgb message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgb_color") == (255, 255, 255) + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "") + assert "Ignoring empty hs message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (0, 0) + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "bad,bad") + assert "Failed to parse hs state update" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (0, 0) + + async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "") + assert "Ignoring empty xy-color message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("xy_color") == (0.323, 0.329) + + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "255,255,255,1") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbw") + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "") + assert "Ignoring empty rgbw message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbw_color") == (255, 255, 255, 1) + + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "255,255,255,1,2") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbww") + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "") + assert "Ignoring empty rgbww message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbww_color") == (255, 255, 255, 1, 2) + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "153") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "color_temp") + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp") == 153 + assert state.attributes.get("effect") == "none" + assert state.attributes.get("hs_color") is None + assert state.attributes.get("xy_color") is None + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "") + assert "Ignoring empty color temp message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["color_temp"] == 153 + + async def test_brightness_controlling_scale(hass, mqtt_mock): """Test the brightness controlling scale.""" with assert_setup_component(1, light.DOMAIN): @@ -574,7 +871,7 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): assert state.attributes.get("brightness") == 127 -async def test_white_value_controlling_scale(hass, mqtt_mock): +async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): """Test the white_value controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -621,7 +918,7 @@ async def test_white_value_controlling_scale(hass, mqtt_mock): assert light_state.attributes["white_value"] == 255 -async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): +async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock): """Test the setting of the state with a template.""" config = { light.DOMAIN: { @@ -706,6 +1003,106 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert state.attributes.get("xy_color") == (0.14, 0.131) +async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): + """Test the setting of the state with a template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_command_topic": "test_light_rgb/rgbw/set", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/brightness/status", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "effect_state_topic": "test_light_rgb/effect/status", + "hs_state_topic": "test_light_rgb/hs/status", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "xy_state_topic": "test_light_rgb/xy/status", + "state_value_template": "{{ value_json.hello }}", + "brightness_value_template": "{{ value_json.hello }}", + "color_temp_value_template": "{{ value_json.hello }}", + "effect_value_template": "{{ value_json.hello }}", + "hs_value_template": '{{ value_json.hello | join(",") }}', + "rgb_value_template": '{{ value_json.hello | join(",") }}', + "rgbw_value_template": '{{ value_json.hello | join(",") }}', + "rgbww_value_template": '{{ value_json.hello | join(",") }}', + "xy_value_template": '{{ value_json.hello | join(",") }}', + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("brightness") is None + assert state.attributes.get("rgb_color") is None + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", '{"hello": [1, 2, 3]}') + async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "ON"}') + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", '{"hello": "50"}') + async_fire_mqtt_message( + hass, "test_light_rgb/effect/status", '{"hello": "rainbow"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 50 + assert state.attributes.get("rgb_color") == (1, 2, 3) + assert state.attributes.get("effect") == "rainbow" + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/rgbw/status", '{"hello": [1, 2, 3, 4]}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgbw_color") == (1, 2, 3, 4) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/rgbww/status", '{"hello": [1, 2, 3, 4, 5]}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgbww_color") == (1, 2, 3, 4, 5) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/color_temp/status", '{"hello": "300"}' + ) + state = hass.states.get("light.test") + assert state.attributes.get("color_temp") == 300 + assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", '{"hello": [100,50]}') + state = hass.states.get("light.test") + assert state.attributes.get("hs_color") == (100, 50) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/xy/status", '{"hello": [0.123,0.123]}' + ) + state = hass.states.get("light.test") + assert state.attributes.get("xy_color") == (0.123, 0.123) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock): """Test the setting of the state with undocumented value_template.""" config = { @@ -735,7 +1132,7 @@ async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { @@ -755,6 +1152,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): "payload_off": "off", } } + color_modes = ["color_temp", "hs", "rgbw"] fake_state = ha.State( "light.test", "on", @@ -782,18 +1180,20 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None assert state.attributes.get("white_value") is None assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes await common.async_turn_on(hass, "light.test") - mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", "on", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes await common.async_turn_off(hass, "light.test") - mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", "off", 2, False ) @@ -805,8 +1205,19 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): await common.async_turn_on( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes mqtt_mock.async_publish.assert_has_calls( [ @@ -829,6 +1240,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None await common.async_turn_on(hass, "light.test", white_value=80, color_temp=125) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes mqtt_mock.async_publish.assert_has_calls( [ @@ -848,6 +1262,195 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes["color_temp"] == 125 +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): + """Test the sending of command in optimistic mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_command_topic": "test_light_rgb/xy/set", + "effect_list": ["colorloop", "random"], + "qos": 2, + "payload_on": "on", + "payload_off": "off", + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + fake_state = ha.State( + "light.test", + "on", + { + "brightness": 95, + "hs_color": [100, 100], + "effect": "random", + "color_temp": 100, + "color_mode": "hs", + }, + ) + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ), assert_setup_component(1, light.DOMAIN): + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 95 + assert state.attributes.get("hs_color") == (100, 100) + assert state.attributes.get("effect") == "random" + assert state.attributes.get("color_temp") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await common.async_turn_on(hass, "light.test", effect="colorloop") + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/effect/set", "colorloop", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 2 + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "colorloop" + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "off", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on( + hass, "light.test", brightness=10, rgb_color=[80, 40, 20] + ) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "10", 2, False), + call("test_light_rgb/rgb/set", "80,40,20", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 10 + assert state.attributes.get("rgb_color") == (80, 40, 20) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on( + hass, "light.test", brightness=20, rgbw_color=[80, 40, 20, 10] + ) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "20", 2, False), + call("test_light_rgb/rgbw/set", "80,40,20,10", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 20 + assert state.attributes.get("rgbw_color") == (80, 40, 20, 10) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on( + hass, "light.test", brightness=40, rgbww_color=[80, 40, 20, 10, 8] + ) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "40", 2, False), + call("test_light_rgb/rgbww/set", "80,40,20,10,8", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 40 + assert state.attributes.get("rgbww_color") == (80, 40, 20, 10, 8) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "50", 2, False), + call("test_light_rgb/hs/set", "359.0,78.0", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 50 + assert state.attributes.get("hs_color") == (359.0, 78.0) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on(hass, "light.test", brightness=60, xy_color=[0.2, 0.3]) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "60", 2, False), + call("test_light_rgb/xy/set", "0.2,0.3", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 60 + assert state.attributes.get("xy_color") == (0.2, 0.3) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on(hass, "light.test", color_temp=125) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/color_temp/set", "125", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 2 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 60 + assert state.attributes.get("color_temp") == 125 + assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): """Test the sending of RGB command with template.""" config = { @@ -875,14 +1478,88 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_has_calls( [ call("test_light_rgb/set", "on", 0, False), - call("test_light_rgb/rgb/set", "#ff803f", 0, False), + call("test_light_rgb/rgb/set", "#ff8040", 0, False), ], any_order=True, ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes["rgb_color"] == (255, 128, 63) + assert state.attributes["rgb_color"] == (255, 128, 64) + + +async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): + """Test the sending of RGBW command with template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbw_command_template": '{{ "#%02x%02x%02x%02x" | ' + "format(red, green, blue, white)}}", + "payload_on": "on", + "payload_off": "off", + "qos": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", rgbw_color=[255, 128, 64, 32]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 0, False), + call("test_light_rgb/rgbw/set", "#ff804020", 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["rgbw_color"] == (255, 128, 64, 32) + + +async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): + """Test the sending of RGBWW command with template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "rgbww_command_template": '{{ "#%02x%02x%02x%02x%02x" | ' + "format(red, green, blue, cold_white, warm_white)}}", + "payload_on": "on", + "payload_off": "off", + "qos": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", rgbww_color=[255, 128, 64, 32, 16]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 0, False), + call("test_light_rgb/rgbww/set", "#ff80402010", 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["rgbww_color"] == (255, 128, 64, 32, 16) async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): @@ -1117,6 +1794,97 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): ) +async def test_legacy_on_command_rgb(hass, mqtt_mock): + """Test on command in RGB brightness mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgb_command_topic": "test_light/rgb", + "white_value_command_topic": "test_light/white_value", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgb: '127,127,127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "127,127,127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgb: '255,255,255' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "255,255,255", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=1) + + # Should get the following MQTT messages. + # test_light/rgb: '1,1,1' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "1,1,1", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + # Ensure color gets scaled with brightness. + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "1,0,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgb: '255,128,0' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "255,128,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + async def test_on_command_rgb(hass, mqtt_mock): """Test on command in RGB brightness mode.""" config = { @@ -1207,6 +1975,186 @@ async def test_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() +async def test_on_command_rgbw(hass, mqtt_mock): + """Test on command in RGBW brightness mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbw_command_topic": "test_light/rgbw", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgbw: '127,127,127,127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "127,127,127,127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbw: '255,255,255,255' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "255,255,255,255", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=1) + + # Should get the following MQTT messages. + # test_light/rgbw: '1,1,1,1' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "1,1,1,1", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + # Ensure color gets scaled with brightness. + await common.async_turn_on(hass, "light.test", rgbw_color=[255, 128, 0, 16]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "1,0,0,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbw: '255,128,0' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "255,128,0,16", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + +async def test_on_command_rgbww(hass, mqtt_mock): + """Test on command in RGBWW brightness mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbww_command_topic": "test_light/rgbww", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgbww: '127,127,127,127,127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "127,127,127,127,127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbww: '255,255,255,255,255' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "255,255,255,255,255", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=1) + + # Should get the following MQTT messages. + # test_light/rgbww: '1,1,1,1,1' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "1,1,1,1,1", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + # Ensure color gets scaled with brightness. + await common.async_turn_on(hass, "light.test", rgbww_color=[255, 128, 0, 16, 32]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "1,0,0,0,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbww: '255,128,0,16,32' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "255,128,0,16,32", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + async def test_on_command_rgb_template(hass, mqtt_mock): """Test on command in RGB brightness mode with RGB template.""" config = { @@ -1228,7 +2176,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock): await common.async_turn_on(hass, "light.test", brightness=127) # Should get the following MQTT messages. - # test_light/rgb: '127,127,127' + # test_light/rgb: '127/127/127' # test_light/set: 'ON' mqtt_mock.async_publish.assert_has_calls( [ @@ -1244,6 +2192,311 @@ async def test_on_command_rgb_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) +async def test_on_command_rgbw_template(hass, mqtt_mock): + """Test on command in RGBW brightness mode with RGBW template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbw_command_topic": "test_light/rgbw", + "rgbw_command_template": "{{ red }}/{{ green }}/{{ blue }}/{{ white }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgb: '127/127/127/127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "127/127/127/127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + +async def test_on_command_rgbww_template(hass, mqtt_mock): + """Test on command in RGBWW brightness mode with RGBWW template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbww_command_topic": "test_light/rgbww", + "rgbww_command_template": "{{ red }}/{{ green }}/{{ blue }}/{{ cold_white }}/{{ warm_white }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgb: '127/127/127/127/127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "127/127/127/127/127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + +async def test_explicit_color_mode(hass, mqtt_mock): + """Test explicit color mode over mqtt.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "color_mode_state_topic": "test_light_rgb/color_mode/status", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_state_topic": "test_light_rgb/effect/status", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_state_topic": "test_light_rgb/xy/status", + "xy_command_topic": "test_light_rgb/xy/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/status", "0") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "rainbow" + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "125,125,125") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "80,40,20,10") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "80,40,20,10,8") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "color_temp") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgb") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgb_color") == (125, 125, 125) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbw") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbw_color") == (80, 40, 20, 10) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbww") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbww_color") == (80, 40, 20, 10, 8) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "hs") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "xy") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("xy_color") == (0.675, 0.322) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + +async def test_explicit_color_mode_templated(hass, mqtt_mock): + """Test templated explicit color mode over mqtt.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "color_mode_state_topic": "test_light_rgb/color_mode/status", + "color_mode_value_template": "{{ value_json.color_mode }}", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + color_modes = ["color_temp", "hs"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/status", "0") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/color_mode/status", '{"color_mode":"color_temp"}' + ) + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/color_mode/status", '{"color_mode":"hs"}' + ) + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async def test_effect(hass, mqtt_mock): """Test effect.""" config = { From 51c8b1eb0b2d0e07610bc753883ae2873fa5857e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 24 May 2021 12:03:43 +0200 Subject: [PATCH 686/852] Improve code quality of TCP platform (#51000) * Improve code placements * Fix entity inheritance * fix tests * Improve PLATFORM_SCHEMA handling * Apply suggestions --- homeassistant/components/tcp/binary_sensor.py | 11 +- homeassistant/components/tcp/common.py | 158 ++++++++++++++++++ homeassistant/components/tcp/sensor.py | 150 +---------------- tests/components/tcp/test_binary_sensor.py | 4 +- tests/components/tcp/test_sensor.py | 8 +- 5 files changed, 175 insertions(+), 156 deletions(-) create mode 100644 homeassistant/components/tcp/common.py diff --git a/homeassistant/components/tcp/binary_sensor.py b/homeassistant/components/tcp/binary_sensor.py index c0e53fba334..ee26ff74a7f 100644 --- a/homeassistant/components/tcp/binary_sensor.py +++ b/homeassistant/components/tcp/binary_sensor.py @@ -3,15 +3,18 @@ from __future__ import annotations from typing import Any, Final -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from .common import TCP_PLATFORM_SCHEMA, TcpEntity from .const import CONF_VALUE_ON -from .sensor import PLATFORM_SCHEMA as TCP_PLATFORM_SCHEMA, TcpSensor -PLATFORM_SCHEMA: Final = TCP_PLATFORM_SCHEMA +PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend(TCP_PLATFORM_SCHEMA) def setup_platform( @@ -24,7 +27,7 @@ def setup_platform( add_entities([TcpBinarySensor(hass, config)]) -class TcpBinarySensor(BinarySensorEntity, TcpSensor): +class TcpBinarySensor(TcpEntity, BinarySensorEntity): """A binary sensor which is on when its state == CONF_VALUE_ON.""" @property diff --git a/homeassistant/components/tcp/common.py b/homeassistant/components/tcp/common.py new file mode 100644 index 00000000000..d2d40358970 --- /dev/null +++ b/homeassistant/components/tcp/common.py @@ -0,0 +1,158 @@ +"""Common code for TCP component.""" +from __future__ import annotations + +import logging +import select +import socket +import ssl +from typing import Any, Final + +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PAYLOAD, + CONF_PORT, + CONF_SSL, + CONF_TIMEOUT, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType + +from .const import ( + CONF_BUFFER_SIZE, + CONF_VALUE_ON, + DEFAULT_BUFFER_SIZE, + DEFAULT_NAME, + DEFAULT_SSL, + DEFAULT_TIMEOUT, + DEFAULT_VERIFY_SSL, +) +from .model import TcpSensorConfig + +_LOGGER: Final = logging.getLogger(__name__) + + +TCP_PLATFORM_SCHEMA: Final[dict[vol.Marker, Any]] = { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_PAYLOAD): cv.string, + vol.Optional(CONF_BUFFER_SIZE, default=DEFAULT_BUFFER_SIZE): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_ON): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, +} + + +class TcpEntity(Entity): + """Base entity class for TCP platform.""" + + def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: + """Set all the config values if they exist and get initial state.""" + + value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass + + self._hass = hass + self._config: TcpSensorConfig = { + CONF_NAME: config[CONF_NAME], + CONF_HOST: config[CONF_HOST], + CONF_PORT: config[CONF_PORT], + CONF_TIMEOUT: config[CONF_TIMEOUT], + CONF_PAYLOAD: config[CONF_PAYLOAD], + CONF_UNIT_OF_MEASUREMENT: config.get(CONF_UNIT_OF_MEASUREMENT), + CONF_VALUE_TEMPLATE: value_template, + CONF_VALUE_ON: config.get(CONF_VALUE_ON), + CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE], + CONF_SSL: config[CONF_SSL], + CONF_VERIFY_SSL: config[CONF_VERIFY_SSL], + } + + self._ssl_context: ssl.SSLContext | None = None + if self._config[CONF_SSL]: + self._ssl_context = ssl.create_default_context() + if not self._config[CONF_VERIFY_SSL]: + self._ssl_context.check_hostname = False + self._ssl_context.verify_mode = ssl.CERT_NONE + + self._state: str | None = None + self.update() + + @property + def name(self) -> str: + """Return the name of this sensor.""" + return self._config[CONF_NAME] + + def update(self) -> None: + """Get the latest value for this sensor.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(self._config[CONF_TIMEOUT]) + try: + sock.connect((self._config[CONF_HOST], self._config[CONF_PORT])) + except OSError as err: + _LOGGER.error( + "Unable to connect to %s on port %s: %s", + self._config[CONF_HOST], + self._config[CONF_PORT], + err, + ) + return + + if self._ssl_context is not None: + sock = self._ssl_context.wrap_socket( + sock, server_hostname=self._config[CONF_HOST] + ) + + try: + sock.send(self._config[CONF_PAYLOAD].encode()) + except OSError as err: + _LOGGER.error( + "Unable to send payload %r to %s on port %s: %s", + self._config[CONF_PAYLOAD], + self._config[CONF_HOST], + self._config[CONF_PORT], + err, + ) + return + + readable, _, _ = select.select([sock], [], [], self._config[CONF_TIMEOUT]) + if not readable: + _LOGGER.warning( + "Timeout (%s second(s)) waiting for a response after " + "sending %r to %s on port %s", + self._config[CONF_TIMEOUT], + self._config[CONF_PAYLOAD], + self._config[CONF_HOST], + self._config[CONF_PORT], + ) + return + + value = sock.recv(self._config[CONF_BUFFER_SIZE]).decode() + + value_template = self._config[CONF_VALUE_TEMPLATE] + if value_template is not None: + try: + self._state = value_template.render(parse_result=False, value=value) + return + except TemplateError: + _LOGGER.error( + "Unable to render template of %r with value: %r", + self._config[CONF_VALUE_TEMPLATE], + value, + ) + return + + self._state = value diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index ff5db39bba7..d282974fd4c 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -1,64 +1,20 @@ """Support for TCP socket based sensors.""" from __future__ import annotations -import logging -import select -import socket -import ssl from typing import Any, Final -import voluptuous as vol - from homeassistant.components.sensor import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SensorEntity, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PAYLOAD, - CONF_PORT, - CONF_SSL, - CONF_TIMEOUT, - CONF_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant -from homeassistant.exceptions import TemplateError -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, StateType -from .const import ( - CONF_BUFFER_SIZE, - CONF_VALUE_ON, - DEFAULT_BUFFER_SIZE, - DEFAULT_NAME, - DEFAULT_SSL, - DEFAULT_TIMEOUT, - DEFAULT_VERIFY_SSL, -) -from .model import TcpSensorConfig +from .common import TCP_PLATFORM_SCHEMA, TcpEntity -_LOGGER: Final = logging.getLogger(__name__) - -PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_PAYLOAD): cv.string, - vol.Optional(CONF_BUFFER_SIZE, default=DEFAULT_BUFFER_SIZE): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_VALUE_ON): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - } -) +PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend(TCP_PLATFORM_SCHEMA) def setup_platform( @@ -71,46 +27,9 @@ def setup_platform( add_entities([TcpSensor(hass, config)]) -class TcpSensor(SensorEntity): +class TcpSensor(TcpEntity, SensorEntity): """Implementation of a TCP socket based sensor.""" - def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: - """Set all the config values if they exist and get initial state.""" - - value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = hass - - self._hass = hass - self._config: TcpSensorConfig = { - CONF_NAME: config[CONF_NAME], - CONF_HOST: config[CONF_HOST], - CONF_PORT: config[CONF_PORT], - CONF_TIMEOUT: config[CONF_TIMEOUT], - CONF_PAYLOAD: config[CONF_PAYLOAD], - CONF_UNIT_OF_MEASUREMENT: config.get(CONF_UNIT_OF_MEASUREMENT), - CONF_VALUE_TEMPLATE: value_template, - CONF_VALUE_ON: config.get(CONF_VALUE_ON), - CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE], - CONF_SSL: config[CONF_SSL], - CONF_VERIFY_SSL: config[CONF_VERIFY_SSL], - } - - self._ssl_context: ssl.SSLContext | None = None - if self._config[CONF_SSL]: - self._ssl_context = ssl.create_default_context() - if not self._config[CONF_VERIFY_SSL]: - self._ssl_context.check_hostname = False - self._ssl_context.verify_mode = ssl.CERT_NONE - - self._state: str | None = None - self.update() - - @property - def name(self) -> str: - """Return the name of this sensor.""" - return self._config[CONF_NAME] - @property def state(self) -> StateType: """Return the state of the device.""" @@ -120,64 +39,3 @@ class TcpSensor(SensorEntity): def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" return self._config[CONF_UNIT_OF_MEASUREMENT] - - def update(self) -> None: - """Get the latest value for this sensor.""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.settimeout(self._config[CONF_TIMEOUT]) - try: - sock.connect((self._config[CONF_HOST], self._config[CONF_PORT])) - except OSError as err: - _LOGGER.error( - "Unable to connect to %s on port %s: %s", - self._config[CONF_HOST], - self._config[CONF_PORT], - err, - ) - return - - if self._ssl_context is not None: - sock = self._ssl_context.wrap_socket( - sock, server_hostname=self._config[CONF_HOST] - ) - - try: - sock.send(self._config[CONF_PAYLOAD].encode()) - except OSError as err: - _LOGGER.error( - "Unable to send payload %r to %s on port %s: %s", - self._config[CONF_PAYLOAD], - self._config[CONF_HOST], - self._config[CONF_PORT], - err, - ) - return - - readable, _, _ = select.select([sock], [], [], self._config[CONF_TIMEOUT]) - if not readable: - _LOGGER.warning( - "Timeout (%s second(s)) waiting for a response after " - "sending %r to %s on port %s", - self._config[CONF_TIMEOUT], - self._config[CONF_PAYLOAD], - self._config[CONF_HOST], - self._config[CONF_PORT], - ) - return - - value = sock.recv(self._config[CONF_BUFFER_SIZE]).decode() - - value_template = self._config[CONF_VALUE_TEMPLATE] - if value_template is not None: - try: - self._state = value_template.render(parse_result=False, value=value) - return - except TemplateError: - _LOGGER.error( - "Unable to render template of %r with value: %r", - self._config[CONF_VALUE_TEMPLATE], - value, - ) - return - - self._state = value diff --git a/tests/components/tcp/test_binary_sensor.py b/tests/components/tcp/test_binary_sensor.py index 21dd84b1892..f8c13b41c30 100644 --- a/tests/components/tcp/test_binary_sensor.py +++ b/tests/components/tcp/test_binary_sensor.py @@ -20,9 +20,9 @@ TEST_ENTITY = "binary_sensor.test_name" def mock_socket_fixture(): """Mock the socket.""" with patch( - "homeassistant.components.tcp.sensor.socket.socket" + "homeassistant.components.tcp.common.socket.socket" ) as mock_socket, patch( - "homeassistant.components.tcp.sensor.select.select", + "homeassistant.components.tcp.common.select.select", return_value=(True, False, False), ): # yield the return value of the socket context manager diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index 48b5703c204..46db3367677 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import call, patch import pytest -import homeassistant.components.tcp.sensor as tcp +import homeassistant.components.tcp.common as tcp from homeassistant.setup import async_setup_component from tests.common import assert_setup_component @@ -41,7 +41,7 @@ socket_test_value = "value" @pytest.fixture(name="mock_socket") def mock_socket_fixture(mock_select): """Mock socket.""" - with patch("homeassistant.components.tcp.sensor.socket.socket") as mock_socket: + with patch("homeassistant.components.tcp.common.socket.socket") as mock_socket: socket_instance = mock_socket.return_value.__enter__.return_value socket_instance.recv.return_value = socket_test_value.encode() yield socket_instance @@ -51,7 +51,7 @@ def mock_socket_fixture(mock_select): def mock_select_fixture(): """Mock select.""" with patch( - "homeassistant.components.tcp.sensor.select.select", + "homeassistant.components.tcp.common.select.select", return_value=(True, False, False), ) as mock_select: yield mock_select @@ -61,7 +61,7 @@ def mock_select_fixture(): def mock_ssl_context_fixture(): """Mock select.""" with patch( - "homeassistant.components.tcp.sensor.ssl.create_default_context", + "homeassistant.components.tcp.common.ssl.create_default_context", ) as mock_ssl_context: mock_ssl_context.return_value.wrap_socket.return_value.recv.return_value = ( socket_test_value + "_ssl" From 2583e4bdc9ed22c18d50589eb2f975db7f515fd8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 May 2021 12:24:07 +0200 Subject: [PATCH 687/852] Add support for RGBW color to blebox light (#49562) --- homeassistant/components/blebox/light.py | 52 ++++---- tests/components/blebox/test_light.py | 153 ++++------------------- 2 files changed, 47 insertions(+), 158 deletions(-) diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index a825d102717..9bb7371a97a 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -5,19 +5,13 @@ from blebox_uniapi.error import BadOnValueError from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, + ATTR_RGBW_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_ONOFF, + COLOR_MODE_RGBW, LightEntity, ) -from homeassistant.util.color import ( - color_hs_to_RGB, - color_rgb_to_hex, - color_RGB_to_hs, - rgb_hex_to_rgb_list, -) +from homeassistant.util.color import color_rgb_to_hex, rgb_hex_to_rgb_list from . import BleBoxEntity, create_blebox_entities @@ -36,12 +30,9 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): """Representation of BleBox lights.""" @property - def supported_features(self): - """Return supported features.""" - white = SUPPORT_WHITE_VALUE if self._feature.supports_white else 0 - color = SUPPORT_COLOR if self._feature.supports_color else 0 - brightness = SUPPORT_BRIGHTNESS if self._feature.supports_brightness else 0 - return white | color | brightness + def supported_color_modes(self): + """Return supported color modes.""" + return {self.color_mode} @property def is_on(self): @@ -54,25 +45,27 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): return self._feature.brightness @property - def white_value(self): - """Return the white value.""" - return self._feature.white_value + def color_mode(self): + """Return the color mode.""" + if self._feature.supports_white and self._feature.supports_color: + return COLOR_MODE_RGBW + if self._feature.supports_brightness: + return COLOR_MODE_BRIGHTNESS + return COLOR_MODE_ONOFF @property - def hs_color(self): + def rgbw_color(self): """Return the hue and saturation.""" rgbw_hex = self._feature.rgbw_hex if rgbw_hex is None: return None - rgb = rgb_hex_to_rgb_list(rgbw_hex)[0:3] - return color_RGB_to_hs(*rgb) + return tuple(rgb_hex_to_rgb_list(rgbw_hex)[0:4]) async def async_turn_on(self, **kwargs): """Turn the light on.""" - white = kwargs.get(ATTR_WHITE_VALUE) - hs_color = kwargs.get(ATTR_HS_COLOR) + rgbw = kwargs.get(ATTR_RGBW_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) feature = self._feature @@ -81,12 +74,9 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): if brightness is not None: value = feature.apply_brightness(value, brightness) - if white is not None: - value = feature.apply_white(value, white) - - if hs_color is not None: - raw_rgb = color_rgb_to_hex(*color_hs_to_RGB(*hs_color)) - value = feature.apply_color(value, raw_rgb) + if rgbw is not None: + value = feature.apply_white(value, rgbw[3]) + value = feature.apply_color(value, color_rgb_to_hex(*rgbw[0:3])) try: await self._feature.async_on(value) diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index 6c8c26fe938..a73bba96fba 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -7,21 +7,13 @@ import pytest from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, -) -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - STATE_OFF, - STATE_ON, + ATTR_RGBW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_RGBW, ) +from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry as dr -from homeassistant.util import color from .conftest import async_setup_entity, mock_feature @@ -59,8 +51,8 @@ async def test_dimmer_init(dimmer, hass, config): state = hass.states.get(entity_id) assert state.name == "dimmerBox-brightness" - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert supported_features & SUPPORT_BRIGHTNESS + color_modes = state.attributes[ATTR_SUPPORTED_COLOR_MODES] + assert color_modes == [COLOR_MODE_BRIGHTNESS] assert state.attributes[ATTR_BRIGHTNESS] == 65 assert state.state == STATE_ON @@ -230,8 +222,8 @@ async def test_wlightbox_s_init(wlightbox_s, hass, config): state = hass.states.get(entity_id) assert state.name == "wLightBoxS-color" - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert supported_features & SUPPORT_BRIGHTNESS + color_modes = state.attributes[ATTR_SUPPORTED_COLOR_MODES] + assert color_modes == [COLOR_MODE_BRIGHTNESS] assert ATTR_BRIGHTNESS not in state.attributes assert state.state == STATE_OFF @@ -330,13 +322,11 @@ async def test_wlightbox_init(wlightbox, hass, config): state = hass.states.get(entity_id) assert state.name == "wLightBox-color" - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert supported_features & SUPPORT_WHITE_VALUE - assert supported_features & SUPPORT_COLOR + color_modes = state.attributes[ATTR_SUPPORTED_COLOR_MODES] + assert color_modes == [COLOR_MODE_RGBW] - assert ATTR_WHITE_VALUE not in state.attributes - assert ATTR_HS_COLOR not in state.attributes assert ATTR_BRIGHTNESS not in state.attributes + assert ATTR_RGBW_COLOR not in state.attributes assert state.state == STATE_OFF device_registry = dr.async_get(hass) @@ -363,12 +353,11 @@ async def test_wlightbox_update(wlightbox, hass, config): await async_setup_entity(hass, config, entity_id) state = hass.states.get(entity_id) - assert state.attributes[ATTR_HS_COLOR] == (352.32, 100.0) - assert state.attributes[ATTR_WHITE_VALUE] == 0x3A + assert state.attributes[ATTR_RGBW_COLOR] == (0xFA, 0x00, 0x20, 0x3A) assert state.state == STATE_ON -async def test_wlightbox_on_via_just_whiteness(wlightbox, hass, config): +async def test_wlightbox_on_rgbw(wlightbox, hass, config): """Test light on.""" feature_mock, entity_id = wlightbox @@ -385,125 +374,37 @@ async def test_wlightbox_on_via_just_whiteness(wlightbox, hass, config): def turn_on(value): feature_mock.is_on = True - assert value == "f1e2d3c7" + assert value == "c1d2f3c7" feature_mock.white_value = 0xC7 # on - feature_mock.rgbw_hex = "f1e2d3c7" + feature_mock.rgbw_hex = "c1d2f3c7" feature_mock.async_on = AsyncMock(side_effect=turn_on) def apply_white(value, white): - assert value == "f1e2d305" + assert value == "00010203" assert white == 0xC7 - return "f1e2d3c7" + return "000102c7" feature_mock.apply_white = apply_white - feature_mock.sensible_on_value = "f1e2d305" - - await hass.services.async_call( - "light", - SERVICE_TURN_ON, - {"entity_id": entity_id, ATTR_WHITE_VALUE: 0xC7}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - assert state.attributes[ATTR_WHITE_VALUE] == 0xC7 - - assert state.attributes[ATTR_HS_COLOR] == color.color_RGB_to_hs(0xF1, 0xE2, 0xD3) - - -async def test_wlightbox_on_via_reset_whiteness(wlightbox, hass, config): - """Test light on.""" - - feature_mock, entity_id = wlightbox - - def initial_update(): - feature_mock.is_on = False - - feature_mock.async_update = AsyncMock(side_effect=initial_update) - await async_setup_entity(hass, config, entity_id) - feature_mock.async_update = AsyncMock() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - def turn_on(value): - feature_mock.is_on = True - feature_mock.white_value = 0x0 - assert value == "f1e2d300" - feature_mock.rgbw_hex = "f1e2d300" - - feature_mock.async_on = AsyncMock(side_effect=turn_on) - - def apply_white(value, white): - assert value == "f1e2d305" - assert white == 0x0 - return "f1e2d300" - - feature_mock.apply_white = apply_white - - feature_mock.sensible_on_value = "f1e2d305" - - await hass.services.async_call( - "light", - SERVICE_TURN_ON, - {"entity_id": entity_id, ATTR_WHITE_VALUE: 0x0}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - assert state.attributes[ATTR_WHITE_VALUE] == 0x0 - assert state.attributes[ATTR_HS_COLOR] == color.color_RGB_to_hs(0xF1, 0xE2, 0xD3) - - -async def test_wlightbox_on_via_just_hsl_color(wlightbox, hass, config): - """Test light on.""" - - feature_mock, entity_id = wlightbox - - def initial_update(): - feature_mock.is_on = False - feature_mock.rgbw_hex = "00000000" - - feature_mock.async_update = AsyncMock(side_effect=initial_update) - await async_setup_entity(hass, config, entity_id) - feature_mock.async_update = AsyncMock() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - hs_color = color.color_RGB_to_hs(0xFF, 0xA1, 0xB2) - - def turn_on(value): - feature_mock.is_on = True - assert value == "ffa1b2e4" - feature_mock.white_value = 0xE4 - feature_mock.rgbw_hex = value - - feature_mock.async_on = AsyncMock(side_effect=turn_on) - def apply_color(value, color_value): - assert value == "c1a2e3e4" - assert color_value == "ffa0b1" - return "ffa1b2e4" + assert value == "000102c7" + assert color_value == "c1d2f3" + return "c1d2f3c7" feature_mock.apply_color = apply_color - feature_mock.sensible_on_value = "c1a2e3e4" + feature_mock.sensible_on_value = "00010203" await hass.services.async_call( "light", SERVICE_TURN_ON, - {"entity_id": entity_id, ATTR_HS_COLOR: hs_color}, + {"entity_id": entity_id, ATTR_RGBW_COLOR: (0xC1, 0xD2, 0xF3, 0xC7)}, blocking=True, ) state = hass.states.get(entity_id) - assert state.attributes[ATTR_HS_COLOR] == hs_color - assert state.attributes[ATTR_WHITE_VALUE] == 0xE4 assert state.state == STATE_ON + assert state.attributes[ATTR_RGBW_COLOR] == (0xC1, 0xD2, 0xF3, 0xC7) async def test_wlightbox_on_to_last_color(wlightbox, hass, config): @@ -538,8 +439,7 @@ async def test_wlightbox_on_to_last_color(wlightbox, hass, config): ) state = hass.states.get(entity_id) - assert state.attributes[ATTR_WHITE_VALUE] == 0xE4 - assert state.attributes[ATTR_HS_COLOR] == color.color_RGB_to_hs(0xF1, 0xE2, 0xD3) + assert state.attributes[ATTR_RGBW_COLOR] == (0xF1, 0xE2, 0xD3, 0xE4) assert state.state == STATE_ON @@ -573,8 +473,7 @@ async def test_wlightbox_off(wlightbox, hass, config): ) state = hass.states.get(entity_id) - assert ATTR_WHITE_VALUE not in state.attributes - assert ATTR_HS_COLOR not in state.attributes + assert ATTR_RGBW_COLOR not in state.attributes assert state.state == STATE_OFF From be13a73db8b56f64b6f200ab53817dab83d277a3 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 24 May 2021 12:59:55 +0200 Subject: [PATCH 688/852] Allow manual scan and add delay in switch verify. (#50974) --- homeassistant/components/modbus/__init__.py | 3 ++ .../components/modbus/base_platform.py | 7 ++- homeassistant/components/modbus/switch.py | 45 ++++++++-------- .../{test_modbus_switch.py => test_switch.py} | 53 ++++++++++++++++++- 4 files changed, 82 insertions(+), 26 deletions(-) rename tests/components/modbus/{test_modbus_switch.py => test_switch.py} (81%) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index c6336739bf2..d1c3c1a0c8a 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -140,6 +140,8 @@ def control_scan_interval(config: dict) -> dict: for entry in hub[conf_key]: scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) if scan_interval < MINIMUM_SCAN_INTERVAL: + if scan_interval == 0: + continue _LOGGER.warning( "%s %s scan_interval(%d) is adjusted to minimum(%d)", component, @@ -236,6 +238,7 @@ SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( ), vol.Optional(CONF_STATE_OFF): cv.positive_int, vol.Optional(CONF_STATE_ON): cv.positive_int, + vol.Optional(CONF_DELAY, default=0): cv.positive_int, } ), } diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index ddfe11717de..d6811bca1ff 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -36,7 +36,7 @@ class BasePlatform(Entity): self._input_type = entry[CONF_INPUT_TYPE] self._value = None self._available = True - self._scan_interval = timedelta(seconds=entry[CONF_SCAN_INTERVAL]) + self._scan_interval = int(entry[CONF_SCAN_INTERVAL]) @abstractmethod async def async_update(self, now=None): @@ -44,7 +44,10 @@ class BasePlatform(Entity): async def async_base_added_to_hass(self): """Handle entity which will be added.""" - async_track_time_interval(self.hass, self.async_update, self._scan_interval) + if self._scan_interval > 0: + async_track_time_interval( + self.hass, self.async_update, timedelta(seconds=self._scan_interval) + ) @property def name(self): diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index ef068d7bd18..502a6fc73b9 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -8,11 +8,13 @@ from homeassistant.const import ( CONF_ADDRESS, CONF_COMMAND_OFF, CONF_COMMAND_ON, + CONF_DELAY, CONF_NAME, CONF_SWITCHES, STATE_ON, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType @@ -64,6 +66,7 @@ class ModbusSwitch(BasePlatform, SwitchEntity, RestoreEntity): if config[CONF_VERIFY] is None: config[CONF_VERIFY] = {} self._verify_active = True + self._verify_delay = config[CONF_VERIFY].get(CONF_DELAY, 0) self._verify_address = config[CONF_VERIFY].get( CONF_ADDRESS, config[CONF_ADDRESS] ) @@ -87,38 +90,34 @@ class ModbusSwitch(BasePlatform, SwitchEntity, RestoreEntity): """Return true if switch is on.""" return self._is_on - async def async_turn_on(self, **kwargs): - """Set switch on.""" - + async def _async_turn(self, command): + """Evaluate switch result.""" result = await self._hub.async_pymodbus_call( - self._slave, self._address, self._command_on, self._write_type + self._slave, self._address, command, self._write_type ) if result is None: self._available = False self.async_write_ha_state() + return + + self._available = True + if not self._verify_active: + self._is_on = command == self._command_on + self.async_write_ha_state() + return + + if self._verify_delay: + async_call_later(self.hass, self._verify_delay, self.async_update) else: - self._available = True - if self._verify_active: - await self.async_update() - else: - self._is_on = True - self.async_write_ha_state() + await self.async_update() + + async def async_turn_on(self, **kwargs): + """Set switch on.""" + await self._async_turn(self._command_on) async def async_turn_off(self, **kwargs): """Set switch off.""" - result = await self._hub.async_pymodbus_call( - self._slave, self._address, self._command_off, self._write_type - ) - if result is None: - self._available = False - self.async_write_ha_state() - else: - self._available = True - if self._verify_active: - await self.async_update() - else: - self._is_on = False - self.async_write_ha_state() + await self._async_turn(self._command_off) async def async_update(self, now=None): """Update the entity state.""" diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_switch.py similarity index 81% rename from tests/components/modbus/test_modbus_switch.py rename to tests/components/modbus/test_switch.py index 8cb1cf69dc9..37ddfec2b4d 100644 --- a/tests/components/modbus/test_modbus_switch.py +++ b/tests/components/modbus/test_switch.py @@ -1,4 +1,7 @@ """The tests for the Modbus switch component.""" +from datetime import timedelta +from unittest import mock + from pymodbus.exceptions import ModbusException import pytest @@ -19,10 +22,12 @@ from homeassistant.const import ( CONF_ADDRESS, CONF_COMMAND_OFF, CONF_COMMAND_ON, + CONF_DELAY, CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SLAVE, CONF_SWITCHES, CONF_TYPE, @@ -32,10 +37,11 @@ from homeassistant.const import ( ) from homeassistant.core import State from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util from .conftest import ReadResult, base_config_test, base_test, prepare_service_update -from tests.common import mock_restore_cache +from tests.common import async_fire_time_changed, mock_restore_cache @pytest.mark.parametrize( @@ -72,6 +78,7 @@ from tests.common import mock_restore_cache CONF_ADDRESS: 1235, CONF_STATE_OFF: 0, CONF_STATE_ON: 1, + CONF_DELAY: 10, }, }, { @@ -93,6 +100,7 @@ from tests.common import mock_restore_cache CONF_COMMAND_OFF: 0x00, CONF_COMMAND_ON: 0x01, CONF_DEVICE_CLASS: "switch", + CONF_SCAN_INTERVAL: 0, CONF_VERIFY: None, }, ], @@ -292,3 +300,46 @@ async def test_service_switch_update(hass, mock_pymodbus): "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == STATE_OFF + + +async def test_delay_switch(hass, mock_pymodbus): + """Run test for switch verify delay.""" + + switch_name = "test_switch" + entity_id = f"{SWITCH_DOMAIN}.{switch_name}" + + config = { + MODBUS_DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_SWITCHES: [ + { + CONF_NAME: switch_name, + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 0, + CONF_VERIFY: { + CONF_DELAY: 1, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + } + ], + } + ] + } + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True + await hass.async_block_till_done() + await hass.services.async_call( + "switch", "turn_on", service_data={"entity_id": entity_id} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + now = now + timedelta(seconds=2) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ON From c497c0eadd7823173023e4f31f863fe3bd98dfea Mon Sep 17 00:00:00 2001 From: hesselonline Date: Mon, 24 May 2021 13:08:24 +0200 Subject: [PATCH 689/852] Add wallbox integration (#48082) * Wallbox component added * resolved mergeconflicts from upstream * fixed an incorrect removal in CODEOWNERS file * fixes for pullrequest automatic test * clean up code after PR tests * fixed strings.json * fix config_flow error > wallbox * fixed some formatting issues * fix pylint warnings * fixed error in number.py > set value * pylint warnings fixed * some more pylint fixes * isort fixes * fix unused_import pylint * remove tests * remove test requirements * config flow test * test errors resolved * test file formatting * isort on test file * sensor test * isort on test * isort test const * remove not working sensor test * remove test const * add switch, number and lock test * docstrings for test classes * sort test_number, create test_sensor * additional tests * fix test error * reduced PR to 1 component * newline in const * ignore test coverage -> dependency on external device (wallbox) * do not ignore config_flow * add test for validate_input * remove obsolete import * additional test config flow * change test sensor * docstring * add additional test for exceptions * fix test_config * more tests * fix test_config_flow * fixed http error test * catch connectionerror and introduce testing for this error * remove .coveragefile * change comment * Update homeassistant/components/wallbox/__init__.py review suggestion by janiversen Co-authored-by: jan iversen * Update homeassistant/components/wallbox/__init__.py review suggestion by janiversen (format only) Co-authored-by: jan iversen * Processed review comments, include more testing for sensor component * Isolated the async_add_executor_job to make the solution more async * add a config flow test * Revert "add a config flow test" This reverts commit 9c1af82fffeb0b46f11ada1000e19b66fd5fd0f1. * Revert "Isolated the async_add_executor_job to make the solution more async" This reverts commit 0bf034c3318f27e649389830d4ad7a7e10eb2d6f. * Make component more async and add config flow tests * Changes based on review comments * made _ methods in WallboxHub for the 'non-async' call to the API and try-catch. Stored the wallbox in the class. * moved the coordinator to __init__ and pass it as part of the WallboxHub class * removed obsolete function in __init__ * removed CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL * fixed spelling and imports on test files * did isort on component files Co-authored-by: jan iversen --- CODEOWNERS | 1 + homeassistant/components/wallbox/__init__.py | 147 ++++++++++++++++ .../components/wallbox/config_flow.py | 58 ++++++ homeassistant/components/wallbox/const.py | 99 +++++++++++ .../components/wallbox/manifest.json | 13 ++ homeassistant/components/wallbox/sensor.py | 61 +++++++ homeassistant/components/wallbox/strings.json | 22 +++ .../components/wallbox/translations/en.json | 22 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/wallbox/__init__.py | 1 + tests/components/wallbox/test_config_flow.py | 128 ++++++++++++++ tests/components/wallbox/test_init.py | 165 ++++++++++++++++++ tests/components/wallbox/test_sensor.py | 81 +++++++++ 15 files changed, 805 insertions(+) create mode 100644 homeassistant/components/wallbox/__init__.py create mode 100644 homeassistant/components/wallbox/config_flow.py create mode 100644 homeassistant/components/wallbox/const.py create mode 100644 homeassistant/components/wallbox/manifest.json create mode 100644 homeassistant/components/wallbox/sensor.py create mode 100644 homeassistant/components/wallbox/strings.json create mode 100644 homeassistant/components/wallbox/translations/en.json create mode 100644 tests/components/wallbox/__init__.py create mode 100644 tests/components/wallbox/test_config_flow.py create mode 100644 tests/components/wallbox/test_init.py create mode 100644 tests/components/wallbox/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index e7f20d27e18..3d7a53749cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -544,6 +544,7 @@ homeassistant/components/vlc_telnet/* @rodripf @dmcc homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volumio/* @OnFreund homeassistant/components/wake_on_lan/* @ntilley905 +homeassistant/components/wallbox/* @hesselonline homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai homeassistant/components/weather/* @fabaff diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py new file mode 100644 index 00000000000..97b2ea12f35 --- /dev/null +++ b/homeassistant/components/wallbox/__init__.py @@ -0,0 +1,147 @@ +"""The Wallbox integration.""" +import asyncio +from datetime import timedelta +import logging + +import requests +from wallbox import Wallbox + +from homeassistant import exceptions +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_CONNECTIONS, CONF_ROUND, CONF_SENSOR_TYPES, CONF_STATION, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] +UPDATE_INTERVAL = 30 + + +class WallboxHub: + """Wallbox Hub class.""" + + def __init__(self, station, username, password, hass): + """Initialize.""" + self._station = station + self._username = username + self._password = password + self._wallbox = Wallbox(self._username, self._password) + self._hass = hass + self._coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="wallbox", + update_method=self.async_get_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + def _authenticate(self): + """Authenticate using Wallbox API.""" + try: + self._wallbox.authenticate() + return True + except requests.exceptions.HTTPError as wallbox_connection_error: + if wallbox_connection_error.response.status_code == 403: + raise InvalidAuth from wallbox_connection_error + raise ConnectionError from wallbox_connection_error + + def _get_data(self): + """Get new sensor data for Wallbox component.""" + try: + self._authenticate() + data = self._wallbox.getChargerStatus(self._station) + + filtered_data = {k: data[k] for k in CONF_SENSOR_TYPES if k in data} + + for key, value in filtered_data.items(): + sensor_round = CONF_SENSOR_TYPES[key][CONF_ROUND] + if sensor_round: + try: + filtered_data[key] = round(value, sensor_round) + except TypeError: + _LOGGER.debug("Cannot format %s", key) + + return filtered_data + except requests.exceptions.HTTPError as wallbox_connection_error: + raise ConnectionError from wallbox_connection_error + + async def async_coordinator_first_refresh(self): + """Refresh coordinator for the first time.""" + await self._coordinator.async_config_entry_first_refresh() + + async def async_authenticate(self) -> bool: + """Authenticate using Wallbox API.""" + return await self._hass.async_add_executor_job(self._authenticate) + + async def async_get_data(self) -> bool: + """Get new sensor data for Wallbox component.""" + data = await self._hass.async_add_executor_job(self._get_data) + return data + + @property + def coordinator(self): + """Return the coordinator.""" + return self._coordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Wallbox from a config entry.""" + wallbox = WallboxHub( + entry.data[CONF_STATION], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + hass, + ) + + await wallbox.async_authenticate() + + await wallbox.async_coordinator_first_refresh() + + hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}}) + hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = wallbox + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN]["connections"].pop(entry.entry_id) + + return unload_ok + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + def __init__(self, msg=""): + """Create a log record.""" + super().__init__() + _LOGGER.error("Cannot connect to Wallbox API. %s", msg) + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + def __init__(self, msg=""): + """Create a log record.""" + super().__init__() + _LOGGER.error("Cannot authenticate with Wallbox API. %s", msg) diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py new file mode 100644 index 00000000000..69b01d96c40 --- /dev/null +++ b/homeassistant/components/wallbox/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow for Wallbox integration.""" +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from . import CannotConnect, InvalidAuth, WallboxHub +from .const import CONF_STATION, DOMAIN + +COMPONENT_DOMAIN = DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_STATION): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + hub = WallboxHub(data["station"], data["username"], data["password"], hass) + + await hub.async_get_data() + + # Return info that you want to store in the config entry. + return {"title": "Wallbox Portal"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): + """Handle a config flow for Wallbox.""" + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py new file mode 100644 index 00000000000..41996107ce0 --- /dev/null +++ b/homeassistant/components/wallbox/const.py @@ -0,0 +1,99 @@ +"""Constants for the Wallbox integration.""" +from homeassistant.const import ( + CONF_ICON, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + ELECTRICAL_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, + LENGTH_KILOMETERS, + PERCENTAGE, + POWER_KILO_WATT, + STATE_UNAVAILABLE, +) + +DOMAIN = "wallbox" + +CONF_STATION = "station" + +CONF_CONNECTIONS = "connections" +CONF_ROUND = "round" + +CONF_SENSOR_TYPES = { + "charging_power": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Charging Power", + CONF_ROUND: 2, + CONF_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + STATE_UNAVAILABLE: False, + }, + "max_available_power": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Max Available Power", + CONF_ROUND: 0, + CONF_UNIT_OF_MEASUREMENT: ELECTRICAL_CURRENT_AMPERE, + STATE_UNAVAILABLE: False, + }, + "charging_speed": { + CONF_ICON: "mdi:speedometer", + CONF_NAME: "Charging Speed", + CONF_ROUND: 0, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "added_range": { + CONF_ICON: "mdi:map-marker-distance", + CONF_NAME: "Added Range", + CONF_ROUND: 0, + CONF_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + STATE_UNAVAILABLE: False, + }, + "added_energy": { + CONF_ICON: "mdi:battery-positive", + CONF_NAME: "Added Energy", + CONF_ROUND: 2, + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + STATE_UNAVAILABLE: False, + }, + "charging_time": { + CONF_ICON: "mdi:timer", + CONF_NAME: "Charging Time", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "cost": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Cost", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "state_of_charge": { + CONF_ICON: "mdi:battery-charging-80", + CONF_NAME: "State of Charge", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, + STATE_UNAVAILABLE: False, + }, + "current_mode": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Current Mode", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "depot_price": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Depot Price", + CONF_ROUND: 2, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "status_description": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Status Description", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, +} diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json new file mode 100644 index 00000000000..aeadf541345 --- /dev/null +++ b/homeassistant/components/wallbox/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "wallbox", + "name": "Wallbox", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/wallbox", + "requirements": ["wallbox==0.4.4"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@hesselonline"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py new file mode 100644 index 00000000000..6d3ef952cbe --- /dev/null +++ b/homeassistant/components/wallbox/sensor.py @@ -0,0 +1,61 @@ +"""Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + CONF_CONNECTIONS, + CONF_ICON, + CONF_NAME, + CONF_SENSOR_TYPES, + CONF_UNIT_OF_MEASUREMENT, + DOMAIN, +) + +CONF_STATION = "station" +UPDATE_INTERVAL = 30 + + +async def async_setup_entry(hass, config, async_add_entities): + """Create wallbox sensor entities in HASS.""" + wallbox = hass.data[DOMAIN][CONF_CONNECTIONS][config.entry_id] + + coordinator = wallbox.coordinator + + async_add_entities( + WallboxSensor(coordinator, idx, ent, config) + for idx, ent in enumerate(coordinator.data) + ) + + +class WallboxSensor(CoordinatorEntity, Entity): + """Representation of the Wallbox portal.""" + + def __init__(self, coordinator, idx, ent, config): + """Initialize a Wallbox sensor.""" + super().__init__(coordinator) + self._properties = CONF_SENSOR_TYPES[ent] + self._name = f"{config.title} {self._properties[CONF_NAME]}" + self._icon = self._properties[CONF_ICON] + self._unit = self._properties[CONF_UNIT_OF_MEASUREMENT] + self._ent = ent + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._ent] + + @property + def unit_of_measurement(self): + """Return the unit of the sensor.""" + return self._unit + + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon diff --git a/homeassistant/components/wallbox/strings.json b/homeassistant/components/wallbox/strings.json new file mode 100644 index 00000000000..63fc5d89e85 --- /dev/null +++ b/homeassistant/components/wallbox/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Wallbox", + "config": { + "step": { + "user": { + "data": { + "station": "Station Serial Number", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/en.json b/homeassistant/components/wallbox/translations/en.json new file mode 100644 index 00000000000..a63fe801490 --- /dev/null +++ b/homeassistant/components/wallbox/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "station": "Station S/N", + "password": "Password", + "username": "Username" + } + } + } + }, + "title": "MyWallbox" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3a28a24315b..d927b244ede 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -275,6 +275,7 @@ FLOWS = [ "vilfo", "vizio", "volumio", + "wallbox", "waze_travel_time", "wemo", "wiffi", diff --git a/requirements_all.txt b/requirements_all.txt index 156b75bb8a2..63286d2e081 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2332,6 +2332,9 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==2.0.1 +# homeassistant.components.wallbox +wallbox==0.4.4 + # homeassistant.components.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac6bcea193f..91618f263fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1256,6 +1256,9 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==2.0.1 +# homeassistant.components.wallbox +wallbox==0.4.4 + # homeassistant.components.folder_watcher watchdog==2.1.2 diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py new file mode 100644 index 00000000000..35bf3cee242 --- /dev/null +++ b/tests/components/wallbox/__init__.py @@ -0,0 +1 @@ +"""Tests for the Wallbox integration.""" diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py new file mode 100644 index 00000000000..074f67abe2c --- /dev/null +++ b/tests/components/wallbox/test_config_flow.py @@ -0,0 +1,128 @@ +"""Test the Wallbox config flow.""" +from unittest.mock import patch + +from voluptuous.schema_builder import raises + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.wallbox import CannotConnect, InvalidAuth, config_flow +from homeassistant.components.wallbox.const import DOMAIN +from homeassistant.core import HomeAssistant + + +async def test_show_set_form(hass: HomeAssistant) -> None: + """Test that the setup form is served.""" + flow = config_flow.ConfigFlow() + flow.hass = hass + result = await flow.async_step_user(user_input=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +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.wallbox.config_flow.WallboxHub.async_authenticate", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345", + "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.wallbox.config_flow.WallboxHub.async_authenticate", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_validate_input(hass): + """Test we can validate input.""" + data = { + "station": "12345", + "username": "test-username", + "password": "test-password", + } + + def alternate_authenticate_method(): + return None + + def alternate_get_charger_status_method(station): + data = '{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}' + return data + + with patch( + "wallbox.Wallbox.authenticate", + side_effect=alternate_authenticate_method, + ), patch( + "wallbox.Wallbox.getChargerStatus", + side_effect=alternate_get_charger_status_method, + ): + + result = await config_flow.validate_input(hass, data) + + assert result == {"title": "Wallbox Portal"} + + +async def test_configflow_class(): + """Test configFlow class.""" + configflow = config_flow.ConfigFlow() + assert configflow + + with patch( + "homeassistant.components.wallbox.config_flow.validate_input", + side_effect=TypeError, + ), raises(Exception): + assert await configflow.async_step_user(True) + + with patch( + "homeassistant.components.wallbox.config_flow.validate_input", + side_effect=CannotConnect, + ), raises(Exception): + assert await configflow.async_step_user(True) + + with patch( + "homeassistant.components.wallbox.config_flow.validate_input", + ), raises(Exception): + assert await configflow.async_step_user(True) + + +def test_cannot_connect_class(): + """Test cannot Connect class.""" + cannot_connect = CannotConnect + assert cannot_connect + + +def test_invalid_auth_class(): + """Test invalid auth class.""" + invalid_auth = InvalidAuth + assert invalid_auth diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py new file mode 100644 index 00000000000..892e77dc7f6 --- /dev/null +++ b/tests/components/wallbox/test_init.py @@ -0,0 +1,165 @@ +"""Test Wallbox Init Component.""" +import json + +import pytest +import requests_mock +from voluptuous.schema_builder import raises + +from homeassistant.components import wallbox +from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "test_username", + CONF_PASSWORD: "test_password", + CONF_STATION: "12345", + }, + entry_id="testEntry", +) + +test_response = json.loads( + '{"charging_power": 0,"max_available_power": 25,"charging_speed": 0,"added_range": 372,"added_energy": 44.697}' +) + +test_response_rounding_error = json.loads( + '{"charging_power": "XX","max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "XX"}' +) + + +async def test_wallbox_setup_entry(hass: HomeAssistantType): + """Test Wallbox Setup.""" + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/12345", + text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}', + status_code=200, + ) + assert await wallbox.async_setup_entry(hass, entry) + + with requests_mock.Mocker() as m, raises(ConnectionError): + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":404}', + status_code=404, + ) + assert await wallbox.async_setup_entry(hass, entry) is False + + +async def test_wallbox_unload_entry(hass: HomeAssistantType): + """Test Wallbox Unload.""" + hass.data[DOMAIN] = {"connections": {entry.entry_id: entry}} + + assert await wallbox.async_unload_entry(hass, entry) + + hass.data[DOMAIN] = {"fail_entry": entry} + + with pytest.raises(KeyError): + await wallbox.async_unload_entry(hass, entry) + + +async def test_get_data(hass: HomeAssistantType): + """Test hub class, get_data.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + json=test_response, + status_code=200, + ) + assert await hub.async_get_data() + + +async def test_get_data_rounding_error(hass: HomeAssistantType): + """Test hub class, get_data with rounding error.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + json=test_response_rounding_error, + status_code=200, + ) + assert await hub.async_get_data() + + +async def test_authentication_exception(hass: HomeAssistantType): + """Test hub class, authentication raises exception.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth): + m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403) + + assert await hub.async_authenticate() + + with requests_mock.Mocker() as m, raises(ConnectionError): + m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=404) + + assert await hub.async_authenticate() + + with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth): + m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403) + m.get( + "https://api.wall-box.com/chargers/status/test", + json=test_response, + status_code=403, + ) + assert await hub.async_get_data() + + +async def test_get_data_exception(hass: HomeAssistantType): + """Test hub class, authentication raises exception.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m, raises(ConnectionError): + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + text="data", + status_code=404, + ) + assert await hub.async_get_data() diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py new file mode 100644 index 00000000000..5c0c3511a30 --- /dev/null +++ b/tests/components/wallbox/test_sensor.py @@ -0,0 +1,81 @@ +"""Test Wallbox Switch component.""" + +import json +from unittest.mock import MagicMock + +from homeassistant.components.wallbox import sensor +from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "test_username", + CONF_PASSWORD: "test_password", + CONF_STATION: "12345", + }, + entry_id="testEntry", +) + +test_response = json.loads( + '{"charging_power": 0,"max_available_power": 25,"charging_speed": 0,"added_range": 372,"added_energy": 44.697}' +) + +test_response_rounding_error = json.loads( + '{"charging_power": "XX","max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "XX"}' +) + +CONF_STATION = ("12345",) +CONF_USERNAME = ("test-username",) +CONF_PASSWORD = "test-password" + +# wallbox = WallboxHub(CONF_STATION, CONF_USERNAME, CONF_PASSWORD, hass) + + +async def test_wallbox_sensor_class(): + """Test wallbox sensor class.""" + + coordinator = MagicMock(return_value="connected") + idx = 1 + ent = "charging_power" + + wallboxSensor = sensor.WallboxSensor(coordinator, idx, ent, entry) + + assert wallboxSensor.icon == "mdi:ev-station" + assert wallboxSensor.unit_of_measurement == "kW" + assert wallboxSensor.name == "Mock Title Charging Power" + assert wallboxSensor.state + + +# async def test_wallbox_updater(hass: HomeAssistantType): +# """Test wallbox updater.""" +# with requests_mock.Mocker() as m: +# m.get( +# "https://api.wall-box.com/auth/token/user", +# text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', +# status_code=200, +# ) +# m.get( +# "https://api.wall-box.com/chargers/status/('12345',)", +# json=test_response, +# status_code=200, +# ) +# await sensor.wallbox_updater(wallbox, hass) + + +# async def test_wallbox_updater_rounding_error(hass: HomeAssistantType): +# """Test wallbox updater rounding error.""" +# with requests_mock.Mocker() as m: +# m.get( +# "https://api.wall-box.com/auth/token/user", +# text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', +# status_code=200, +# ) +# m.get( +# "https://api.wall-box.com/chargers/status/('12345',)", +# json=test_response_rounding_error, +# status_code=200, +# ) +# await sensor.wallbox_updater(wallbox, hass) From 85495c08b01ea36850ec67e7e52f490fa7abe023 Mon Sep 17 00:00:00 2001 From: jacekpaszkowski Date: Mon, 24 May 2021 13:31:57 +0200 Subject: [PATCH 690/852] Add support for effects, transition/brightness parameters to template light, min_mireds and max_mireds templates (#43850) * Add support for effects, transition/brightness parameters to template light, min_mireds and max_mireds templates * code fixes * min_mireds, max_mireds fixes * test fixes * more fixes * format fix * style fix * _update_effect_list change * style fix * Fixes after review * additional fixes * duplicated lines removed * fixes after CI run * test fixes * code and test fixes * supports transition change, added test cases --- homeassistant/components/template/light.py | 250 +++++++++- tests/components/template/test_light.py | 533 ++++++++++++++++++++- 2 files changed, 772 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 2479388eaaf..f546c5dc4da 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -6,12 +6,16 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, LightEntity, ) @@ -49,6 +53,12 @@ CONF_COLOR_TEMPLATE = "color_template" CONF_COLOR_ACTION = "set_color" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" CONF_WHITE_VALUE_ACTION = "set_white_value" +CONF_EFFECT_ACTION = "set_effect" +CONF_EFFECT_LIST_TEMPLATE = "effect_list_template" +CONF_EFFECT_TEMPLATE = "effect_template" +CONF_MAX_MIREDS_TEMPLATE = "max_mireds_template" +CONF_MIN_MIREDS_TEMPLATE = "min_mireds_template" +CONF_SUPPORTS_TRANSITION = "supports_transition_template" LIGHT_SCHEMA = vol.All( cv.deprecated(CONF_ENTITY_ID), @@ -70,6 +80,12 @@ LIGHT_SCHEMA = vol.All( vol.Optional(CONF_COLOR_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_WHITE_VALUE_ACTION): cv.SCRIPT_SCHEMA, + vol.Inclusive(CONF_EFFECT_LIST_TEMPLATE, "effect"): cv.template, + vol.Inclusive(CONF_EFFECT_TEMPLATE, "effect"): cv.template, + vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_MAX_MIREDS_TEMPLATE): cv.template, + vol.Optional(CONF_MIN_MIREDS_TEMPLATE): cv.template, + vol.Optional(CONF_SUPPORTS_TRANSITION): cv.template, vol.Optional(CONF_UNIQUE_ID): cv.string, } ), @@ -108,6 +124,15 @@ async def _async_create_entities(hass, config): white_value_action = device_config.get(CONF_WHITE_VALUE_ACTION) white_value_template = device_config.get(CONF_WHITE_VALUE_TEMPLATE) + effect_action = device_config.get(CONF_EFFECT_ACTION) + effect_list_template = device_config.get(CONF_EFFECT_LIST_TEMPLATE) + effect_template = device_config.get(CONF_EFFECT_TEMPLATE) + + max_mireds_template = device_config.get(CONF_MAX_MIREDS_TEMPLATE) + min_mireds_template = device_config.get(CONF_MIN_MIREDS_TEMPLATE) + + supports_transition_template = device_config.get(CONF_SUPPORTS_TRANSITION) + lights.append( LightTemplate( hass, @@ -127,6 +152,12 @@ async def _async_create_entities(hass, config): color_template, white_value_action, white_value_template, + effect_action, + effect_list_template, + effect_template, + max_mireds_template, + min_mireds_template, + supports_transition_template, unique_id, ) ) @@ -161,6 +192,12 @@ class LightTemplate(TemplateEntity, LightEntity): color_template, white_value_action, white_value_template, + effect_action, + effect_list_template, + effect_template, + max_mireds_template, + min_mireds_template, + supports_transition_template, unique_id, ): """Initialize the light.""" @@ -197,12 +234,25 @@ class LightTemplate(TemplateEntity, LightEntity): hass, white_value_action, friendly_name, domain ) self._white_value_template = white_value_template + self._effect_script = None + if effect_action is not None: + self._effect_script = Script(hass, effect_action, friendly_name, domain) + self._effect_list_template = effect_list_template + self._effect_template = effect_template + self._max_mireds_template = max_mireds_template + self._min_mireds_template = min_mireds_template + self._supports_transition_template = supports_transition_template self._state = False self._brightness = None self._temperature = None self._color = None self._white_value = None + self._effect = None + self._effect_list = None + self._max_mireds = None + self._min_mireds = None + self._supports_transition = False self._unique_id = unique_id @property @@ -215,6 +265,22 @@ class LightTemplate(TemplateEntity, LightEntity): """Return the CT color value in mireds.""" return self._temperature + @property + def max_mireds(self): + """Return the max mireds value in mireds.""" + if self._max_mireds is not None: + return self._max_mireds + + return super().max_mireds + + @property + def min_mireds(self): + """Return the min mireds value in mireds.""" + if self._min_mireds is not None: + return self._min_mireds + + return super().min_mireds + @property def white_value(self): """Return the white value.""" @@ -225,6 +291,16 @@ class LightTemplate(TemplateEntity, LightEntity): """Return the hue and saturation color value [float, float].""" return self._color + @property + def effect(self): + """Return the effect.""" + return self._effect + + @property + def effect_list(self): + """Return the effect list.""" + return self._effect_list + @property def name(self): """Return the display name of this light.""" @@ -247,6 +323,10 @@ class LightTemplate(TemplateEntity, LightEntity): supported_features |= SUPPORT_COLOR if self._white_value_script is not None: supported_features |= SUPPORT_WHITE_VALUE + if self._effect_script is not None: + supported_features |= SUPPORT_EFFECT + if self._supports_transition is True: + supported_features |= SUPPORT_TRANSITION return supported_features @property @@ -268,6 +348,22 @@ class LightTemplate(TemplateEntity, LightEntity): self._update_brightness, none_on_template_error=True, ) + if self._max_mireds_template: + self.add_template_attribute( + "_max_mireds_template", + self._max_mireds_template, + None, + self._update_max_mireds, + none_on_template_error=True, + ) + if self._min_mireds_template: + self.add_template_attribute( + "_min_mireds_template", + self._min_mireds_template, + None, + self._update_min_mireds, + none_on_template_error=True, + ) if self._temperature_template: self.add_template_attribute( "_temperature", @@ -292,6 +388,30 @@ class LightTemplate(TemplateEntity, LightEntity): self._update_white_value, none_on_template_error=True, ) + if self._effect_list_template: + self.add_template_attribute( + "_effect_list", + self._effect_list_template, + None, + self._update_effect_list, + none_on_template_error=True, + ) + if self._effect_template: + self.add_template_attribute( + "_effect", + self._effect_template, + None, + self._update_effect, + none_on_template_error=True, + ) + if self._supports_transition_template: + self.add_template_attribute( + "_supports_transition_template", + self._supports_transition_template, + None, + self._update_supports_transition, + none_on_template_error=True, + ) await super().async_added_to_hass() async def async_turn_on(self, **kwargs): @@ -324,33 +444,65 @@ class LightTemplate(TemplateEntity, LightEntity): self._temperature = kwargs[ATTR_COLOR_TEMP] optimistic_set = True - if ATTR_BRIGHTNESS in kwargs and self._level_script: - await self._level_script.async_run( - {"brightness": kwargs[ATTR_BRIGHTNESS]}, context=self._context - ) - elif ATTR_COLOR_TEMP in kwargs and self._temperature_script: + common_params = {} + + if ATTR_BRIGHTNESS in kwargs: + common_params["brightness"] = kwargs[ATTR_BRIGHTNESS] + + if ATTR_TRANSITION in kwargs and self._supports_transition is True: + common_params["transition"] = kwargs[ATTR_TRANSITION] + + if ATTR_COLOR_TEMP in kwargs and self._temperature_script: + common_params["color_temp"] = kwargs[ATTR_COLOR_TEMP] + await self._temperature_script.async_run( - {"color_temp": kwargs[ATTR_COLOR_TEMP]}, context=self._context + common_params, context=self._context ) elif ATTR_WHITE_VALUE in kwargs and self._white_value_script: + common_params["white_value"] = kwargs[ATTR_WHITE_VALUE] + await self._white_value_script.async_run( - {"white_value": kwargs[ATTR_WHITE_VALUE]}, context=self._context + common_params, context=self._context ) + elif ATTR_EFFECT in kwargs and self._effect_script: + effect = kwargs[ATTR_EFFECT] + if effect not in self._effect_list: + _LOGGER.error( + "Received invalid effect: %s. Expected one of: %s", + effect, + self._effect_list, + exc_info=True, + ) + + common_params["effect"] = effect + + await self._effect_script.async_run(common_params, context=self._context) elif ATTR_HS_COLOR in kwargs and self._color_script: hs_value = kwargs[ATTR_HS_COLOR] + common_params["hs"] = hs_value + common_params["h"] = int(hs_value[0]) + common_params["s"] = int(hs_value[1]) + await self._color_script.async_run( - {"hs": hs_value, "h": int(hs_value[0]), "s": int(hs_value[1])}, + common_params, context=self._context, ) + elif ATTR_BRIGHTNESS in kwargs and self._level_script: + await self._level_script.async_run(common_params, context=self._context) else: - await self._on_script.async_run(context=self._context) + await self._on_script.async_run(common_params, context=self._context) if optimistic_set: self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the light off.""" - await self._off_script.async_run(context=self._context) + if ATTR_TRANSITION in kwargs and self._supports_transition is True: + await self._off_script.async_run( + {"transition": kwargs[ATTR_TRANSITION]}, context=self._context + ) + else: + await self._off_script.async_run(context=self._context) if self._template is None: self._state = False self.async_write_ha_state() @@ -397,6 +549,45 @@ class LightTemplate(TemplateEntity, LightEntity): ) self._white_value = None + @callback + def _update_effect_list(self, effect_list): + """Update the effect list from the template.""" + if effect_list in ("None", ""): + self._effect_list = None + return + + if not isinstance(effect_list, list): + _LOGGER.error( + "Received invalid effect list: %s. Expected list of strings", + effect_list, + ) + self._effect_list = None + return + + if len(effect_list) == 0: + self._effect_list = None + return + + self._effect_list = effect_list + + @callback + def _update_effect(self, effect): + """Update the effect from the template.""" + if effect in ("None", ""): + self._effect = None + return + + if effect not in self._effect_list: + _LOGGER.error( + "Received invalid effect: %s. Expected one of: %s", + effect, + self._effect_list, + ) + self._effect = None + return + + self._effect = effect + @callback def _update_state(self, result): """Update the state from the template.""" @@ -479,3 +670,42 @@ class LightTemplate(TemplateEntity, LightEntity): else: _LOGGER.error("Received invalid hs_color : (%s)", render) self._color = None + + @callback + def _update_max_mireds(self, render): + """Update the max mireds from the template.""" + + try: + if render in ("None", ""): + self._max_mireds = None + return + self._max_mireds = int(render) + except ValueError: + _LOGGER.error( + "Template must supply an integer temperature within the range for this light, or 'None'", + exc_info=True, + ) + self._max_mireds = None + + @callback + def _update_min_mireds(self, render): + """Update the min mireds from the template.""" + try: + if render in ("None", ""): + self._min_mireds = None + return + self._min_mireds = int(render) + except ValueError: + _LOGGER.error( + "Template must supply an integer temperature within the range for this light, or 'None'", + exc_info=True, + ) + self._min_mireds = None + + @callback + def _update_supports_transition(self, render): + """Update the supports transition from the template.""" + if render in ("None", ""): + self._supports_transition = False + return + self._supports_transition = bool(render) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index b3a4b2a1aa4..ec0347b8470 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -8,8 +8,11 @@ import homeassistant.components.light as light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, + SUPPORT_TRANSITION, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -378,6 +381,64 @@ async def test_on_action(hass, calls): assert len(calls) == 1 +async def test_on_action_with_transition(hass, calls): + """Test on action with transition.""" + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{states.light.test_state.state}}", + "turn_on": { + "service": "test.automation", + "data_template": { + "transition": "{{transition}}", + }, + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "supports_transition_template": "{{true}}", + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + "transition": "{{transition}}", + }, + }, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("light.test_state", STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_OFF + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_TRANSITION: 5}, + blocking=True, + ) + + assert len(calls) == 1 + print(calls[0].data) + assert calls[0].data["transition"] == 5 + + async def test_on_action_optimistic(hass, calls): """Test on action with optimistic state.""" assert await setup.async_setup_component( @@ -443,7 +504,9 @@ async def test_off_action(hass, calls): "service": "light.turn_on", "entity_id": "light.test_state", }, - "turn_off": {"service": "test.automation"}, + "turn_off": { + "service": "test.automation", + }, "set_level": { "service": "light.turn_on", "data_template": { @@ -477,6 +540,63 @@ async def test_off_action(hass, calls): assert len(calls) == 1 +async def test_off_action_with_transition(hass, calls): + """Test off action with transition.""" + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{states.light.test_state.state}}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "test.automation", + "data_template": { + "transition": "{{transition}}", + }, + }, + "supports_transition_template": "{{true}}", + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + "transition": "{{transition}}", + }, + }, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("light.test_state", STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_ON + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_TRANSITION: 2}, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[0].data["transition"] == 2 + + async def test_off_action_optimistic(hass, calls): """Test off action with optimistic state.""" assert await setup.async_setup_component( @@ -1119,6 +1239,417 @@ async def test_color_template(hass, expected_hs, template): assert state.attributes.get("hs_color") == expected_hs +async def test_effect_action_valid_effect(hass, calls): + """Test setting valid effect with template.""" + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{true}}", + "turn_on": {"service": "test.automation"}, + "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}}", + }, + }, + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": "{{ ['Disco', 'Police'] }}", + "effect_template": "{{ 'Disco' }}", + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_EFFECT: "Disco"}, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[0].data["effect"] == "Disco" + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("effect") == "Disco" + + +async def test_effect_action_invalid_effect(hass, calls): + """Test setting invalid effect with template.""" + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{true}}", + "turn_on": {"service": "test.automation"}, + "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}}", + }, + }, + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": "{{ ['Disco', 'Police'] }}", + "effect_template": "{{ None }}", + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_EFFECT: "RGB"}, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[0].data["effect"] == "RGB" + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("effect") is None + + +@pytest.mark.parametrize( + "expected_effect_list,template", + [ + ( + ["Strobe color", "Police", "Christmas", "RGB", "Random Loop"], + "{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}", + ), + ( + ["Police", "RGB", "Random Loop"], + "{{ ['Police', 'RGB', 'Random Loop'] }}", + ), + (None, "{{ [] }}"), + (None, "{{ '[]' }}"), + (None, "{{ 124 }}"), + (None, "{{ '124' }}"), + (None, "{{ none }}"), + (None, ""), + ], +) +async def test_effect_list_template(hass, expected_effect_list, template): + """Test the template for the effect list.""" + with assert_setup_component(1, light.DOMAIN): + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": template, + "effect_template": "{{ None }}", + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("effect_list") == expected_effect_list + + +@pytest.mark.parametrize( + "expected_effect,template", + [ + (None, "Disco"), + (None, "None"), + (None, "{{ None }}"), + ("Police", "Police"), + ("Strobe color", "{{ 'Strobe color' }}"), + ], +) +async def test_effect_template(hass, expected_effect, template): + """Test the template for the effect.""" + with assert_setup_component(1, light.DOMAIN): + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": "{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}", + "effect_template": template, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("effect") == expected_effect + + +@pytest.mark.parametrize( + "expected_min_mireds,template", + [ + (118, "{{118}}"), + (153, "{{x - 12}}"), + (153, "None"), + (153, "{{ none }}"), + (153, ""), + (153, "{{ 'a' }}"), + ], +) +async def test_min_mireds_template(hass, expected_min_mireds, template): + """Test the template for the min mireds.""" + with assert_setup_component(1, light.DOMAIN): + assert await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "temperature_template": "{{200}}", + "min_mireds_template": template, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("min_mireds") == expected_min_mireds + + +@pytest.mark.parametrize( + "expected_max_mireds,template", + [ + (488, "{{488}}"), + (500, "{{x - 12}}"), + (500, "None"), + (500, "{{ none }}"), + (500, ""), + (500, "{{ 'a' }}"), + ], +) +async def test_max_mireds_template(hass, expected_max_mireds, template): + """Test the template for the max mireds.""" + with assert_setup_component(1, light.DOMAIN): + assert await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "temperature_template": "{{200}}", + "max_mireds_template": template, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("max_mireds") == expected_max_mireds + + +@pytest.mark.parametrize( + "expected_supports_transition,template", + [ + (True, "{{true}}"), + (True, "{{1 == 1}}"), + (False, "{{false}}"), + (False, "{{ none }}"), + (False, ""), + (False, "None"), + ], +) +async def test_supports_transition_template( + hass, expected_supports_transition, template +): + """Test the template for the supports transition.""" + with assert_setup_component(1, light.DOMAIN): + assert await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "supports_transition_template": template, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + + expected_value = 1 + + if expected_supports_transition is True: + expected_value = 0 + + assert state is not None + assert ( + int(state.attributes.get("supported_features")) & SUPPORT_TRANSITION + ) != expected_value + + async def test_available_template_with_entities(hass): """Test availability templates with values from other entities.""" await setup.async_setup_component( From 60e65a4bc2260544a70385baf1dad58e5dbe65dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 May 2021 06:50:40 -0500 Subject: [PATCH 691/852] Bump async-upnp-client to 0.18.0 (#51017) - Adds support for a long running SSDP listener --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 997c0585c6d..434ff0e9c39 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.17.0"], + "requirements": ["async-upnp-client==0.18.0"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 348f8f3a46c..d3dbc0c920e 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "defusedxml==0.7.1", "netdisco==2.8.3", - "async-upnp-client==0.17.0" + "async-upnp-client==0.18.0" ], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index eee840381e1..b130e721e35 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.17.0"], + "requirements": ["async-upnp-client==0.18.0"], "codeowners": ["@StevenLooman"], "ssdp": [ { diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 078582387dc..0202efda332 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.2 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.17.0 +async-upnp-client==0.18.0 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 63286d2e081..3f3f68c9142 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -295,7 +295,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.17.0 +async-upnp-client==0.18.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91618f263fd..aacef3ba5ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -187,7 +187,7 @@ arcam-fmj==0.5.3 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.17.0 +async-upnp-client==0.18.0 # homeassistant.components.aurora auroranoaa==0.0.2 From 1546dbbf25f5124f47438ee37ea2ffdd0928906b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 24 May 2021 14:03:44 +0200 Subject: [PATCH 692/852] Add restore temperature to modbus climate (#50963) * Add restore temperature to climate. * please mypy. * Review 2. --- homeassistant/components/modbus/climate.py | 6 ++- ...test_modbus_climate.py => test_climate.py} | 42 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) rename tests/components/modbus/{test_modbus_climate.py => test_climate.py} (69%) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index d23146bd2ba..4e6a20b1700 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -20,6 +20,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .base_platform import BasePlatform @@ -98,7 +99,7 @@ async def async_setup_platform( async_add_entities(entities) -class ModbusThermostat(BasePlatform, ClimateEntity): +class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): """Representation of a Modbus Thermostat.""" def __init__( @@ -131,6 +132,9 @@ class ModbusThermostat(BasePlatform, ClimateEntity): async def async_added_to_hass(self): """Handle entity which will be added.""" await self.async_base_added_to_hass() + state = await self.async_get_last_state() + if state and state.attributes.get(ATTR_TEMPERATURE): + self._target_temperature = float(state.attributes[ATTR_TEMPERATURE]) @property def supported_features(self): diff --git a/tests/components/modbus/test_modbus_climate.py b/tests/components/modbus/test_climate.py similarity index 69% rename from tests/components/modbus/test_modbus_climate.py rename to tests/components/modbus/test_climate.py index 1e14b255ba5..c73a73e47e8 100644 --- a/tests/components/modbus/test_modbus_climate.py +++ b/tests/components/modbus/test_climate.py @@ -2,16 +2,25 @@ import pytest from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.climate.const import HVAC_MODE_AUTO from homeassistant.components.modbus.const import ( CONF_CLIMATES, CONF_CURRENT_TEMP, CONF_DATA_COUNT, CONF_TARGET_TEMP, ) -from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_SLAVE, +) +from homeassistant.core import State from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from tests.common import mock_restore_cache + @pytest.mark.parametrize( "do_options", @@ -101,3 +110,34 @@ async def test_service_climate_update(hass, mock_pymodbus): "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == "auto" + + +async def test_restore_state_climate(hass): + """Run test for sensor restore state.""" + + climate_name = "test_climate" + test_temp = 37 + entity_id = f"{CLIMATE_DOMAIN}.{climate_name}" + test_value = State(entity_id, 35) + test_value.attributes = {ATTR_TEMPERATURE: test_temp} + config_sensor = { + CONF_NAME: climate_name, + CONF_TARGET_TEMP: 117, + CONF_CURRENT_TEMP: 117, + } + mock_restore_cache( + hass, + (test_value,), + ) + await base_config_test( + hass, + config_sensor, + climate_name, + CLIMATE_DOMAIN, + CONF_CLIMATES, + None, + method_discovery=True, + ) + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_AUTO + assert state.attributes[ATTR_TEMPERATURE] == test_temp From c74e65ac2d8c16c7b29f47f35337afea29945f33 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 24 May 2021 14:53:54 +0200 Subject: [PATCH 693/852] Streamline modbus test_init (#50990) * Streamline test_init. * Review comments. * Remove hub name. --- homeassistant/components/modbus/fan.py | 3 +- homeassistant/components/modbus/light.py | 3 +- homeassistant/components/modbus/modbus.py | 5 - homeassistant/components/modbus/switch.py | 3 +- tests/components/modbus/test_init.py | 342 ++++++++-------------- 5 files changed, 120 insertions(+), 236 deletions(-) diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index 0f75748472b..9e23e0291f1 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -158,8 +158,7 @@ class ModbusFan(BasePlatform, FanEntity, RestoreEntity): self._is_on = False elif value is not None: _LOGGER.error( - "Unexpected response from hub %s, slave %s register %s, got 0x%2x", - self._hub.name, + "Unexpected response from modbus device slave %s register %s, got 0x%2x", self._slave, self._verify_address, value, diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index d253ea4df7e..e1dfba40176 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -149,8 +149,7 @@ class ModbusLight(BasePlatform, LightEntity, RestoreEntity): self._is_on = False elif value is not None: _LOGGER.error( - "Unexpected response from hub %s, slave %s register %s, got 0x%2x", - self._hub.name, + "Unexpected response from modbus device slave %s register %s, got 0x%2x", self._slave, self._verify_address, value, diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 943e4a81d54..4a02f019238 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -195,11 +195,6 @@ class ModbusHub: }, } - @property - def name(self): - """Return the name of this hub.""" - return self._config_name - def _log_error(self, exception_error: ModbusException, error_state=True): log_text = "Pymodbus: " + str(exception_error) if self._in_error: diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 502a6fc73b9..a9a4994a90b 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -147,8 +147,7 @@ class ModbusSwitch(BasePlatform, SwitchEntity, RestoreEntity): self._is_on = False elif value is not None: _LOGGER.error( - "Unexpected response from hub %s, slave %s register %s, got 0x%2x", - self._hub.name, + "Unexpected response from modbus device slave %s register %s, got 0x%2x", self._slave, self._verify_address, value, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 8959fe82319..0819e5a3e89 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -1,4 +1,14 @@ -"""The tests for the Modbus init.""" +"""The tests for the Modbus init. + +This file is responsible for testing: +- pymodbus API +- Functionality of class ModbusHub +- Coverage 100%: + __init__.py + base_platform.py + const.py + modbus.py +""" from datetime import timedelta import logging from unittest import mock @@ -20,6 +30,10 @@ from homeassistant.components.modbus.const import ( CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_WRITE_COIL, + CALL_TYPE_WRITE_COILS, + CALL_TYPE_WRITE_REGISTER, + CALL_TYPE_WRITE_REGISTERS, CONF_BAUDRATE, CONF_BYTESIZE, CONF_INPUT_TYPE, @@ -54,11 +68,14 @@ from .conftest import TEST_MODBUS_NAME, ReadResult from tests.common import async_fire_time_changed TEST_SENSOR_NAME = "testSensor" +TEST_ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" +TEST_HOST = "modbusTestHost" -@pytest.mark.parametrize( - "value,value_type", - [ +async def test_number_validator(): + """Test number validator.""" + + for value, value_type in [ (15, int), (15.1, float), ("15", int), @@ -67,48 +84,27 @@ TEST_SENSOR_NAME = "testSensor" (-15.1, float), ("-15", int), ("-15.1", float), - ], -) -async def test_number_validator(value, value_type): - """Test number validator.""" - - assert isinstance(number(value), value_type) - - -async def test_number_exception(): - """Test number exception.""" + ]: + assert isinstance(number(value), value_type) try: number("x15.1") except (vol.Invalid): return - pytest.fail("Number not throwing exception") -async def _config_helper(hass, do_config, caplog): - """Run test for modbus.""" - - config = {DOMAIN: do_config} - - caplog.set_level(logging.ERROR) - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() - assert DOMAIN in hass.config.components - assert len(caplog.records) == 0 - - @pytest.mark.parametrize( "do_config", [ { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, }, { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, @@ -116,12 +112,12 @@ async def _config_helper(hass, do_config, caplog): }, { CONF_TYPE: "udp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, }, { CONF_TYPE: "udp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, @@ -129,12 +125,12 @@ async def _config_helper(hass, do_config, caplog): }, { CONF_TYPE: "rtuovertcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, }, { CONF_TYPE: "rtuovertcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, @@ -163,176 +159,116 @@ async def _config_helper(hass, do_config, caplog): }, { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, CONF_DELAY: 5, }, + [ + { + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + CONF_NAME: TEST_MODBUS_NAME, + }, + { + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + CONF_NAME: TEST_MODBUS_NAME + "2", + }, + { + CONF_TYPE: "serial", + CONF_BAUDRATE: 9600, + CONF_BYTESIZE: 8, + CONF_METHOD: "rtu", + CONF_PORT: "usb01", + CONF_PARITY: "E", + CONF_STOPBITS: 1, + CONF_NAME: TEST_MODBUS_NAME + "3", + }, + ], ], ) async def test_config_modbus(hass, caplog, do_config, mock_pymodbus): - """Run test for modbus.""" - await _config_helper(hass, do_config, caplog) + """Run configuration test for modbus.""" + config = {DOMAIN: do_config} + caplog.set_level(logging.ERROR) + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + assert DOMAIN in hass.config.components + assert len(caplog.records) == 0 -async def test_config_multiple_modbus(hass, caplog, mock_pymodbus): - """Run test for multiple modbus.""" - do_config = [ +VALUE = "value" +FUNC = "func" +DATA = "data" +SERVICE = "service" + + +@pytest.mark.parametrize( + "do_write", + [ { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, - CONF_NAME: TEST_MODBUS_NAME, + DATA: ATTR_VALUE, + VALUE: 15, + SERVICE: SERVICE_WRITE_REGISTER, + FUNC: CALL_TYPE_WRITE_REGISTER, }, { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, - CONF_NAME: TEST_MODBUS_NAME + "2", + DATA: ATTR_VALUE, + VALUE: [1, 2, 3], + SERVICE: SERVICE_WRITE_REGISTER, + FUNC: CALL_TYPE_WRITE_REGISTERS, }, { - CONF_TYPE: "serial", - CONF_BAUDRATE: 9600, - CONF_BYTESIZE: 8, - CONF_METHOD: "rtu", - CONF_PORT: "usb01", - CONF_PARITY: "E", - CONF_STOPBITS: 1, - CONF_NAME: TEST_MODBUS_NAME + "3", + DATA: ATTR_STATE, + VALUE: False, + SERVICE: SERVICE_WRITE_COIL, + FUNC: CALL_TYPE_WRITE_COIL, }, - ] - - await _config_helper(hass, do_config, caplog) - - -async def test_pb_service_write_register(hass, caplog, mock_modbus): + { + DATA: ATTR_STATE, + VALUE: [True, False, True], + SERVICE: SERVICE_WRITE_COIL, + FUNC: CALL_TYPE_WRITE_COILS, + }, + ], +) +async def test_pb_service_write(hass, do_write, caplog, mock_modbus): """Run test for service write_register.""" - # Pymodbus write single, response OK. - data = {ATTR_HUB: TEST_MODBUS_NAME, ATTR_UNIT: 17, ATTR_ADDRESS: 16, ATTR_VALUE: 15} - await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) - assert mock_modbus.write_register.called - assert mock_modbus.write_register.call_args[0] == ( - data[ATTR_ADDRESS], - data[ATTR_VALUE], - ) - mock_modbus.reset_mock() + func_name = { + CALL_TYPE_WRITE_COIL: mock_modbus.write_coil, + CALL_TYPE_WRITE_COILS: mock_modbus.write_coils, + CALL_TYPE_WRITE_REGISTER: mock_modbus.write_register, + CALL_TYPE_WRITE_REGISTERS: mock_modbus.write_registers, + } - # Pymodbus write single, response error or exception - caplog.set_level(logging.DEBUG) - mock_modbus.write_register.return_value = ExceptionResponse(0x06) - await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) - assert mock_modbus.write_register.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - mock_modbus.write_register.return_value = IllegalFunctionRequest(0x06) - await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) - assert mock_modbus.write_register.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - mock_modbus.write_register.side_effect = ModbusException("fail write_") - await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) - assert mock_modbus.write_register.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - # Pymodbus write multiple, response OK. - data[ATTR_VALUE] = [1, 2, 3] - await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) - assert mock_modbus.write_registers.called - assert mock_modbus.write_registers.call_args[0] == ( - data[ATTR_ADDRESS], - data[ATTR_VALUE], - ) - mock_modbus.reset_mock() - - # Pymodbus write multiple, response error or exception - mock_modbus.write_registers.return_value = ExceptionResponse(0x06) - await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) - assert mock_modbus.write_registers.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - mock_modbus.write_registers.return_value = IllegalFunctionRequest(0x06) - await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) - assert mock_modbus.write_registers.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - mock_modbus.write_registers.side_effect = ModbusException("fail write_") - await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True) - assert mock_modbus.write_registers.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - -async def test_pb_service_write_coil(hass, caplog, mock_modbus): - """Run test for service write_coil.""" - - # Pymodbus write single, response OK. data = { ATTR_HUB: TEST_MODBUS_NAME, ATTR_UNIT: 17, ATTR_ADDRESS: 16, - ATTR_STATE: False, + do_write[DATA]: do_write[VALUE], } - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert mock_modbus.write_coil.called - assert mock_modbus.write_coil.call_args[0] == ( + await hass.services.async_call(DOMAIN, do_write[SERVICE], data, blocking=True) + assert func_name[do_write[FUNC]].called + assert func_name[do_write[FUNC]].call_args[0] == ( data[ATTR_ADDRESS], - data[ATTR_STATE], + data[do_write[DATA]], ) mock_modbus.reset_mock() - # Pymodbus write single, response error or exception - caplog.set_level(logging.DEBUG) - mock_modbus.write_coil.return_value = ExceptionResponse(0x06) - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert mock_modbus.write_coil.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - mock_modbus.write_coil.return_value = IllegalFunctionRequest(0x06) - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert mock_modbus.write_coil.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - mock_modbus.write_coil.side_effect = ModbusException("fail write_") - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert mock_modbus.write_coil.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - # Pymodbus write multiple, response OK. - data[ATTR_STATE] = [True, False, True] - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert mock_modbus.write_coils.called - assert mock_modbus.write_coils.call_args[0] == ( - data[ATTR_ADDRESS], - data[ATTR_STATE], - ) - mock_modbus.reset_mock() - - # Pymodbus write multiple, response error or exception - mock_modbus.write_coils.return_value = ExceptionResponse(0x06) - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert mock_modbus.write_coils.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - mock_modbus.write_coils.return_value = IllegalFunctionRequest(0x06) - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert mock_modbus.write_coils.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() - - mock_modbus.write_coils.side_effect = ModbusException("fail write_") - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert mock_modbus.write_coils.called - assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() + for return_value in [ + ExceptionResponse(0x06), + IllegalFunctionRequest(0x06), + ModbusException("fail write_"), + ]: + caplog.set_level(logging.DEBUG) + func_name[do_write[FUNC]].return_value = return_value + await hass.services.async_call(DOMAIN, do_write[SERVICE], data, blocking=True) + assert func_name[do_write[FUNC]].called + assert caplog.messages[-1].startswith("Pymodbus:") + mock_modbus.reset_mock() async def _read_helper(hass, do_group, do_type, do_return, do_exception, mock_pymodbus): @@ -340,7 +276,7 @@ async def _read_helper(hass, do_group, do_type, do_return, do_exception, mock_py DOMAIN: [ { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, CONF_NAME: TEST_MODBUS_NAME, do_group: [ @@ -442,7 +378,7 @@ async def test_pymodbus_constructor_fail(hass, caplog): DOMAIN: [ { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, } ] @@ -465,7 +401,7 @@ async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus): DOMAIN: [ { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, } ] @@ -491,7 +427,7 @@ async def test_delay(hass, mock_pymodbus): DOMAIN: [ { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, CONF_NAME: TEST_MODBUS_NAME, CONF_DELAY: test_delay, @@ -533,47 +469,3 @@ async def test_delay(hass, mock_pymodbus): async_fire_time_changed(hass, now) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON - - -async def test_thread_lock(hass, mock_pymodbus): - """Run test for block of threads.""" - - # the purpose of this test is to test the threads are not being blocked - # We "hijiack" a binary_sensor to make a proper blackbox test. - test_scan_interval = 5 - sensors = [] - for i in range(200): - sensors.append( - { - CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_NAME: f"{TEST_SENSOR_NAME}_{i}", - CONF_ADDRESS: 52 + i, - CONF_SCAN_INTERVAL: test_scan_interval, - } - ) - config = { - DOMAIN: [ - { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, - CONF_NAME: TEST_MODBUS_NAME, - CONF_BINARY_SENSORS: sensors, - } - ] - } - mock_pymodbus.read_coils.return_value = ReadResult([0x01]) - now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() - stop_time = now + timedelta(seconds=10) - step_timedelta = timedelta(seconds=1) - while now < stop_time: - now = now + step_timedelta - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - for i in range(200): - entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_{i}" - assert hass.states.get(entity_id).state == STATE_ON From 2ae91bf0ea810ea2af5390a5dfe00fb5bc46eec2 Mon Sep 17 00:00:00 2001 From: Nash Kaminski Date: Mon, 24 May 2021 09:41:37 -0500 Subject: [PATCH 694/852] Correct humidifier detection in venstar component and add tests (#50439) As of version 0.14, the venstar_colortouch lib always initializes hum_setpoint to None. When a thermostat actually reports a humidifier state, this value is replaced with the integer value of the setpoint. This changeset corrects the humidifier detection as well as adds basic test cases for the Venstar component. --- homeassistant/components/venstar/climate.py | 2 +- .../components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/venstar/__init__.py | 1 + tests/components/venstar/test_climate.py | 79 +++++++++++++++++++ tests/components/venstar/util.py | 57 +++++++++++++ tests/fixtures/venstar/colortouch_info.json | 1 + tests/fixtures/venstar/colortouch_root.json | 1 + .../fixtures/venstar/colortouch_sensors.json | 1 + tests/fixtures/venstar/t2k_info.json | 1 + tests/fixtures/venstar/t2k_root.json | 1 + tests/fixtures/venstar/t2k_sensors.json | 1 + 13 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 tests/components/venstar/__init__.py create mode 100644 tests/components/venstar/test_climate.py create mode 100644 tests/components/venstar/util.py create mode 100644 tests/fixtures/venstar/colortouch_info.json create mode 100644 tests/fixtures/venstar/colortouch_root.json create mode 100644 tests/fixtures/venstar/colortouch_sensors.json create mode 100644 tests/fixtures/venstar/t2k_info.json create mode 100644 tests/fixtures/venstar/t2k_root.json create mode 100644 tests/fixtures/venstar/t2k_sensors.json diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index b4d8264a3ab..72e9ecc3de4 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -124,7 +124,7 @@ class VenstarThermostat(ClimateEntity): if self._client.mode == self._client.MODE_AUTO: features |= SUPPORT_TARGET_TEMPERATURE_RANGE - if self._humidifier and hasattr(self._client, "hum_active"): + if self._humidifier and self._client.hum_setpoint is not None: features |= SUPPORT_TARGET_HUMIDITY return features diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 0baa1e56cfa..444a3fabf9a 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -2,7 +2,7 @@ "domain": "venstar", "name": "Venstar", "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.13"], + "requirements": ["venstarcolortouch==0.14"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 3f3f68c9142..6dae9b586e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2308,7 +2308,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.4.0 # homeassistant.components.venstar -venstarcolortouch==0.13 +venstarcolortouch==0.14 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aacef3ba5ef..4e296b34971 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1243,6 +1243,9 @@ url-normalize==1.4.1 # homeassistant.components.uvc uvcclient==0.11.0 +# homeassistant.components.venstar +venstarcolortouch==0.14 + # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/tests/components/venstar/__init__.py b/tests/components/venstar/__init__.py new file mode 100644 index 00000000000..908755a585f --- /dev/null +++ b/tests/components/venstar/__init__.py @@ -0,0 +1 @@ +"""Tests for the venstar integration.""" diff --git a/tests/components/venstar/test_climate.py b/tests/components/venstar/test_climate.py new file mode 100644 index 00000000000..9461032060b --- /dev/null +++ b/tests/components/venstar/test_climate.py @@ -0,0 +1,79 @@ +"""The climate tests for the venstar integration.""" + +from homeassistant.components.climate.const import ( + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, +) + +from .util import async_init_integration, mock_venstar_devices + +EXPECTED_BASE_SUPPORTED_FEATURES = ( + SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE +) + + +@mock_venstar_devices +async def test_colortouch(hass): + """Test interfacing with a venstar colortouch with attached humidifier.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.colortouch") + assert state.state == "heat" + + expected_attributes = { + "hvac_modes": ["heat", "cool", "off", "auto"], + "min_temp": 7, + "max_temp": 35, + "min_humidity": 0, + "max_humidity": 60, + "fan_modes": ["on", "auto"], + "preset_modes": ["none", "away", "temperature"], + "current_temperature": 21.0, + "temperature": 20.5, + "current_humidity": 41, + "humidity": 30, + "fan_mode": "auto", + "hvac_action": "idle", + "preset_mode": "temperature", + "fan_state": 0, + "hvac_mode": 0, + "friendly_name": "COLORTOUCH", + "supported_features": EXPECTED_BASE_SUPPORTED_FEATURES + | SUPPORT_TARGET_HUMIDITY, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + +@mock_venstar_devices +async def test_t2000(hass): + """Test interfacing with a venstar T2000 presently turned off.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.t2000") + assert state.state == "off" + + expected_attributes = { + "hvac_modes": ["heat", "cool", "off", "auto"], + "min_temp": 7, + "max_temp": 35, + "fan_modes": ["on", "auto"], + "preset_modes": ["none", "away", "temperature"], + "current_temperature": 14.0, + "temperature": None, + "fan_mode": "auto", + "hvac_action": "idle", + "preset_mode": "temperature", + "fan_state": 0, + "hvac_mode": 0, + "friendly_name": "T2000", + "supported_features": EXPECTED_BASE_SUPPORTED_FEATURES, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/venstar/util.py b/tests/components/venstar/util.py new file mode 100644 index 00000000000..b86f8475798 --- /dev/null +++ b/tests/components/venstar/util.py @@ -0,0 +1,57 @@ +"""Tests for the venstar integration.""" + +import requests_mock + +from homeassistant.components.climate.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PLATFORM +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import load_fixture + +TEST_MODELS = ["t2k", "colortouch"] + + +def mock_venstar_devices(f): + """Decorate function to mock a Venstar Colortouch and T2000 thermostat API.""" + + async def wrapper(hass): + # Mock thermostats are: + # Venstar T2000, FW 4.38 + # Venstar "colortouch" T7850, FW 5.1 + with requests_mock.mock() as m: + for model in TEST_MODELS: + m.get( + f"http://venstar-{model}.localdomain/", + text=load_fixture(f"venstar/{model}_root.json"), + ) + m.get( + f"http://venstar-{model}.localdomain/query/info", + text=load_fixture(f"venstar/{model}_info.json"), + ) + m.get( + f"http://venstar-{model}.localdomain/query/sensors", + text=load_fixture(f"venstar/{model}_sensors.json"), + ) + return await f(hass) + + return wrapper + + +async def async_init_integration( + hass: HomeAssistant, + skip_setup: bool = False, +): + """Set up the venstar integration in Home Assistant.""" + platform_config = [] + for model in TEST_MODELS: + platform_config.append( + { + CONF_PLATFORM: "venstar", + CONF_HOST: f"venstar-{model}.localdomain", + } + ) + config = {DOMAIN: platform_config} + + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() diff --git a/tests/fixtures/venstar/colortouch_info.json b/tests/fixtures/venstar/colortouch_info.json new file mode 100644 index 00000000000..44812beb762 --- /dev/null +++ b/tests/fixtures/venstar/colortouch_info.json @@ -0,0 +1 @@ +{"name":"COLORTOUCH","mode":1,"state":0,"fan":0,"fanstate":0,"tempunits":0,"schedule":0,"schedulepart":255,"away":0,"spacetemp":71.0,"heattemp":69.0,"cooltemp":74.0,"cooltempmin":35.0,"cooltempmax":99.0,"heattempmin":35.00,"heattempmax":99.0,"activestage":0,"hum_active":0,"hum":41,"hum_setpoint":30,"dehum_setpoint":99,"setpointdelta":2.0,"availablemodes":0} diff --git a/tests/fixtures/venstar/colortouch_root.json b/tests/fixtures/venstar/colortouch_root.json new file mode 100644 index 00000000000..820f88210b6 --- /dev/null +++ b/tests/fixtures/venstar/colortouch_root.json @@ -0,0 +1 @@ +{"api_ver":7,"type":"residential","model":"COLORTOUCH","firmware":"5.1"} \ No newline at end of file diff --git a/tests/fixtures/venstar/colortouch_sensors.json b/tests/fixtures/venstar/colortouch_sensors.json new file mode 100644 index 00000000000..a1ba04753b8 --- /dev/null +++ b/tests/fixtures/venstar/colortouch_sensors.json @@ -0,0 +1 @@ +{"sensors":[{"name":"Thermostat","temp":70.0,"hum":41},{"name":"Space Temp","temp":70.0}]} diff --git a/tests/fixtures/venstar/t2k_info.json b/tests/fixtures/venstar/t2k_info.json new file mode 100644 index 00000000000..81398dad391 --- /dev/null +++ b/tests/fixtures/venstar/t2k_info.json @@ -0,0 +1 @@ +{"name":"T2000","mode":0,"state":0,"activestage":0,"fan":0,"fanstate":0,"tempunits":0,"schedule":0,"schedulepart":1,"away":0,"spacetemp":14.5,"heattemp":10.0,"cooltemp":29.5,"cooltempmin":2.0,"cooltempmax":37.0,"heattempmin":2.0,"heattempmax":37.0,"setpointdelta":2,"availablemodes":2} diff --git a/tests/fixtures/venstar/t2k_root.json b/tests/fixtures/venstar/t2k_root.json new file mode 100644 index 00000000000..5f7449181a6 --- /dev/null +++ b/tests/fixtures/venstar/t2k_root.json @@ -0,0 +1 @@ +{"api_ver": 7, "type": "residential", "model": "T2000", "firmware": "4.38"} \ No newline at end of file diff --git a/tests/fixtures/venstar/t2k_sensors.json b/tests/fixtures/venstar/t2k_sensors.json new file mode 100644 index 00000000000..120b5820088 --- /dev/null +++ b/tests/fixtures/venstar/t2k_sensors.json @@ -0,0 +1 @@ +{"sensors": [{"name":"Thermostat","temp":14},{"name":"Space Temp","temp":14}]} \ No newline at end of file From 987e8ed5ed67269c822e2f9a90c7e15f893e5db3 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 24 May 2021 16:54:57 +0200 Subject: [PATCH 695/852] Add consider_home option to Fritz device_tracker (#50741) Co-authored-by: J. Nick Koston --- homeassistant/components/fritz/__init__.py | 10 ++++- homeassistant/components/fritz/common.py | 32 ++++++++----- homeassistant/components/fritz/config_flow.py | 45 ++++++++++++++++++- .../components/fritz/device_tracker.py | 19 +++++--- homeassistant/components/fritz/strings.json | 9 ++++ .../components/fritz/translations/en.json | 9 ++++ tests/components/fritz/test_config_flow.py | 35 +++++++++++++++ 7 files changed, 140 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index bc762dadcb7..35e924c807c 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await fritz_tools.async_setup() - await fritz_tools.async_start() + await fritz_tools.async_start(entry.options) except FritzSecurityError as ex: raise ConfigEntryAuthFailed from ex except FritzConnectionException as ex: @@ -53,6 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_unload) ) + entry.async_on_unload(entry.add_update_listener(update_listener)) + # Load the other platforms like switch hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -79,3 +81,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_unload_services(hass) return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Update when config_entry options update.""" + if entry.options: + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 47c3ef88681..7fe7069de17 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -16,6 +16,7 @@ from fritzconnection.core.exceptions import ( from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus +from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -59,6 +60,7 @@ class FritzBoxTools: """Initialize FritzboxTools class.""" self._cancel_scan = None self._devices: dict[str, Any] = {} + self._options = None self._unique_id = None self.connection = None self.fritz_hosts = None @@ -95,10 +97,10 @@ class FritzBoxTools: self.sw_version = info.get("NewSoftwareVersion") self.mac = self.unique_id - async def async_start(self): + async def async_start(self, options): """Start FritzHosts connection.""" self.fritz_hosts = FritzHosts(fc=self.connection) - + self._options = options await self.hass.async_add_executor_job(self.scan_devices) self._cancel_scan = async_track_time_interval( @@ -141,6 +143,8 @@ class FritzBoxTools: """Scan for new devices and return a list of found device ids.""" _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) + consider_home = self._options[CONF_CONSIDER_HOME] + new_device = False for known_host in self._update_info(): if not known_host.get("mac"): @@ -154,10 +158,10 @@ class FritzBoxTools: dev_info = Device(dev_mac, dev_ip, dev_name) if dev_mac in self._devices: - self._devices[dev_mac].update(dev_info, dev_home) + self._devices[dev_mac].update(dev_info, dev_home, consider_home) else: device = FritzDevice(dev_mac) - device.update(dev_info, dev_home) + device.update(dev_info, dev_home, consider_home) self._devices[dev_mac] = device new_device = True @@ -204,19 +208,25 @@ class FritzDevice: self._last_activity = None self._connected = False - def update(self, dev_info, dev_home): + def update(self, dev_info, dev_home, consider_home): """Update device info.""" utc_point_in_time = dt_util.utcnow() + if not self._name: self._name = dev_info.name or self._mac.replace(":", "_") - self._connected = dev_home - if not self._connected: + if not dev_home and self._last_activity: + self._connected = ( + utc_point_in_time - self._last_activity + ).total_seconds() < consider_home + else: + self._connected = dev_home + + if self._connected: + self._ip_address = dev_info.ip_address + self._last_activity = utc_point_in_time + else: self._ip_address = None - return - - self._last_activity = utc_point_in_time - self._ip_address = dev_info.ip_address @property def is_connected(self): diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 103ddbef9d9..4001dcadc71 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -2,19 +2,25 @@ from __future__ import annotations import logging +from typing import Any from urllib.parse import urlparse from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError import voluptuous as vol +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN, ) -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .common import FritzBoxTools from .const import ( @@ -34,6 +40,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return FritzBoxToolsOptionsFlowHandler(config_entry) + def __init__(self): """Initialize FRITZ!Box Tools flow.""" self._host = None @@ -85,6 +97,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PORT: self.fritz_tools.port, CONF_USERNAME: self.fritz_tools.username, }, + options={ + CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(), + }, ) async def async_step_ssdp(self, discovery_info): @@ -244,3 +259,31 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PORT: import_config.get(CONF_PORT, DEFAULT_PORT), } ) + + +class FritzBoxToolsOptionsFlowHandler(OptionsFlow): + """Handle a option flow.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle options flow.""" + + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_CONSIDER_HOME, + default=self.config_entry.options.get( + CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() + ), + ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)), + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 743918fa33c..dbf6bc0df93 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -116,6 +116,7 @@ class FritzBoxTracker(ScannerEntity): self._router = router self._mac = device.mac_address self._name = device.hostname or DEFAULT_DEVICE_NAME + self._last_activity = device.last_activity self._active = False self._attrs: dict = {} @@ -186,16 +187,22 @@ class FritzBoxTracker(ScannerEntity): """Return if the entity should be enabled when first added to the entity registry.""" return False + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return the attributes.""" + attrs: dict[str, str] = {} + if self._last_activity is not None: + attrs["last_time_reachable"] = self._last_activity.isoformat( + timespec="seconds" + ) + return attrs + @callback def async_process_update(self) -> None: """Update device.""" - device = self._router.devices[self._mac] + device: FritzDevice = self._router.devices[self._mac] self._active = device.is_connected - - if device.last_activity: - self._attrs["last_time_reachable"] = device.last_activity.isoformat( - timespec="seconds" - ) + self._last_activity = device.last_activity @callback def async_on_demand_update(self): diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 407f08b6ddf..f1cdb719741 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -40,5 +40,14 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Seconds to consider a device at 'home'" + } + } + } } } diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index cd5e1776d76..0fa47bd8328 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -51,5 +51,14 @@ "title": "Setup FRITZ!Box Tools" } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Seconds to consider a device at 'home'" + } + } + } } } \ No newline at end of file diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index b86e9934b24..6e051ef1bdd 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -4,6 +4,10 @@ from unittest.mock import patch from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError import pytest +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) from homeassistant.components.fritz.const import ( DOMAIN, ERROR_AUTH_INVALID, @@ -83,6 +87,10 @@ async def test_user(hass: HomeAssistant, fc_class_mock): assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" + assert ( + result["options"][CONF_CONSIDER_HOME] + == DEFAULT_CONSIDER_HOME.total_seconds() + ) assert not result["result"].unique_id await hass.async_block_till_done() @@ -416,3 +424,30 @@ async def test_import(hass: HomeAssistant, fc_class_mock): await hass.async_block_till_done() assert mock_setup_entry.called + + +async def test_options_flow(hass: HomeAssistant, fc_class_mock): + """Test options flow.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.common.FritzBoxTools" + ): + result = await hass.config_entries.options.async_init(mock_config.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_init(mock_config.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CONSIDER_HOME: 37, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert mock_config.options[CONF_CONSIDER_HOME] == 37 From e964c607a3339f3b2e9c6b524954ae7feee3e3b6 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Mon, 24 May 2021 08:38:37 -0700 Subject: [PATCH 696/852] jinja2.contextfilter decorator renamed to pass_context (#51007) * jinja2.contextfilter decorator renamed to pass_context * bump jinja2 dependency --- homeassistant/helpers/template.py | 10 +++++----- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 86223d2a950..f65100a8775 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -22,7 +22,7 @@ from urllib.parse import urlencode as urllib_urlencode import weakref import jinja2 -from jinja2 import contextfilter, contextfunction +from jinja2 import contextfunction, pass_context from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace import voluptuous as vol @@ -1315,7 +1315,7 @@ def to_json(value): return json.dumps(value) -@contextfilter +@pass_context def random_every_time(context, values): """Choose a random value. @@ -1482,7 +1482,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return contextfunction(wrapper) self.globals["device_entities"] = hassfunction(device_entities) - self.filters["device_entities"] = contextfilter(self.globals["device_entities"]) + self.filters["device_entities"] = pass_context(self.globals["device_entities"]) if limited: # Only device_entities is available to limited templates, mark other @@ -1514,9 +1514,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return self.globals["expand"] = hassfunction(expand) - self.filters["expand"] = contextfilter(self.globals["expand"]) + self.filters["expand"] = pass_context(self.globals["expand"]) self.globals["closest"] = hassfunction(closest) - self.filters["closest"] = contextfilter(hassfunction(closest_filter)) + self.filters["closest"] = pass_context(hassfunction(closest_filter)) self.globals["distance"] = hassfunction(distance) self.globals["is_state"] = hassfunction(is_state) self.globals["is_state_attr"] = hassfunction(is_state_attr) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0202efda332..e3533632af8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ emoji==1.2.0 hass-nabucasa==0.43.0 home-assistant-frontend==20210518.0 httpx==0.18.0 -jinja2>=2.11.3 +jinja2>=3.0.1 netdisco==2.8.3 paho-mqtt==1.5.1 pillow==8.1.2 diff --git a/requirements.txt b/requirements.txt index 4661a23a70f..7d9b7739669 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 httpx==0.18.0 -jinja2>=2.11.3 +jinja2>=3.0.1 PyJWT==1.7.1 cryptography==3.3.2 pip>=8.0.3,<20.3 diff --git a/setup.py b/setup.py index d987f4671b4..0178b201372 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ REQUIRES = [ "certifi>=2020.12.5", "ciso8601==2.1.3", "httpx==0.18.0", - "jinja2>=2.11.3", + "jinja2>=3.0.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==3.3.2", From 6a7968593d8e024fc6c4c2f7535a6a18b0f2f34a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 May 2021 09:27:13 -0700 Subject: [PATCH 697/852] Make camera source check faster (#51035) --- homeassistant/components/camera/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d9ccb0490c6..4791a963048 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -696,10 +696,13 @@ async def async_handle_play_stream_service( # It is required to send a different payload for cast media players entity_ids = service_call.data[ATTR_MEDIA_PLAYER] + sources = entity_sources(hass) cast_entity_ids = [ entity - for entity, source in entity_sources(hass).items() - if entity in entity_ids and source["domain"] == "cast" + for entity in entity_ids + # All entities should be in sources. This extra guard is to + # avoid people writing to the state machine and breaking it. + if entity in sources and sources[entity]["domain"] == "cast" ] other_entity_ids = list(set(entity_ids) - set(cast_entity_ids)) From ebf6e3d985a2cb75333554cb411b23001258be08 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 24 May 2021 13:11:09 -0400 Subject: [PATCH 698/852] Add zwave_js WS API commands to replace and remove failed nodes (#51018) * Add zwave_js WS API commands to replace and remove failed nodes * no need to manually add node to driver in test --- homeassistant/components/zwave_js/api.py | 166 ++++++++++++- tests/components/zwave_js/test_api.py | 228 +++++++++++++++++- .../nortek_thermostat_added_event.json | 4 +- 3 files changed, 386 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 91fa4589074..32c23fe760b 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -51,6 +51,7 @@ TYPE = "type" PROPERTY = "property" PROPERTY_KEY = "property_key" VALUE = "value" +SECURE = "secure" # constants for log config commands CONFIG = "config" @@ -129,6 +130,8 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_add_node) websocket_api.async_register_command(hass, websocket_stop_inclusion) websocket_api.async_register_command(hass, websocket_remove_node) + websocket_api.async_register_command(hass, websocket_remove_failed_node) + websocket_api.async_register_command(hass, websocket_replace_failed_node) websocket_api.async_register_command(hass, websocket_stop_exclusion) websocket_api.async_register_command(hass, websocket_refresh_node_info) websocket_api.async_register_command(hass, websocket_refresh_node_values) @@ -211,7 +214,7 @@ async def websocket_node_status( { vol.Required(TYPE): "zwave_js/add_node", vol.Required(ENTRY_ID): str, - vol.Optional("secure", default=False): bool, + vol.Optional(SECURE, default=False): bool, } ) @websocket_api.async_response @@ -225,7 +228,7 @@ async def websocket_add_node( ) -> None: """Add a node to the Z-Wave network.""" controller = client.driver.controller - include_non_secure = not msg["secure"] + include_non_secure = not msg[SECURE] @callback def async_cleanup() -> None: @@ -409,6 +412,165 @@ async def websocket_remove_node( ) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/replace_failed_node", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + vol.Optional(SECURE, default=False): bool, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_replace_failed_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Replace a failed node with a new node.""" + controller = client.driver.controller + include_non_secure = not msg[SECURE] + node_id = msg[NODE_ID] + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_event(event: dict) -> None: + connection.send_message( + websocket_api.event_message(msg[ID], {"event": event["event"]}) + ) + + @callback + def forward_stage(event: dict) -> None: + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": event["event"], "stage": event["stageName"]} + ) + ) + + @callback + def node_added(event: dict) -> None: + node = event["node"] + interview_unsubs = [ + node.on("interview started", forward_event), + node.on("interview completed", forward_event), + node.on("interview stage completed", forward_stage), + node.on("interview failed", forward_event), + ] + unsubs.extend(interview_unsubs) + node_details = { + "node_id": node.node_id, + "status": node.status, + "ready": node.ready, + } + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "node added", "node": node_details} + ) + ) + + @callback + def node_removed(event: dict) -> None: + node = event["node"] + node_details = { + "node_id": node.node_id, + } + + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "node removed", "node": node_details} + ) + ) + + @callback + def device_registered(device: DeviceEntry) -> None: + device_details = { + "name": device.name, + "id": device.id, + "manufacturer": device.manufacturer, + "model": device.model, + } + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "device registered", "device": device_details} + ) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + unsubs = [ + controller.on("inclusion started", forward_event), + controller.on("inclusion failed", forward_event), + controller.on("inclusion stopped", forward_event), + controller.on("node removed", node_removed), + controller.on("node added", node_added), + async_dispatcher_connect( + hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device_registered + ), + ] + + result = await controller.async_replace_failed_node(node_id, include_non_secure) + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/remove_failed_node", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_remove_failed_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Remove a failed node from the Z-Wave network.""" + controller = client.driver.controller + node_id = msg[NODE_ID] + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + unsub() + + @callback + def node_removed(event: dict) -> None: + node = event["node"] + node_details = { + "node_id": node.node_id, + } + + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "node removed", "node": node_details} + ) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + unsub = controller.on("node removed", node_removed) + + result = await controller.async_remove_failed_node(node_id) + connection.send_result( + msg[ID], + result, + ) + + @websocket_api.require_admin @websocket_api.websocket_command( { diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index f192c69b80a..141998526ee 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -146,7 +146,7 @@ async def test_add_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "node added" node_details = { - "node_id": 53, + "node_id": 67, "status": 0, "ready": False, } @@ -160,7 +160,7 @@ async def test_add_node( # Test receiving interview events event = Event( type="interview started", - data={"source": "node", "event": "interview started", "nodeId": 53}, + data={"source": "node", "event": "interview started", "nodeId": 67}, ) client.driver.receive_event(event) @@ -173,7 +173,7 @@ async def test_add_node( "source": "node", "event": "interview stage completed", "stageName": "NodeInfo", - "nodeId": 53, + "nodeId": 67, }, ) client.driver.receive_event(event) @@ -184,7 +184,7 @@ async def test_add_node( event = Event( type="interview completed", - data={"source": "node", "event": "interview completed", "nodeId": 53}, + data={"source": "node", "event": "interview completed", "nodeId": 67}, ) client.driver.receive_event(event) @@ -193,7 +193,7 @@ async def test_add_node( event = Event( type="interview failed", - data={"source": "node", "event": "interview failed", "nodeId": 53}, + data={"source": "node", "event": "interview failed", "nodeId": 67}, ) client.driver.receive_event(event) @@ -289,9 +289,6 @@ async def test_remove_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "exclusion started" - # Add mock node to controller - client.driver.controller.nodes[67] = nortek_thermostat - dev_reg = dr.async_get(hass) # Create device registry entry for mock node @@ -325,6 +322,221 @@ async def test_remove_node( assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_replace_failed_node( + hass, + integration, + client, + hass_ws_client, + nortek_thermostat, + nortek_thermostat_added_event, + nortek_thermostat_removed_event, +): + """Test the replace_failed_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + dev_reg = dr.async_get(hass) + + # Create device registry entry for mock node + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "3245146787-67")}, + name="Node 67", + ) + + client.async_send_command.return_value = {"success": True} + + # Order of events we receive for a successful replacement is `inclusion started`, + # `inclusion stopped`, `node removed`, `node added`, then interview stages. + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/replace_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + event = Event( + type="inclusion started", + data={ + "source": "controller", + "event": "inclusion started", + "secure": False, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "inclusion started" + + event = Event( + type="inclusion stopped", + data={ + "source": "controller", + "event": "inclusion stopped", + "secure": False, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "inclusion stopped" + + # Fire node removed event + client.driver.receive_event(nortek_thermostat_removed_event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "node removed" + + # Verify device was removed from device registry + device = dev_reg.async_get_device( + identifiers={(DOMAIN, "3245146787-67")}, + ) + assert device is None + + client.driver.receive_event(nortek_thermostat_added_event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "node added" + node_details = { + "node_id": 67, + "status": 0, + "ready": False, + } + assert msg["event"]["node"] == node_details + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "device registered" + # Check the keys of the device item + assert list(msg["event"]["device"]) == ["name", "id", "manufacturer", "model"] + + # Test receiving interview events + event = Event( + type="interview started", + data={"source": "node", "event": "interview started", "nodeId": 67}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview started" + + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "NodeInfo", + "nodeId": 67, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview stage completed" + assert msg["event"]["stage"] == "NodeInfo" + + event = Event( + type="interview completed", + data={"source": "node", "event": "interview completed", "nodeId": 67}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview completed" + + event = Event( + type="interview failed", + data={"source": "node", "event": "interview failed", "nodeId": 67}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview failed" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/replace_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_remove_failed_node( + hass, + integration, + client, + hass_ws_client, + nortek_thermostat, + nortek_thermostat_removed_event, +): + """Test the remove_failed_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/remove_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + dev_reg = dr.async_get(hass) + + # Create device registry entry for mock node + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "3245146787-67")}, + name="Node 67", + ) + + # Fire node removed event + client.driver.receive_event(nortek_thermostat_removed_event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "node removed" + + # Verify device was removed from device registry + device = dev_reg.async_get_device( + identifiers={(DOMAIN, "3245146787-67")}, + ) + assert device is None + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/remove_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + async def test_refresh_node_info( hass, client, integration, hass_ws_client, multisensor_6 ): diff --git a/tests/fixtures/zwave_js/nortek_thermostat_added_event.json b/tests/fixtures/zwave_js/nortek_thermostat_added_event.json index 60078100caf..0f90d2ae147 100644 --- a/tests/fixtures/zwave_js/nortek_thermostat_added_event.json +++ b/tests/fixtures/zwave_js/nortek_thermostat_added_event.json @@ -2,7 +2,7 @@ "source": "controller", "event": "node added", "node": { - "nodeId": 53, + "nodeId": 67, "index": 0, "status": 0, "ready": false, @@ -17,7 +17,7 @@ "interviewAttempts": 1, "endpoints": [ { - "nodeId": 53, + "nodeId": 67, "index": 0 } ], From 1ec4332e25ac48e0ee1abcaadb5b6458f039b1cb Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 25 May 2021 01:23:16 +0800 Subject: [PATCH 699/852] Use ConfigType instead of Config in async_setup type hint (#51037) --- homeassistant/components/glances/__init__.py | 5 +++-- homeassistant/components/hassio/__init__.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index a2969c032f8..464b320dac0 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -16,12 +16,13 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import Config, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_VERSION, @@ -59,7 +60,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: Config) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Configure Glances using config flow only.""" if DOMAIN in config: for entry in config[DOMAIN]: diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 2c25868dfcd..e33c689c59e 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -22,10 +22,11 @@ from homeassistant.const import ( SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, ) -from homeassistant.core import DOMAIN as HASS_DOMAIN, Config, HomeAssistant, callback +from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, recorder from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow @@ -338,7 +339,7 @@ def get_supervisor_ip(): return os.environ["SUPERVISOR"].partition(":")[0] -async def async_setup(hass: HomeAssistant, config: Config) -> bool: # noqa: C901 +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901 """Set up the Hass.io component.""" # Check local setup for env in ("HASSIO", "HASSIO_TOKEN"): From 872167521856df8c6df03230521bfaae9bd3c015 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 24 May 2021 20:13:25 +0200 Subject: [PATCH 700/852] Use BaseSwitch class in modbus switch/fan/light (#51031) --- .../components/modbus/base_platform.py | 121 +++++++++++++++- homeassistant/components/modbus/fan.py | 134 +----------------- homeassistant/components/modbus/light.py | 132 +---------------- homeassistant/components/modbus/switch.py | 129 +---------------- 4 files changed, 136 insertions(+), 380 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index d6811bca1ff..ed2f6e69863 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -8,15 +8,29 @@ from typing import Any from homeassistant.const import ( CONF_ADDRESS, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_DELAY, CONF_DEVICE_CLASS, CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, + STATE_ON, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import async_call_later, async_track_time_interval +from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_INPUT_TYPE +from .const import ( + CALL_TYPE_COIL, + CALL_TYPE_WRITE_COIL, + CALL_TYPE_WRITE_REGISTER, + CONF_INPUT_TYPE, + CONF_STATE_OFF, + CONF_STATE_ON, + CONF_VERIFY, + CONF_WRITE_TYPE, +) from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -68,3 +82,106 @@ class BasePlatform(Entity): def available(self) -> bool: """Return True if entity is available.""" return self._available + + +class BaseSwitch(BasePlatform, RestoreEntity): + """Base class representing a Modbus switch.""" + + def __init__(self, hub: ModbusHub, config: dict) -> None: + """Initialize the switch.""" + config[CONF_INPUT_TYPE] = "" + super().__init__(hub, config) + self._is_on = None + if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: + self._write_type = CALL_TYPE_WRITE_COIL + else: + self._write_type = CALL_TYPE_WRITE_REGISTER + self.command_on = config[CONF_COMMAND_ON] + self._command_off = config[CONF_COMMAND_OFF] + if CONF_VERIFY in config: + if config[CONF_VERIFY] is None: + config[CONF_VERIFY] = {} + self._verify_active = True + self._verify_delay = config[CONF_VERIFY].get(CONF_DELAY, 0) + self._verify_address = config[CONF_VERIFY].get( + CONF_ADDRESS, config[CONF_ADDRESS] + ) + self._verify_type = config[CONF_VERIFY].get( + CONF_INPUT_TYPE, config[CONF_WRITE_TYPE] + ) + self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self.command_on) + self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) + else: + self._verify_active = False + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await self.async_base_added_to_hass() + state = await self.async_get_last_state() + if state: + self._is_on = state.state == STATE_ON + + @property + def is_on(self): + """Return true if switch is on.""" + return self._is_on + + async def async_turn(self, command): + """Evaluate switch result.""" + result = await self._hub.async_pymodbus_call( + self._slave, self._address, command, self._write_type + ) + if result is None: + self._available = False + self.async_write_ha_state() + return + + self._available = True + if not self._verify_active: + self._is_on = command == self.command_on + self.async_write_ha_state() + return + + if self._verify_delay: + async_call_later(self.hass, self._verify_delay, self.async_update) + else: + await self.async_update() + + async def async_turn_off(self, **kwargs): + """Set switch off.""" + await self.async_turn(self._command_off) + + async def async_update(self, now=None): + """Update the entity state.""" + # remark "now" is a dummy parameter to avoid problems with + # async_track_time_interval + if not self._verify_active: + self._available = True + self.async_write_ha_state() + return + + result = await self._hub.async_pymodbus_call( + self._slave, self._verify_address, 1, self._verify_type + ) + if result is None: + self._available = False + self.async_write_ha_state() + return + + self._available = True + if self._verify_type == CALL_TYPE_COIL: + self._is_on = bool(result.bits[0] & 1) + else: + value = int(result.registers[0]) + if value == self._state_on: + self._is_on = True + elif value == self._state_off: + self._is_on = False + elif value is not None: + _LOGGER.error( + "Unexpected response from modbus device slave %s register %s, got 0x%2x", + self._slave, + self._verify_address, + value, + ) + self.async_write_ha_state() diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index 9e23e0291f1..7fabff25711 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -4,30 +4,12 @@ from __future__ import annotations import logging from homeassistant.components.fan import FanEntity -from homeassistant.const import ( - CONF_ADDRESS, - CONF_COMMAND_OFF, - CONF_COMMAND_ON, - CONF_NAME, - STATE_ON, -) +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from .base_platform import BasePlatform -from .const import ( - CALL_TYPE_COIL, - CALL_TYPE_WRITE_COIL, - CALL_TYPE_WRITE_REGISTER, - CONF_FANS, - CONF_INPUT_TYPE, - CONF_STATE_OFF, - CONF_STATE_ON, - CONF_VERIFY, - CONF_WRITE_TYPE, - MODBUS_DOMAIN, -) +from .base_platform import BaseSwitch +from .const import CONF_FANS, MODBUS_DOMAIN from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -48,46 +30,8 @@ async def async_setup_platform( async_add_entities(fans) -class ModbusFan(BasePlatform, FanEntity, RestoreEntity): - """Base class representing a Modbus fan.""" - - def __init__(self, hub: ModbusHub, config: dict) -> None: - """Initialize the fan.""" - config[CONF_INPUT_TYPE] = "" - super().__init__(hub, config) - self._is_on: bool = False - if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: - self._write_type = CALL_TYPE_WRITE_COIL - else: - self._write_type = CALL_TYPE_WRITE_REGISTER - self._command_on = config[CONF_COMMAND_ON] - self._command_off = config[CONF_COMMAND_OFF] - if CONF_VERIFY in config: - if config[CONF_VERIFY] is None: - config[CONF_VERIFY] = {} - self._verify_active = True - self._verify_address = config[CONF_VERIFY].get( - CONF_ADDRESS, config[CONF_ADDRESS] - ) - self._verify_type = config[CONF_VERIFY].get( - CONF_INPUT_TYPE, config[CONF_WRITE_TYPE] - ) - self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self._command_on) - self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) - else: - self._verify_active = False - - async def async_added_to_hass(self): - """Handle entity which will be added.""" - await self.async_base_added_to_hass() - state = await self.async_get_last_state() - if state: - self._is_on = state.state == STATE_ON - - @property - def is_on(self): - """Return true if fan is on.""" - return self._is_on +class ModbusFan(BaseSwitch, FanEntity): + """Class representing a Modbus fan.""" async def async_turn_on( self, @@ -97,70 +41,4 @@ class ModbusFan(BasePlatform, FanEntity, RestoreEntity): **kwargs, ) -> None: """Set fan on.""" - - result = await self._hub.async_pymodbus_call( - self._slave, self._address, self._command_on, self._write_type - ) - if result is None: - self._available = False - self.async_write_ha_state() - return - - self._available = True - if self._verify_active: - await self.async_update() - return - - self._is_on = True - self.async_write_ha_state() - - async def async_turn_off(self, **kwargs): - """Set fan off.""" - result = await self._hub.async_pymodbus_call( - self._slave, self._address, self._command_off, self._write_type - ) - if result is None: - self._available = False - self.async_write_ha_state() - else: - self._available = True - if self._verify_active: - await self.async_update() - else: - self._is_on = False - self.async_write_ha_state() - - async def async_update(self, now=None): - """Update the entity state.""" - # remark "now" is a dummy parameter to avoid problems with - # async_track_time_interval - if not self._verify_active: - self._available = True - self.async_write_ha_state() - return - - result = await self._hub.async_pymodbus_call( - self._slave, self._verify_address, 1, self._verify_type - ) - if result is None: - self._available = False - self.async_write_ha_state() - return - - self._available = True - if self._verify_type == CALL_TYPE_COIL: - self._is_on = bool(result.bits[0] & 1) - else: - value = int(result.registers[0]) - if value == self._state_on: - self._is_on = True - elif value == self._state_off: - self._is_on = False - elif value is not None: - _LOGGER.error( - "Unexpected response from modbus device slave %s register %s, got 0x%2x", - self._slave, - self._verify_address, - value, - ) - self.async_write_ha_state() + await self.async_turn(self.command_on) diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index e1dfba40176..f56b01ff001 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -4,30 +4,12 @@ from __future__ import annotations import logging from homeassistant.components.light import LightEntity -from homeassistant.const import ( - CONF_ADDRESS, - CONF_COMMAND_OFF, - CONF_COMMAND_ON, - CONF_LIGHTS, - CONF_NAME, - STATE_ON, -) +from homeassistant.const import CONF_LIGHTS, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from .base_platform import BasePlatform -from .const import ( - CALL_TYPE_COIL, - CALL_TYPE_WRITE_COIL, - CALL_TYPE_WRITE_REGISTER, - CONF_INPUT_TYPE, - CONF_STATE_OFF, - CONF_STATE_ON, - CONF_VERIFY, - CONF_WRITE_TYPE, - MODBUS_DOMAIN, -) +from .base_platform import BaseSwitch +from .const import MODBUS_DOMAIN from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -47,111 +29,9 @@ async def async_setup_platform( async_add_entities(lights) -class ModbusLight(BasePlatform, LightEntity, RestoreEntity): - """Base class representing a Modbus light.""" - - def __init__(self, hub: ModbusHub, config: dict) -> None: - """Initialize the light.""" - config[CONF_INPUT_TYPE] = "" - super().__init__(hub, config) - self._is_on = None - if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: - self._write_type = CALL_TYPE_WRITE_COIL - else: - self._write_type = CALL_TYPE_WRITE_REGISTER - self._command_on = config[CONF_COMMAND_ON] - self._command_off = config[CONF_COMMAND_OFF] - if CONF_VERIFY in config: - if config[CONF_VERIFY] is None: - config[CONF_VERIFY] = {} - self._verify_active = True - self._verify_address = config[CONF_VERIFY].get( - CONF_ADDRESS, config[CONF_ADDRESS] - ) - self._verify_type = config[CONF_VERIFY].get( - CONF_INPUT_TYPE, config[CONF_WRITE_TYPE] - ) - self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self._command_on) - self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) - else: - self._verify_active = False - - async def async_added_to_hass(self): - """Handle entity which will be added.""" - await self.async_base_added_to_hass() - state = await self.async_get_last_state() - if state: - self._is_on = state.state == STATE_ON - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on +class ModbusLight(BaseSwitch, LightEntity): + """Class representing a Modbus light.""" async def async_turn_on(self, **kwargs): """Set light on.""" - - result = await self._hub.async_pymodbus_call( - self._slave, self._address, self._command_on, self._write_type - ) - if result is None: - self._available = False - self.async_write_ha_state() - else: - self._available = True - if self._verify_active: - await self.async_update() - else: - self._is_on = True - self.async_write_ha_state() - - async def async_turn_off(self, **kwargs): - """Set light off.""" - result = await self._hub.async_pymodbus_call( - self._slave, self._address, self._command_off, self._write_type - ) - if result is None: - self._available = False - self.async_write_ha_state() - else: - self._available = True - if self._verify_active: - await self.async_update() - else: - self._is_on = False - self.async_write_ha_state() - - async def async_update(self, now=None): - """Update the entity state.""" - # remark "now" is a dummy parameter to avoid problems with - # async_track_time_interval - if not self._verify_active: - self._available = True - self.async_write_ha_state() - return - - result = await self._hub.async_pymodbus_call( - self._slave, self._verify_address, 1, self._verify_type - ) - if result is None: - self._available = False - self.async_write_ha_state() - return - - self._available = True - if self._verify_type == CALL_TYPE_COIL: - self._is_on = bool(result.bits[0] & 1) - else: - value = int(result.registers[0]) - if value == self._state_on: - self._is_on = True - elif value == self._state_off: - self._is_on = False - elif value is not None: - _LOGGER.error( - "Unexpected response from modbus device slave %s register %s, got 0x%2x", - self._slave, - self._verify_address, - value, - ) - self.async_write_ha_state() + await self.async_turn(self.command_on) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index a9a4994a90b..98e15d5b311 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -4,32 +4,12 @@ from __future__ import annotations import logging from homeassistant.components.switch import SwitchEntity -from homeassistant.const import ( - CONF_ADDRESS, - CONF_COMMAND_OFF, - CONF_COMMAND_ON, - CONF_DELAY, - CONF_NAME, - CONF_SWITCHES, - STATE_ON, -) +from homeassistant.const import CONF_NAME, CONF_SWITCHES from homeassistant.core import HomeAssistant -from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from .base_platform import BasePlatform -from .const import ( - CALL_TYPE_COIL, - CALL_TYPE_WRITE_COIL, - CALL_TYPE_WRITE_REGISTER, - CONF_INPUT_TYPE, - CONF_STATE_OFF, - CONF_STATE_ON, - CONF_VERIFY, - CONF_WRITE_TYPE, - MODBUS_DOMAIN, -) +from .base_platform import BaseSwitch +from .const import MODBUS_DOMAIN from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -48,108 +28,9 @@ async def async_setup_platform( async_add_entities(switches) -class ModbusSwitch(BasePlatform, SwitchEntity, RestoreEntity): +class ModbusSwitch(BaseSwitch, SwitchEntity): """Base class representing a Modbus switch.""" - def __init__(self, hub: ModbusHub, config: dict) -> None: - """Initialize the switch.""" - config[CONF_INPUT_TYPE] = "" - super().__init__(hub, config) - self._is_on = None - if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: - self._write_type = CALL_TYPE_WRITE_COIL - else: - self._write_type = CALL_TYPE_WRITE_REGISTER - self._command_on = config[CONF_COMMAND_ON] - self._command_off = config[CONF_COMMAND_OFF] - if CONF_VERIFY in config: - if config[CONF_VERIFY] is None: - config[CONF_VERIFY] = {} - self._verify_active = True - self._verify_delay = config[CONF_VERIFY].get(CONF_DELAY, 0) - self._verify_address = config[CONF_VERIFY].get( - CONF_ADDRESS, config[CONF_ADDRESS] - ) - self._verify_type = config[CONF_VERIFY].get( - CONF_INPUT_TYPE, config[CONF_WRITE_TYPE] - ) - self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self._command_on) - self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) - else: - self._verify_active = False - - async def async_added_to_hass(self): - """Handle entity which will be added.""" - await self.async_base_added_to_hass() - state = await self.async_get_last_state() - if state: - self._is_on = state.state == STATE_ON - - @property - def is_on(self): - """Return true if switch is on.""" - return self._is_on - - async def _async_turn(self, command): - """Evaluate switch result.""" - result = await self._hub.async_pymodbus_call( - self._slave, self._address, command, self._write_type - ) - if result is None: - self._available = False - self.async_write_ha_state() - return - - self._available = True - if not self._verify_active: - self._is_on = command == self._command_on - self.async_write_ha_state() - return - - if self._verify_delay: - async_call_later(self.hass, self._verify_delay, self.async_update) - else: - await self.async_update() - async def async_turn_on(self, **kwargs): """Set switch on.""" - await self._async_turn(self._command_on) - - async def async_turn_off(self, **kwargs): - """Set switch off.""" - await self._async_turn(self._command_off) - - async def async_update(self, now=None): - """Update the entity state.""" - # remark "now" is a dummy parameter to avoid problems with - # async_track_time_interval - if not self._verify_active: - self._available = True - self.async_write_ha_state() - return - - result = await self._hub.async_pymodbus_call( - self._slave, self._verify_address, 1, self._verify_type - ) - if result is None: - self._available = False - self.async_write_ha_state() - return - - self._available = True - if self._verify_type == CALL_TYPE_COIL: - self._is_on = bool(result.bits[0] & 1) - else: - value = int(result.registers[0]) - if value == self._state_on: - self._is_on = True - elif value == self._state_off: - self._is_on = False - elif value is not None: - _LOGGER.error( - "Unexpected response from modbus device slave %s register %s, got 0x%2x", - self._slave, - self._verify_address, - value, - ) - self.async_write_ha_state() + await self.async_turn(self.command_on) From 394e044c6673d4d1c997764abca6dd6cac0eaa8d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 24 May 2021 20:15:41 +0200 Subject: [PATCH 701/852] Add state classes to Toon (#50978) --- homeassistant/components/toon/const.py | 67 ++++++++++++++++++++++++- homeassistant/components/toon/sensor.py | 21 ++++---- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index d1a8b702438..2946aacaa72 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -1,14 +1,17 @@ """Constants for the Toon integration.""" -from datetime import timedelta +from datetime import datetime, timedelta from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_PROBLEM, ) from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -127,6 +130,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: None, }, "gas_average": { ATTR_NAME: "Average Gas Usage", @@ -136,6 +141,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "gas_average_daily": { ATTR_NAME: "Average Daily Gas Usage", @@ -145,6 +152,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "gas_daily_usage": { ATTR_NAME: "Gas Usage Today", @@ -154,6 +163,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "gas_daily_cost": { ATTR_NAME: "Gas Cost Today", @@ -163,6 +174,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "gas_meter_reading": { ATTR_NAME: "Gas Meter", @@ -172,6 +185,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: datetime.fromtimestamp(0), }, "gas_value": { ATTR_NAME: "Current Gas Usage", @@ -181,6 +196,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "power_average": { ATTR_NAME: "Average Power Usage", @@ -190,6 +207,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "power_average_daily": { ATTR_NAME: "Average Daily Energy Usage", @@ -199,6 +218,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "power_daily_cost": { ATTR_NAME: "Energy Cost Today", @@ -208,6 +229,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "power_daily_value": { ATTR_NAME: "Energy Usage Today", @@ -217,6 +240,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "power_meter_reading": { ATTR_NAME: "Electricity Meter Feed IN Tariff 1", @@ -226,6 +251,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: datetime.fromtimestamp(0), }, "power_meter_reading_low": { ATTR_NAME: "Electricity Meter Feed IN Tariff 2", @@ -235,6 +262,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: datetime.fromtimestamp(0), }, "power_value": { ATTR_NAME: "Current Power Usage", @@ -244,6 +273,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: None, }, "solar_meter_reading_produced": { ATTR_NAME: "Electricity Meter Feed OUT Tariff 1", @@ -253,6 +284,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: datetime.fromtimestamp(0), }, "solar_meter_reading_low_produced": { ATTR_NAME: "Electricity Meter Feed OUT Tariff 2", @@ -262,6 +295,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: datetime.fromtimestamp(0), }, "solar_value": { ATTR_NAME: "Current Solar Power Production", @@ -271,6 +306,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: None, }, "solar_maximum": { ATTR_NAME: "Max Solar Power Production Today", @@ -280,6 +317,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "solar_produced": { ATTR_NAME: "Solar Power Production to Grid", @@ -289,6 +328,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: ATTR_MEASUREMENT, + ATTR_LAST_RESET: None, }, "power_usage_day_produced_solar": { ATTR_NAME: "Solar Energy Produced Today", @@ -298,6 +339,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "power_usage_day_to_grid_usage": { ATTR_NAME: "Energy Produced To Grid Today", @@ -307,6 +350,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "power_usage_day_from_grid_usage": { ATTR_NAME: "Energy Usage From Grid Today", @@ -316,6 +361,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "solar_average_produced": { ATTR_NAME: "Average Solar Power Production to Grid", @@ -325,6 +372,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "thermostat_info_current_modulation_level": { ATTR_NAME: "Boiler Modulation Level", @@ -334,6 +383,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:percent", ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: None, }, "power_usage_current_covered_by_solar": { ATTR_NAME: "Current Power Usage Covered By Solar", @@ -343,6 +394,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:solar-power", ATTR_DEFAULT_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: None, }, "water_average": { ATTR_NAME: "Average Water Usage", @@ -352,6 +405,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "water_average_daily": { ATTR_NAME: "Average Daily Water Usage", @@ -361,6 +416,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "water_daily_usage": { ATTR_NAME: "Water Usage Today", @@ -370,6 +427,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, "water_meter_reading": { ATTR_NAME: "Water Meter", @@ -379,6 +438,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: datetime.fromtimestamp(0), }, "water_value": { ATTR_NAME: "Current Water Usage", @@ -388,6 +449,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water-pump", ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: None, }, "water_daily_cost": { ATTR_NAME: "Water Cost Today", @@ -397,6 +460,8 @@ SENSOR_ENTITIES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water-pump", ATTR_DEFAULT_ENABLED: False, + ATTR_STATE_CLASS: None, + ATTR_LAST_RESET: None, }, } diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 36f5dedde3d..0e269c0bff3 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -1,7 +1,11 @@ """Support for Toon sensors.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -124,6 +128,11 @@ class ToonSensor(ToonEntity, SensorEntity): name=SENSOR_ENTITIES[key][ATTR_NAME], ) + self._attr_last_reset = SENSOR_ENTITIES[key][ATTR_LAST_RESET] + self._attr_state_class = SENSOR_ENTITIES[key][ATTR_STATE_CLASS] + self._attr_unit_of_measurement = SENSOR_ENTITIES[key][ATTR_UNIT_OF_MEASUREMENT] + self._sttr_device_class = SENSOR_ENTITIES[key][ATTR_DEVICE_CLASS] + @property def unique_id(self) -> str: """Return the unique ID for this sensor.""" @@ -140,16 +149,6 @@ class ToonSensor(ToonEntity, SensorEntity): ) return getattr(section, SENSOR_ENTITIES[self.key][ATTR_MEASUREMENT]) - @property - def unit_of_measurement(self) -> str | None: - """Return the unit this state is expressed in.""" - return SENSOR_ENTITIES[self.key][ATTR_UNIT_OF_MEASUREMENT] - - @property - def device_class(self) -> str | None: - """Return the device class.""" - return SENSOR_ENTITIES[self.key][ATTR_DEVICE_CLASS] - class ToonElectricityMeterDeviceSensor(ToonSensor, ToonElectricityMeterDeviceEntity): """Defines a Electricity Meter sensor.""" From 12e2c59a4c9a013b726d0aa7392a5726d5e0a182 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 24 May 2021 21:09:57 +0200 Subject: [PATCH 702/852] Improve typing in DuneHD integration (#51025) * Improve typing * One more typing fix * Run hassfest * Fix test * Fix return from constructor * Add missing Final * Improve long string format * Use bool for mute * Remove unnecessary str type * Fix host type * Add missing Final * Increase test coverage * Suggested change Co-authored-by: Ruslan Sayfutdinov Co-authored-by: Ruslan Sayfutdinov --- .strict-typing | 1 + homeassistant/components/dunehd/__init__.py | 14 ++- .../components/dunehd/config_flow.py | 52 ++++++----- homeassistant/components/dunehd/const.py | 10 ++- .../components/dunehd/media_player.py | 88 ++++++++++++------- mypy.ini | 11 +++ tests/components/dunehd/test_config_flow.py | 8 +- 7 files changed, 116 insertions(+), 68 deletions(-) diff --git a/.strict-typing b/.strict-typing index 3c409c85448..050847891f2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -23,6 +23,7 @@ homeassistant.components.canary.* homeassistant.components.cover.* homeassistant.components.device_automation.* homeassistant.components.device_tracker.* +homeassistant.components.dunehd.* homeassistant.components.elgato.* homeassistant.components.fitbit.* homeassistant.components.fritzbox.* diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py index af81b60b38e..24851dac4e8 100644 --- a/homeassistant/components/dunehd/__init__.py +++ b/homeassistant/components/dunehd/__init__.py @@ -1,16 +1,22 @@ """The Dune HD component.""" +from __future__ import annotations + +from typing import Final + from pdunehd import DuneHDPlayer +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant from .const import DOMAIN -PLATFORMS = ["media_player"] +PLATFORMS: Final[list[str]] = ["media_player"] -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - host = entry.data[CONF_HOST] + host: str = entry.data[CONF_HOST] player = DuneHDPlayer(host) @@ -22,7 +28,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index cbb248410e0..b6aec1e62f5 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -1,29 +1,34 @@ """Adds config flow for Dune HD integration.""" +from __future__ import annotations + import ipaddress import logging import re +from typing import Any, Final from pdunehd import DuneHDPlayer import voluptuous as vol from homeassistant import config_entries, exceptions from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -def host_valid(host): +def host_valid(host: str) -> bool: """Return True if hostname or IP address is valid.""" try: if ipaddress.ip_address(host).version in [4, 6]: return True except ValueError: - if len(host) > 253: - return False - allowed = re.compile(r"(?!-)[A-Z\d\-\_]{1,63}(? 253: + return False + allowed = re.compile(r"(?!-)[A-Z\d\-\_]{1,63}(? None: """Initialize Dune HD player.""" player = DuneHDPlayer(host) state = await self.hass.async_add_executor_job(player.update_state) if not state: raise CannotConnect() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: if host_valid(user_input[CONF_HOST]): - self.host = user_input[CONF_HOST] + host: str = user_input[CONF_HOST] try: - if self.host_already_configured(self.host): + if self.host_already_configured(host): raise AlreadyConfigured() - await self.init_device(self.host) + await self.init_device(host) except CannotConnect: errors[CONF_HOST] = "cannot_connect" except AlreadyConfigured: errors[CONF_HOST] = "already_configured" else: - return self.async_create_entry(title=self.host, data=user_input) + return self.async_create_entry(title=host, data=user_input) else: errors[CONF_HOST] = "invalid_host" @@ -69,21 +72,24 @@ class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input=None): + async def async_step_import( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle configuration by yaml file.""" - self.host = user_input[CONF_HOST] + assert user_input is not None + host: str = user_input[CONF_HOST] - self._async_abort_entries_match({CONF_HOST: self.host}) + self._async_abort_entries_match({CONF_HOST: host}) try: - await self.init_device(self.host) + await self.init_device(host) except CannotConnect: - _LOGGER.error("Import aborted, cannot connect to %s", self.host) + _LOGGER.error("Import aborted, cannot connect to %s", host) return self.async_abort(reason="cannot_connect") else: - return self.async_create_entry(title=self.host, data=user_input) + return self.async_create_entry(title=host, data=user_input) - def host_already_configured(self, host): + def host_already_configured(self, host: str) -> bool: """See if we already have a dunehd entry matching user input configured.""" existing_hosts = { entry.data[CONF_HOST] for entry in self._async_current_entries() diff --git a/homeassistant/components/dunehd/const.py b/homeassistant/components/dunehd/const.py index eef77d4bcbd..1cc89cf2028 100644 --- a/homeassistant/components/dunehd/const.py +++ b/homeassistant/components/dunehd/const.py @@ -1,4 +1,8 @@ """Constants for Dune HD integration.""" -ATTR_MANUFACTURER = "Dune" -DOMAIN = "dunehd" -DEFAULT_NAME = "Dune HD" +from __future__ import annotations + +from typing import Final + +ATTR_MANUFACTURER: Final = "Dune" +DOMAIN: Final = "dunehd" +DEFAULT_NAME: Final = "Dune HD" diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 8d73585cd69..17e9b6d9a37 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -1,7 +1,15 @@ """Dune HD implementation of the media player.""" +from __future__ import annotations + +from typing import Any, Final + +from pdunehd import DuneHDPlayer import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -10,7 +18,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, ) -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -19,13 +27,17 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN -CONF_SOURCES = "sources" +CONF_SOURCES: Final = "sources" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_SOURCES): vol.Schema({cv.string: cv.string}), @@ -33,7 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -DUNEHD_PLAYER_SUPPORT = ( +DUNEHD_PLAYER_SUPPORT: Final[int] = ( SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF @@ -43,9 +55,14 @@ DUNEHD_PLAYER_SUPPORT = ( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the Dune HD media player platform.""" - host = config.get(CONF_HOST) + host: str = config[CONF_HOST] hass.async_create_task( hass.config_entries.flow.async_init( @@ -54,11 +71,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Add Dune HD entities from a config_entry.""" - unique_id = config_entry.entry_id + unique_id = entry.entry_id - player = hass.data[DOMAIN][config_entry.entry_id] + player: str = hass.data[DOMAIN][entry.entry_id] async_add_entities([DuneHDPlayerEntity(player, DEFAULT_NAME, unique_id)], True) @@ -66,22 +85,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DuneHDPlayerEntity(MediaPlayerEntity): """Implementation of the Dune HD player.""" - def __init__(self, player, name, unique_id): + def __init__(self, player: DuneHDPlayer, name: str, unique_id: str) -> None: """Initialize entity to control Dune HD.""" self._player = player self._name = name - self._media_title = None - self._state = None + self._media_title: str | None = None + self._state: dict[str, Any] = {} self._unique_id = unique_id - def update(self): + def update(self) -> bool: """Update internal status of the entity.""" self._state = self._player.update_state() self.__update_title() return True @property - def state(self): + def state(self) -> StateType: """Return player state.""" state = STATE_OFF if "playback_position" in self._state: @@ -95,22 +114,22 @@ class DuneHDPlayerEntity(MediaPlayerEntity): return state @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" - return bool(self._state) + return len(self._state) > 0 @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "identifiers": {(DOMAIN, self._unique_id)}, @@ -119,57 +138,58 @@ class DuneHDPlayerEntity(MediaPlayerEntity): } @property - def volume_level(self): + def volume_level(self) -> float: """Return the volume level of the media player (0..1).""" return int(self._state.get("playback_volume", 0)) / 100 @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool: """Return a boolean if volume is currently muted.""" return int(self._state.get("playback_mute", 0)) == 1 @property - def supported_features(self): + def supported_features(self) -> int: """Flag media player features that are supported.""" return DUNEHD_PLAYER_SUPPORT - def volume_up(self): + def volume_up(self) -> None: """Volume up media player.""" self._state = self._player.volume_up() - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" self._state = self._player.volume_down() - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute/unmute player volume.""" self._state = self._player.mute(mute) - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self._media_title = None self._state = self._player.turn_off() - def turn_on(self): + def turn_on(self) -> None: """Turn off media player.""" self._state = self._player.turn_on() - def media_play(self): + def media_play(self) -> None: """Play media player.""" self._state = self._player.play() - def media_pause(self): + def media_pause(self) -> None: """Pause media player.""" self._state = self._player.pause() @property - def media_title(self): + def media_title(self) -> str | None: """Return the current media source.""" self.__update_title() if self._media_title: return self._media_title + return None - def __update_title(self): + def __update_title(self) -> None: if self._state.get("player_state") == "bluray_playback": self._media_title = "Blu-Ray" elif self._state.get("player_state") == "photo_viewer": @@ -179,10 +199,10 @@ class DuneHDPlayerEntity(MediaPlayerEntity): else: self._media_title = None - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" self._state = self._player.previous_track() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self._state = self._player.next_track() diff --git a/mypy.ini b/mypy.ini index cda4ff75ac3..b02a4c055da 100644 --- a/mypy.ini +++ b/mypy.ini @@ -264,6 +264,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.dunehd.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.elgato.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index c78bdec9dd3..c8e7b0f7f3e 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -67,10 +67,10 @@ async def test_user_invalid_host(hass): async def test_user_very_long_host(hass): """Test that errors are shown when the host is longer than 253 chars.""" long_host = ( - "very_long_host_very_long_host_very_long_host_very_long_host_very_long" - "host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_ho" - "st_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_" - "very_long_host_very_long_host" + "very_long_host_very_long_host_very_long_host_very_long_host_very_long_" + "host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_" + "host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_" + "host_very_long_host_very_long_host" ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: long_host} From a7eedeeabae0937298b3a8389490906a9292fac5 Mon Sep 17 00:00:00 2001 From: Xuefer Date: Tue, 25 May 2021 03:24:56 +0800 Subject: [PATCH 703/852] onvif: more debug info (#49658) Signed-off-by: Xuefer --- homeassistant/components/onvif/device.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index e8428696bfc..35dc436d201 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -104,8 +104,11 @@ class ONVIFDevice: # Fetch basic device info and capabilities self.info = await self.async_get_device_info() + LOGGER.debug("Camera %s info = %s", self.name, self.info) self.capabilities = await self.async_get_capabilities() + LOGGER.debug("Camera %s capabilities = %s", self.name, self.capabilities) self.profiles = await self.async_get_profiles() + LOGGER.debug("Camera %s profiles = %s", self.name, self.profiles) # No camera profiles to add if not self.profiles: From 2a47805b4a3d05384d35b378d7b3597f0cb5b3cd Mon Sep 17 00:00:00 2001 From: Xuefer Date: Tue, 25 May 2021 03:27:40 +0800 Subject: [PATCH 704/852] Close onvif device cleanly (#49659) * onvif: close device cleanly Signed-off-by: Xuefer * onvif: Too many nested blocks Signed-off-by: Xuefer * update tests to cover onvif config_flow Signed-off-by: Xuefer --- homeassistant/components/onvif/config_flow.py | 16 +++--- tests/components/onvif/test_config_flow.py | 53 +++++++++++++++++-- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index aeac90b301f..1fa904e67e4 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -206,9 +206,12 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not self.device_id: try: network_interfaces = await device_mgmt.GetNetworkInterfaces() - for interface in network_interfaces: - if interface.Enabled: - self.device_id = interface.Info.HwAddress + interface = next( + filter(lambda interface: interface.Enabled, network_interfaces), + None, + ) + if interface: + self.device_id = interface.Info.HwAddress except Fault as fault: if "not implemented" not in fault.message: raise fault @@ -248,8 +251,6 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not h264: return self.async_abort(reason="no_h264") - await device.close() - title = f"{self.onvif_config[CONF_NAME]} - {self.device_id}" return self.async_create_entry(title=title, data=self.onvif_config) @@ -259,13 +260,14 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.onvif_config[CONF_NAME], err, ) - await device.close() return self.async_abort(reason="onvif_error") except Fault: errors["base"] = "cannot_connect" - await device.close() + finally: + await device.close() + return self.async_show_form(step_id="auth", errors=errors) async def async_step_import(self, user_input): diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 2bacd61e09f..626eec433d1 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -41,6 +41,7 @@ def setup_mock_onvif_camera( with_h264=True, two_profiles=False, with_interfaces=True, + with_interfaces_not_implemented=False, with_serial=True, ): """Prepare mock onvif.ONVIFCamera.""" @@ -54,9 +55,14 @@ def setup_mock_onvif_camera( interface.Enabled = True interface.Info.HwAddress = MAC - devicemgmt.GetNetworkInterfaces = AsyncMock( - return_value=[interface] if with_interfaces else [] - ) + if with_interfaces_not_implemented: + devicemgmt.GetNetworkInterfaces = AsyncMock( + side_effect=Fault("not implemented") + ) + else: + devicemgmt.GetNetworkInterfaces = AsyncMock( + return_value=[interface] if with_interfaces else [] + ) media_service = MagicMock() @@ -413,6 +419,47 @@ async def test_flow_manual_entry(hass): } +async def test_flow_import_not_implemented(hass): + """Test that config flow uses Serial Number when no MAC available.""" + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device, patch( + "homeassistant.components.onvif.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.onvif.async_setup_entry", return_value=True + ) as mock_setup_entry: + setup_mock_onvif_camera(mock_onvif_camera, with_interfaces_not_implemented=True) + setup_mock_device(mock_device) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{NAME} - {SERIAL_NUMBER}" + assert result["data"] == { + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + } + + async def test_flow_import_no_mac(hass): """Test that config flow uses Serial Number when no MAC available.""" with patch( From 0fb2504e0c456b7e67a1713bf82efeda725ee1a1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 25 May 2021 00:12:25 +0000 Subject: [PATCH 705/852] [ci skip] Translation update --- .../components/fritz/translations/et.json | 9 ++++ .../components/fritz/translations/ru.json | 9 ++++ .../components/sia/translations/en.json | 50 +++++++++++++++++++ .../components/sia/translations/et.json | 50 +++++++++++++++++++ .../components/sia/translations/ru.json | 50 +++++++++++++++++++ .../components/sia/translations/zh-Hant.json | 50 +++++++++++++++++++ .../components/wallbox/translations/en.json | 4 +- .../components/wallbox/translations/et.json | 22 ++++++++ .../components/wallbox/translations/ru.json | 22 ++++++++ 9 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/sia/translations/en.json create mode 100644 homeassistant/components/sia/translations/et.json create mode 100644 homeassistant/components/sia/translations/ru.json create mode 100644 homeassistant/components/sia/translations/zh-Hant.json create mode 100644 homeassistant/components/wallbox/translations/et.json create mode 100644 homeassistant/components/wallbox/translations/ru.json diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json index 5914932125c..2866ae13336 100644 --- a/homeassistant/components/fritz/translations/et.json +++ b/homeassistant/components/fritz/translations/et.json @@ -51,5 +51,14 @@ "title": "Seadista FRITZ! Box Tools" } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Millal m\u00e4\u00e4rata seade olema kodus (sekundites)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json index 52fe427f822..7c030ca4d88 100644 --- a/homeassistant/components/fritz/translations/ru.json +++ b/homeassistant/components/fritz/translations/ru.json @@ -51,5 +51,14 @@ "title": "FRITZ!Box Tools" } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u0414\u043e\u043c\u0430\"" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/en.json b/homeassistant/components/sia/translations/en.json new file mode 100644 index 00000000000..ff6781669dd --- /dev/null +++ b/homeassistant/components/sia/translations/en.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "The account is not a hex value, please use only 0-9 and A-F.", + "invalid_account_length": "The account is not the right length, it has to be between 3 and 16 characters.", + "invalid_key_format": "The key is not a hex value, please use only 0-9 and A-F.", + "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 characters hex characters.", + "invalid_ping": "The ping interval needs to be between 1 and 1440 minutes.", + "invalid_zones": "There needs to be at least 1 zone.", + "unknown": "Unexpected error" + }, + "step": { + "additional_account": { + "data": { + "account": "Account ID", + "additional_account": "Additional accounts", + "encryption_key": "Encryption Key", + "ping_interval": "Ping Interval (min)", + "zones": "Number of zones for the account" + }, + "title": "Add another account to the current port." + }, + "user": { + "data": { + "account": "Account ID", + "additional_account": "Additional accounts", + "encryption_key": "Encryption Key", + "ping_interval": "Ping Interval (min)", + "port": "Port", + "protocol": "Protocol", + "zones": "Number of zones for the account" + }, + "title": "Create a connection for SIA based alarm systems." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignore the timestamp check of the SIA events", + "zones": "Number of zones for the account" + }, + "description": "Set the options for account: {account}", + "title": "Options for the SIA Setup." + } + } + }, + "title": "SIA Alarm Systems" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/et.json b/homeassistant/components/sia/translations/et.json new file mode 100644 index 00000000000..931718d78d8 --- /dev/null +++ b/homeassistant/components/sia/translations/et.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "Konto ei ole HEX v\u00e4\u00e4rtus, lubatud on ainult 0\u20139 ja A-F.", + "invalid_account_length": "Kontonimi ei ole \u00f5ige pikkusega, see peab olema 3\u201316 t\u00e4hem\u00e4rki.", + "invalid_key_format": "V\u00f5ti ei ole HEX v\u00e4\u00e4rtus, lubatud on ainult 0-9 ja A-F.", + "invalid_key_length": "V\u00f5ti ei ole \u00f5ige pikkusega, see peab olema 16, 24 v\u00f5i 32 HEX t\u00e4hem\u00e4rki.", + "invalid_ping": "P\u00e4ringute intervall peab olema 1 kuni 1440 minutit.", + "invalid_zones": "Peab olema v\u00e4hemalt 1 tsoon.", + "unknown": "Tundmatu t\u00f5rge" + }, + "step": { + "additional_account": { + "data": { + "account": "Konto", + "additional_account": "T\u00e4ienfav konto", + "encryption_key": "Kr\u00fcptov\u00f5ti", + "ping_interval": "P\u00e4ringute intervall", + "zones": "Tsoonid" + }, + "title": "Lisa praegusele pordile veel \u00fcks konto." + }, + "user": { + "data": { + "account": "Konto ID", + "additional_account": "T\u00e4iendavad kontod", + "encryption_key": "Kr\u00fcptov\u00f5ti", + "ping_interval": "P\u00e4ringute intervall (minutites)", + "port": "Port", + "protocol": "Protokoll", + "zones": "Konto tsoonide arv" + }, + "title": "Loo \u00fchendus SIA-p\u00f5hise valves\u00fcsteemiga." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Eira SIA-i s\u00fcndmuste ajatempli kontrolli", + "zones": "Tsoonid" + }, + "description": "Konto suvandite m\u00e4\u00e4ramine: {account}", + "title": "SIA seadistuse valikud." + } + } + }, + "title": "SIA Alarm Systems" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/ru.json b/homeassistant/components/sia/translations/ru.json new file mode 100644 index 00000000000..f7cfd4b5152 --- /dev/null +++ b/homeassistant/components/sia/translations/ru.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e 0\u20139 \u0438 AF.", + "invalid_account_length": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0435\u0439 \u0434\u043b\u0438\u043d\u044b, \u043e\u043d\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u043e\u0442 3 \u0434\u043e 16 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432.", + "invalid_key_format": "\u041a\u043b\u044e\u0447 \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e 0\u20139 \u0438 AF.", + "invalid_key_length": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0434\u043b\u0438\u043d\u0430 \u043a\u043b\u044e\u0447\u0430. \u041a\u043b\u044e\u0447 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 16, 24 \u0438\u043b\u0438 32 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0445 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432.", + "invalid_ping": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0441\u0432\u044f\u0437\u0438 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0442 1 \u0434\u043e 1440 \u043c\u0438\u043d\u0443\u0442.", + "invalid_zones": "\u0414\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c 1 \u0437\u043e\u043d\u0430.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "additional_account": { + "data": { + "account": "ID \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438", + "additional_account": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438", + "encryption_key": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "ping_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0441\u0432\u044f\u0437\u0438 (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)", + "zones": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043e\u043d \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438" + }, + "title": "\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0435\u0449\u0451 \u043e\u0434\u043d\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043a \u0442\u0435\u043a\u0443\u0449\u0435\u043c\u0443 \u043f\u043e\u0440\u0442\u0443" + }, + "user": { + "data": { + "account": "ID \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438", + "additional_account": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438", + "encryption_key": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "ping_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0441\u0432\u044f\u0437\u0438 (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)", + "port": "\u041f\u043e\u0440\u0442", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "zones": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043e\u043d \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438" + }, + "title": "\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0438\u0441\u0442\u0435\u043c \u043e\u0445\u0440\u0430\u043d\u043d\u043e\u0439 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 SIA" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043e\u0442\u043c\u0435\u0442\u043a\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 SIA", + "zones": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043e\u043d \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438" + }, + "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438: {account}", + "title": "SIA Alarm Systems" + } + } + }, + "title": "SIA Alarm Systems" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/zh-Hant.json b/homeassistant/components/sia/translations/zh-Hant.json new file mode 100644 index 00000000000..6ebf56aa049 --- /dev/null +++ b/homeassistant/components/sia/translations/zh-Hant.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "\u5e33\u865f\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002", + "invalid_account_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u4ecb\u65bc 3 \u81f3 16 \u500b\u5b57\u5143\u4e4b\u9593\u3002", + "invalid_key_format": "\u5bc6\u9470\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002", + "invalid_key_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u70ba 14\u300124 \u6216 32 \u500b\u5341\u516d\u9032\u4f4d\u5b57\u5143\u3002", + "invalid_ping": "Ping \u9593\u8ddd\u5fc5\u9808\u70ba 1 \u81f3 1440 \u5206\u9418\u4e4b\u9593\u3002", + "invalid_zones": "\u81f3\u5c11\u5fc5\u9808\u6709\u4e00\u500b\u5206\u5340\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "additional_account": { + "data": { + "account": "\u5e33\u865f ID", + "additional_account": "\u9644\u52a0\u5e33\u865f", + "encryption_key": "\u5bc6\u9470", + "ping_interval": "Pin \u9593\u8ddd\uff08\u5206\u9418\uff09", + "zones": "\u5e33\u865f\u5206\u5340\u6578\u76ee" + }, + "title": "\u65bc\u76ee\u524d\u901a\u8a0a\u57e0\u65b0\u589e\u53e6\u4e00\u7d44\u5e33\u865f\u3002" + }, + "user": { + "data": { + "account": "\u5e33\u865f ID", + "additional_account": "\u9644\u52a0\u5e33\u865f", + "encryption_key": "\u5bc6\u9470", + "ping_interval": "Pin \u9593\u8ddd\uff08\u5206\u9418\uff09", + "port": "\u901a\u8a0a\u57e0", + "protocol": "\u901a\u8a0a\u5354\u5b9a", + "zones": "\u5e33\u865f\u5206\u5340\u6578\u76ee" + }, + "title": "\u65b0\u589e SIA \u8b66\u5831\u7cfb\u7d71\u9023\u7dda\u3002" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "\u5ffd\u7565 SIA \u4e8b\u4ef6\u6642\u9593\u6233\u6aa2\u67e5", + "zones": "\u5e33\u865f\u5206\u5340\u6578\u76ee" + }, + "description": "\u8a2d\u5b9a\u5e33\u865f\u9078\u9805\uff1a{account}", + "title": "SIA \u8a2d\u5b9a\u9078\u9805" + } + } + }, + "title": "SIA \u8b66\u5831\u7cfb\u7d71" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/en.json b/homeassistant/components/wallbox/translations/en.json index a63fe801490..52dcf8530d4 100644 --- a/homeassistant/components/wallbox/translations/en.json +++ b/homeassistant/components/wallbox/translations/en.json @@ -11,12 +11,12 @@ "step": { "user": { "data": { - "station": "Station S/N", "password": "Password", + "station": "Station Serial Number", "username": "Username" } } } }, - "title": "MyWallbox" + "title": "Wallbox" } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/et.json b/homeassistant/components/wallbox/translations/et.json new file mode 100644 index 00000000000..12e24fd83ba --- /dev/null +++ b/homeassistant/components/wallbox/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Tundmatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "station": "Seadme seerianumber", + "username": "Kasutajanimi" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/ru.json b/homeassistant/components/wallbox/translations/ru.json new file mode 100644 index 00000000000..b6b33a6eb48 --- /dev/null +++ b/homeassistant/components/wallbox/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\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", + "station": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0441\u0442\u0430\u043d\u0446\u0438\u0438", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file From 2eb87b8806588e2d065b54705ba745cf8a3f2663 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 25 May 2021 13:57:07 +0800 Subject: [PATCH 706/852] Combine StreamBuffer into SegmentBuffer in stream (#51041) * Combine StreamBuffer into SegmentBuffer in stream * Use new style type hint in comment Remove unused member self._segment * Change reset_av to static helper function * Change make_new_av to only return OutputContainer --- homeassistant/components/stream/core.py | 16 +--- homeassistant/components/stream/worker.py | 109 ++++++++++++---------- 2 files changed, 62 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index a289464f92b..0d29474858f 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import deque import io -from typing import TYPE_CHECKING, Callable +from typing import Callable from aiohttp import web import attr @@ -16,23 +16,9 @@ from homeassistant.util.decorator import Registry from .const import ATTR_STREAMS, DOMAIN -if TYPE_CHECKING: - import av.container - import av.video - PROVIDERS = Registry() -@attr.s -class StreamBuffer: - """Represent a segment.""" - - segment: io.BytesIO = attr.ib() - output: av.container.OutputContainer = attr.ib() - vstream: av.video.VideoStream = attr.ib() - astream = attr.ib(default=None) # type=Optional[av.audio.AudioStream] - - @attr.s class Segment: """Represent a segment.""" diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 05dc0b076a4..d6562cf93db 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -2,8 +2,9 @@ from __future__ import annotations from collections import deque -import io +from io import BytesIO import logging +from typing import cast import av @@ -17,38 +18,11 @@ from .const import ( SEGMENT_CONTAINER_FORMAT, STREAM_TIMEOUT, ) -from .core import Segment, StreamBuffer, StreamOutput +from .core import Segment, StreamOutput _LOGGER = logging.getLogger(__name__) -def create_stream_buffer(video_stream, audio_stream, sequence): - """Create a new StreamBuffer.""" - - segment = io.BytesIO() - container_options = { - # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 - "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets", - "avoid_negative_ts": "disabled", - "fragment_index": str(sequence), - } - output = av.open( - segment, - mode="w", - format=SEGMENT_CONTAINER_FORMAT, - container_options={ - "video_track_timescale": str(int(1 / video_stream.time_base)), - **container_options, - }, - ) - vstream = output.add_stream(template=video_stream) - # Check if audio is requested - astream = None - if audio_stream and audio_stream.name in AUDIO_CODECS: - astream = output.add_stream(template=audio_stream) - return StreamBuffer(segment, output, vstream, astream) - - class SegmentBuffer: """Buffer for writing a sequence of packets to the output as a segment.""" @@ -61,12 +35,41 @@ class SegmentBuffer: self._outputs: list[StreamOutput] = [] self._sequence = 0 self._segment_start_pts = None - self._stream_buffer = None + self._memory_file: BytesIO = cast(BytesIO, None) + self._av_output: av.container.OutputContainer = None + self._input_video_stream: av.video.VideoStream = None + self._input_audio_stream = None # av.audio.AudioStream | None + self._output_video_stream: av.video.VideoStream = None + self._output_audio_stream = None # av.audio.AudioStream | None - def set_streams(self, video_stream, audio_stream): + @staticmethod + def make_new_av( + memory_file, sequence: int, input_vstream: av.video.VideoStream + ) -> av.container.OutputContainer: + """Make a new av OutputContainer.""" + return av.open( + memory_file, + mode="w", + format=SEGMENT_CONTAINER_FORMAT, + container_options={ + # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 + # "cmaf" flag replaces several of the movflags used, but too recent to use for now + "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer", + "avoid_negative_ts": "disabled", + "fragment_index": str(sequence + 1), + "video_track_timescale": str(int(1 / input_vstream.time_base)), + }, + ) + + def set_streams( + self, + video_stream: av.video.VideoStream, + audio_stream, + # no type hint for audio_stream until https://github.com/PyAV-Org/PyAV/pull/775 is merged + ) -> None: """Initialize output buffer with streams from container.""" - self._video_stream = video_stream - self._audio_stream = audio_stream + self._input_video_stream = video_stream + self._input_audio_stream = audio_stream def reset(self, video_pts): """Initialize a new stream segment.""" @@ -77,15 +80,27 @@ class SegmentBuffer: # Fetch the latest StreamOutputs, which may have changed since the # worker started. self._outputs = self._outputs_callback().values() - self._stream_buffer = create_stream_buffer( - self._video_stream, self._audio_stream, self._sequence + self._memory_file = BytesIO() + self._av_output = self.make_new_av( + memory_file=self._memory_file, + sequence=self._sequence, + input_vstream=self._input_video_stream, ) + self._output_video_stream = self._av_output.add_stream( + template=self._input_video_stream + ) + # Check if audio is requested + self._output_audio_stream = None + if self._input_audio_stream and self._input_audio_stream.name in AUDIO_CODECS: + self._output_audio_stream = self._av_output.add_stream( + template=self._input_audio_stream + ) def mux_packet(self, packet): - """Mux a packet to the appropriate StreamBuffers.""" + """Mux a packet to the appropriate output stream.""" # Check for end of segment - if packet.stream == self._video_stream and packet.is_keyframe: + if packet.stream == self._input_video_stream and packet.is_keyframe: duration = (packet.pts - self._segment_start_pts) * packet.time_base if duration >= MIN_SEGMENT_DURATION: # Save segment to outputs @@ -95,19 +110,17 @@ class SegmentBuffer: self.reset(packet.pts) # Mux the packet - if packet.stream == self._video_stream: - packet.stream = self._stream_buffer.vstream - self._stream_buffer.output.mux(packet) - elif packet.stream == self._audio_stream: - packet.stream = self._stream_buffer.astream - self._stream_buffer.output.mux(packet) + if packet.stream == self._input_video_stream: + packet.stream = self._output_video_stream + self._av_output.mux(packet) + elif packet.stream == self._input_audio_stream: + packet.stream = self._output_audio_stream + self._av_output.mux(packet) def flush(self, duration): """Create a segment from the buffered packets and write to output.""" - self._stream_buffer.output.close() - segment = Segment( - self._sequence, self._stream_buffer.segment, duration, self._stream_id - ) + self._av_output.close() + segment = Segment(self._sequence, self._memory_file, duration, self._stream_id) for stream_output in self._outputs: stream_output.put(segment) @@ -120,7 +133,7 @@ class SegmentBuffer: def close(self): """Close stream buffer.""" - self._stream_buffer.output.close() + self._av_output.close() def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901 From c5383219f1e923b35287b143d26e8a73cba01ad8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 25 May 2021 11:52:20 +0200 Subject: [PATCH 707/852] Remove stale debug statements from tests (#51059) --- tests/auth/providers/test_trusted_networks.py | 1 - tests/components/caldav/test_calendar.py | 2 -- tests/components/here_travel_time/test_sensor.py | 1 - tests/components/ozw/test_websocket_api.py | 1 - tests/components/samsungtv/test_config_flow.py | 1 - tests/components/sonarr/test_sensor.py | 1 - tests/components/template/test_light.py | 1 - tests/helpers/test_entity_platform.py | 1 - tests/test_config.py | 1 - tests/test_exceptions.py | 1 - 10 files changed, 11 deletions(-) diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 4ece4875ba4..39764fa4206 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -296,7 +296,6 @@ async def test_trusted_group_login(manager_with_user, provider_with_user): schema = step["data_schema"] # only user listed - print(user.id) assert schema({"user": user.id}) with pytest.raises(vol.Invalid): assert schema({"user": owner.id}) diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index d8c6a44a3ea..5a5b7adf0e3 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -401,8 +401,6 @@ async def test_ongoing_floating_event_returned(mock_now, hass, calendar): await hass.async_block_till_done() state = hass.states.get("calendar.private") - print(dt.DEFAULT_TIME_ZONE) - print(state) assert state.name == calendar.name assert state.state == STATE_ON assert dict(state.attributes) == { diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index b2fcef715f1..2f69dc97a84 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -83,7 +83,6 @@ def _build_mock_url(origin, destination, modes, api_key, departure=None, arrival if departure is None and arrival is None: parameters["departure"] = "now" url = base_url + urllib.parse.urlencode(parameters) - print(url) return url diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py index 383d3425ffb..ad3b568b62a 100644 --- a/tests/components/ozw/test_websocket_api.py +++ b/tests/components/ozw/test_websocket_api.py @@ -153,7 +153,6 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): # Test set config parameter config_param = result[0] - print(config_param) current_val = config_param[ATTR_VALUE] new_val = next( option[0] diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 5ac6caf40a9..04dffd31801 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -728,7 +728,6 @@ async def test_autodetect_legacy(hass: HomeAssistant, remote: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - print(result) assert result["type"] == "create_entry" assert result["data"][CONF_METHOD] == "legacy" assert remote.call_count == 1 diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 9824319f3ff..96d350ac6df 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -109,7 +109,6 @@ async def test_disabled_by_default_sensors( """Test the disabled by default sensors.""" await setup_integration(hass, aioclient_mock) registry = er.async_get(hass) - print(registry.entities) state = hass.states.get(entity_id) assert state is None diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index ec0347b8470..b2eb5f06417 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -435,7 +435,6 @@ async def test_on_action_with_transition(hass, calls): ) assert len(calls) == 1 - print(calls[0].data) assert calls[0].data["transition"] == 5 diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 4b5ede7ca00..6675e441adf 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -946,7 +946,6 @@ async def test_entity_info_added_to_entity_registry(hass): registry = er.async_get(hass) entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default") - print(entry_default) assert entry_default.capabilities == {"max": 100} assert entry_default.supported_features == 5 assert entry_default.device_class == "mock-device-class" diff --git a/tests/test_config.py b/tests/test_config.py index 2ca46e2c1e9..87496c566e3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -218,7 +218,6 @@ def test_customize_dict_schema(): values = ({ATTR_FRIENDLY_NAME: None}, {ATTR_ASSUMED_STATE: "2"}) for val in values: - print(val) with pytest.raises(MultipleInvalid): config_util.CUSTOMIZE_DICT_SCHEMA(val) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 959f0846cae..e353c0dd82d 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -29,7 +29,6 @@ def test_conditionerror_format(): ) error_container1 = ConditionErrorContainer("box", errors=[error_pos1, error_pos2]) - print(error_container1) assert ( str(error_container1) == """In 'box' (item 1 of 2): From ef33bbe9bc5d80cdd3dd135f82885fc9893cc169 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 25 May 2021 12:43:50 +0200 Subject: [PATCH 708/852] Fix dispatcher for Fritz integration (#51061) --- homeassistant/components/fritz/common.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 7fe7069de17..ad906e8956b 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -20,7 +20,7 @@ from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util @@ -165,9 +165,9 @@ class FritzBoxTools: self._devices[dev_mac] = device new_device = True - async_dispatcher_send(self.hass, self.signal_device_update) + dispatcher_send(self.hass, self.signal_device_update) if new_device: - async_dispatcher_send(self.hass, self.signal_device_new) + dispatcher_send(self.hass, self.signal_device_new) async def service_fritzbox(self, service: str) -> None: """Define FRITZ!Box services.""" From 3573249720f689088020978dd89d63a8d4c6d24c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 25 May 2021 07:10:42 -0400 Subject: [PATCH 709/852] Bump zwave-js-server-python to 0.25.0 (#51053) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index e730b6ae9db..9c632f72f47 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.24.0"], + "requirements": ["zwave-js-server-python==0.25.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 6dae9b586e8..0d41f2be892 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2442,4 +2442,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.24.0 +zwave-js-server-python==0.25.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e296b34971..2b5de01177a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1324,4 +1324,4 @@ zigpy-znp==0.5.1 zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.24.0 +zwave-js-server-python==0.25.0 From fe34f42aa5f3a82843b7aae88a34064bb78c7bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Moreno?= Date: Tue, 25 May 2021 13:11:48 +0200 Subject: [PATCH 710/852] Add new Meteoclimatic integration (#36906) Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .coveragerc | 3 + CODEOWNERS | 1 + .../components/meteoclimatic/__init__.py | 52 +++++++ .../components/meteoclimatic/config_flow.py | 64 +++++++++ .../components/meteoclimatic/const.py | 134 ++++++++++++++++++ .../components/meteoclimatic/manifest.json | 13 ++ .../components/meteoclimatic/strings.json | 20 +++ .../meteoclimatic/translations/en.json | 20 +++ .../components/meteoclimatic/weather.py | 93 ++++++++++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/meteoclimatic/__init__.py | 1 + tests/components/meteoclimatic/conftest.py | 13 ++ .../meteoclimatic/test_config_flow.py | 88 ++++++++++++ 15 files changed, 509 insertions(+) create mode 100644 homeassistant/components/meteoclimatic/__init__.py create mode 100644 homeassistant/components/meteoclimatic/config_flow.py create mode 100644 homeassistant/components/meteoclimatic/const.py create mode 100644 homeassistant/components/meteoclimatic/manifest.json create mode 100644 homeassistant/components/meteoclimatic/strings.json create mode 100644 homeassistant/components/meteoclimatic/translations/en.json create mode 100644 homeassistant/components/meteoclimatic/weather.py create mode 100644 tests/components/meteoclimatic/__init__.py create mode 100644 tests/components/meteoclimatic/conftest.py create mode 100644 tests/components/meteoclimatic/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 5fc54f1dbea..3399a10a8b0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -611,6 +611,9 @@ omit = homeassistant/components/meteo_france/sensor.py homeassistant/components/meteo_france/weather.py homeassistant/components/meteoalarm/* + homeassistant/components/meteoclimatic/__init__.py + homeassistant/components/meteoclimatic/const.py + homeassistant/components/meteoclimatic/weather.py homeassistant/components/metoffice/sensor.py homeassistant/components/metoffice/weather.py homeassistant/components/microsoft/tts.py diff --git a/CODEOWNERS b/CODEOWNERS index 3d7a53749cb..faa623456f1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -290,6 +290,7 @@ homeassistant/components/met/* @danielhiversen @thimic homeassistant/components/met_eireann/* @DylanGore homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame homeassistant/components/meteoalarm/* @rolfberkenbosch +homeassistant/components/meteoclimatic/* @adrianmo homeassistant/components/metoffice/* @MrHarcombe homeassistant/components/miflora/* @danielhiversen @basnijholt homeassistant/components/mikrotik/* @engrbm87 diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py new file mode 100644 index 00000000000..79e63e9b64d --- /dev/null +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -0,0 +1,52 @@ +"""Support for Meteoclimatic weather data.""" +import logging + +from meteoclimatic import MeteoclimaticClient +from meteoclimatic.exceptions import MeteoclimaticError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_STATION_CODE, DOMAIN, PLATFORMS, SCAN_INTERVAL + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up a Meteoclimatic entry.""" + station_code = entry.data[CONF_STATION_CODE] + meteoclimatic_client = MeteoclimaticClient() + + async def async_update_data(): + """Obtain the latest data from Meteoclimatic.""" + try: + data = await hass.async_add_executor_job( + meteoclimatic_client.weather_at_station, station_code + ) + return data.__dict__ + except MeteoclimaticError as err: + raise UpdateFailed(f"Error while retrieving data: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"Meteoclimatic Coordinator for {station_code}", + update_method=async_update_data, + update_interval=SCAN_INTERVAL, + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + return unload_ok diff --git a/homeassistant/components/meteoclimatic/config_flow.py b/homeassistant/components/meteoclimatic/config_flow.py new file mode 100644 index 00000000000..49be3889ead --- /dev/null +++ b/homeassistant/components/meteoclimatic/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow to configure the Meteoclimatic integration.""" +import logging + +from meteoclimatic import MeteoclimaticClient +from meteoclimatic.exceptions import MeteoclimaticError, StationNotFound +import voluptuous as vol + +from homeassistant import config_entries + +from .const import CONF_STATION_CODE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class MeteoclimaticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Meteoclimatic config flow.""" + + VERSION = 1 + + def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_STATION_CODE, default=user_input.get(CONF_STATION_CODE, "") + ): str + } + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + return self._show_setup_form(user_input, errors) + + station_code = user_input[CONF_STATION_CODE] + client = MeteoclimaticClient() + + try: + weather = await self.hass.async_add_executor_job( + client.weather_at_station, station_code + ) + except StationNotFound as exp: + _LOGGER.error("Station not found: %s", exp) + errors["base"] = "not_found" + return self._show_setup_form(user_input, errors) + except MeteoclimaticError as exp: + _LOGGER.error("Error when obtaining Meteoclimatic weather: %s", exp) + return self.async_abort(reason="unknown") + + # Check if already configured + await self.async_set_unique_id(station_code, raise_on_progress=False) + + return self.async_create_entry( + title=weather.station.name, data={CONF_STATION_CODE: station_code} + ) diff --git a/homeassistant/components/meteoclimatic/const.py b/homeassistant/components/meteoclimatic/const.py new file mode 100644 index 00000000000..eb3823a9b42 --- /dev/null +++ b/homeassistant/components/meteoclimatic/const.py @@ -0,0 +1,134 @@ +"""Meteoclimatic component constants.""" + +from datetime import timedelta + +from meteoclimatic import Condition + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, +) +from homeassistant.const import ( + DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + LENGTH_MILLIMETERS, + PERCENTAGE, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) + +DOMAIN = "meteoclimatic" +PLATFORMS = ["weather"] +ATTRIBUTION = "Data provided by Meteoclimatic" + +SCAN_INTERVAL = timedelta(minutes=10) + +CONF_STATION_CODE = "station_code" + +DEFAULT_WEATHER_CARD = True + +SENSOR_TYPE_NAME = "name" +SENSOR_TYPE_UNIT = "unit" +SENSOR_TYPE_ICON = "icon" +SENSOR_TYPE_CLASS = "device_class" +SENSOR_TYPES = { + "temp_current": { + SENSOR_TYPE_NAME: "Temperature", + SENSOR_TYPE_UNIT: TEMP_CELSIUS, + SENSOR_TYPE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + "temp_max": { + SENSOR_TYPE_NAME: "Max Temp.", + SENSOR_TYPE_UNIT: TEMP_CELSIUS, + SENSOR_TYPE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + "temp_min": { + SENSOR_TYPE_NAME: "Min Temp.", + SENSOR_TYPE_UNIT: TEMP_CELSIUS, + SENSOR_TYPE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + "humidity_current": { + SENSOR_TYPE_NAME: "Humidity", + SENSOR_TYPE_UNIT: PERCENTAGE, + SENSOR_TYPE_CLASS: DEVICE_CLASS_HUMIDITY, + }, + "humidity_max": { + SENSOR_TYPE_NAME: "Max Humidity", + SENSOR_TYPE_UNIT: PERCENTAGE, + SENSOR_TYPE_CLASS: DEVICE_CLASS_HUMIDITY, + }, + "humidity_min": { + SENSOR_TYPE_NAME: "Min Humidity", + SENSOR_TYPE_UNIT: PERCENTAGE, + SENSOR_TYPE_CLASS: DEVICE_CLASS_HUMIDITY, + }, + "pressure_current": { + SENSOR_TYPE_NAME: "Pressure", + SENSOR_TYPE_UNIT: PRESSURE_HPA, + SENSOR_TYPE_CLASS: DEVICE_CLASS_PRESSURE, + }, + "pressure_max": { + SENSOR_TYPE_NAME: "Max Pressure", + SENSOR_TYPE_UNIT: PRESSURE_HPA, + SENSOR_TYPE_CLASS: DEVICE_CLASS_PRESSURE, + }, + "pressure_min": { + SENSOR_TYPE_NAME: "Min Pressure", + SENSOR_TYPE_UNIT: PRESSURE_HPA, + SENSOR_TYPE_CLASS: DEVICE_CLASS_PRESSURE, + }, + "wind_current": { + SENSOR_TYPE_NAME: "Wind Speed", + SENSOR_TYPE_UNIT: SPEED_KILOMETERS_PER_HOUR, + SENSOR_TYPE_ICON: "mdi:weather-windy", + }, + "wind_max": { + SENSOR_TYPE_NAME: "Max Wind Speed", + SENSOR_TYPE_UNIT: SPEED_KILOMETERS_PER_HOUR, + SENSOR_TYPE_ICON: "mdi:weather-windy", + }, + "wind_bearing": { + SENSOR_TYPE_NAME: "Wind Bearing", + SENSOR_TYPE_UNIT: DEGREE, + SENSOR_TYPE_ICON: "mdi:weather-windy", + }, + "rain": { + SENSOR_TYPE_NAME: "Rain", + SENSOR_TYPE_UNIT: LENGTH_MILLIMETERS, + SENSOR_TYPE_ICON: "mdi:weather-rainy", + }, +} + +CONDITION_CLASSES = { + ATTR_CONDITION_CLEAR_NIGHT: [Condition.moon, Condition.hazemoon], + ATTR_CONDITION_CLOUDY: [Condition.mooncloud], + ATTR_CONDITION_EXCEPTIONAL: [], + ATTR_CONDITION_FOG: [Condition.fog, Condition.mist], + ATTR_CONDITION_HAIL: [], + ATTR_CONDITION_LIGHTNING: [Condition.storm], + ATTR_CONDITION_LIGHTNING_RAINY: [], + ATTR_CONDITION_PARTLYCLOUDY: [Condition.suncloud, Condition.hazesun], + ATTR_CONDITION_POURING: [], + ATTR_CONDITION_RAINY: [Condition.rain], + ATTR_CONDITION_SNOWY: [], + ATTR_CONDITION_SNOWY_RAINY: [], + ATTR_CONDITION_SUNNY: [Condition.sun], + ATTR_CONDITION_WINDY: [], + ATTR_CONDITION_WINDY_VARIANT: [], +} diff --git a/homeassistant/components/meteoclimatic/manifest.json b/homeassistant/components/meteoclimatic/manifest.json new file mode 100644 index 00000000000..71174f216a4 --- /dev/null +++ b/homeassistant/components/meteoclimatic/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "meteoclimatic", + "name": "Meteoclimatic", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/meteoclimatic", + "requirements": [ + "pymeteoclimatic==0.0.6" + ], + "codeowners": [ + "@adrianmo" + ], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/meteoclimatic/strings.json b/homeassistant/components/meteoclimatic/strings.json new file mode 100644 index 00000000000..2353c22c7cc --- /dev/null +++ b/homeassistant/components/meteoclimatic/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "title": "Meteoclimatic", + "description": "Enter the Meteoclimatic station code (e.g., ESCAT4300000043206B)", + "data": { + "code": "Station code" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "error": { + "not_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/en.json b/homeassistant/components/meteoclimatic/translations/en.json new file mode 100644 index 00000000000..4868d4e4656 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Station already configured", + "unknown": "Unknown error: please try again later" + }, + "error": { + "not_found": "The station code did not return any data. Check that the code belongs to a station and it has the right format (e.g., ESCAT4300000043206B)" + }, + "step": { + "user": { + "data": { + "code": "Station code" + }, + "description": "Enter the Meteoclimatic station code (e.g., ESCAT4300000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py new file mode 100644 index 00000000000..7059e935b2e --- /dev/null +++ b/homeassistant/components/meteoclimatic/weather.py @@ -0,0 +1,93 @@ +"""Support for Meteoclimatic weather service.""" +from meteoclimatic import Condition + +from homeassistant.components.weather import WeatherEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ATTRIBUTION, CONDITION_CLASSES, DOMAIN + + +def format_condition(condition): + """Return condition from dict CONDITION_CLASSES.""" + for key, value in CONDITION_CLASSES.items(): + if condition in value: + return key + if isinstance(condition, Condition): + return condition.value + return condition + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Meteoclimatic weather platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([MeteoclimaticWeather(coordinator)], False) + + +class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, coordinator: DataUpdateCoordinator) -> None: + """Initialise the weather platform.""" + super().__init__(coordinator) + self._unique_id = self.coordinator.data["station"].code + self._name = self.coordinator.data["station"].name + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self._unique_id + + @property + def condition(self): + """Return the current condition.""" + return format_condition(self.coordinator.data["weather"].condition) + + @property + def temperature(self): + """Return the temperature.""" + return self.coordinator.data["weather"].temp_current + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def humidity(self): + """Return the humidity.""" + return self.coordinator.data["weather"].humidity_current + + @property + def pressure(self): + """Return the pressure.""" + return self.coordinator.data["weather"].pressure_current + + @property + def wind_speed(self): + """Return the wind speed.""" + return self.coordinator.data["weather"].wind_current + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self.coordinator.data["weather"].wind_bearing + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d927b244ede..79245491a7e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -150,6 +150,7 @@ FLOWS = [ "met", "met_eireann", "meteo_france", + "meteoclimatic", "metoffice", "mikrotik", "mill", diff --git a/requirements_all.txt b/requirements_all.txt index 0d41f2be892..e02fdf5d5ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1559,6 +1559,9 @@ pymediaroom==0.6.4.1 # homeassistant.components.melcloud pymelcloud==2.5.2 +# homeassistant.components.meteoclimatic +pymeteoclimatic==0.0.6 + # homeassistant.components.somfy pymfy==0.9.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b5de01177a..cb53e0c013d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -870,6 +870,9 @@ pymazda==0.1.5 # homeassistant.components.melcloud pymelcloud==2.5.2 +# homeassistant.components.meteoclimatic +pymeteoclimatic==0.0.6 + # homeassistant.components.somfy pymfy==0.9.3 diff --git a/tests/components/meteoclimatic/__init__.py b/tests/components/meteoclimatic/__init__.py new file mode 100644 index 00000000000..29ba2ef18c3 --- /dev/null +++ b/tests/components/meteoclimatic/__init__.py @@ -0,0 +1 @@ +"""Tests for the Meteoclimatic component.""" diff --git a/tests/components/meteoclimatic/conftest.py b/tests/components/meteoclimatic/conftest.py new file mode 100644 index 00000000000..964f67d6473 --- /dev/null +++ b/tests/components/meteoclimatic/conftest.py @@ -0,0 +1,13 @@ +"""Meteoclimatic generic test utils.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def patch_requests(): + """Stub out services that makes requests.""" + patch_client = patch("homeassistant.components.meteoclimatic.MeteoclimaticClient") + + with patch_client: + yield diff --git a/tests/components/meteoclimatic/test_config_flow.py b/tests/components/meteoclimatic/test_config_flow.py new file mode 100644 index 00000000000..e5daaea1978 --- /dev/null +++ b/tests/components/meteoclimatic/test_config_flow.py @@ -0,0 +1,88 @@ +"""Tests for the Meteoclimatic config flow.""" +from unittest.mock import patch + +from meteoclimatic.exceptions import MeteoclimaticError, StationNotFound +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.meteoclimatic.const import CONF_STATION_CODE, DOMAIN +from homeassistant.config_entries import SOURCE_USER + +TEST_STATION_CODE = "ESCAT4300000043206B" +TEST_STATION_NAME = "Reus (Tarragona)" + + +@pytest.fixture(name="client") +def mock_controller_client(): + """Mock a successful client.""" + with patch( + "homeassistant.components.meteoclimatic.config_flow.MeteoclimaticClient", + update=False, + ) as service_mock: + service_mock.return_value.get_data.return_value = { + "station_code": TEST_STATION_CODE + } + weather = service_mock.return_value.weather_at_station.return_value + weather.station.name = TEST_STATION_NAME + yield service_mock + + +@pytest.fixture(autouse=True) +def mock_setup(): + """Prevent setup.""" + with patch( + "homeassistant.components.meteoclimatic.async_setup_entry", + return_value=True, + ): + yield + + +async def test_user(hass, client): + """Test user config.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_STATION_CODE: TEST_STATION_CODE}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == TEST_STATION_CODE + assert result["title"] == TEST_STATION_NAME + assert result["data"][CONF_STATION_CODE] == TEST_STATION_CODE + + +async def test_not_found(hass): + """Test when we have the station code is not found.""" + with patch( + "homeassistant.components.meteoclimatic.config_flow.MeteoclimaticClient.weather_at_station", + side_effect=StationNotFound(TEST_STATION_CODE), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_STATION_CODE: TEST_STATION_CODE}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "not_found" + + +async def test_unknown_error(hass): + """Test when we have an unknown error fetching station data.""" + with patch( + "homeassistant.components.meteoclimatic.config_flow.MeteoclimaticClient.weather_at_station", + side_effect=MeteoclimaticError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_STATION_CODE: TEST_STATION_CODE}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" From d8ff52e55bf2f1595a5dc73ea43e83b1853266b0 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Tue, 25 May 2021 13:26:24 +0200 Subject: [PATCH 711/852] Add support for custom themes to use dark mode (#46532) --- homeassistant/components/frontend/__init__.py | 34 +++++++++++++++++-- tests/components/frontend/test_init.py | 28 +++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index ed339b9dc8b..1b104982026 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -36,6 +36,9 @@ mimetypes.add_type("application/javascript", ".js") DOMAIN = "frontend" CONF_THEMES = "themes" +CONF_THEMES_MODES = "modes" +CONF_THEMES_LIGHT = "light" +CONF_THEMES_DARK = "dark" CONF_EXTRA_HTML_URL = "extra_html_url" CONF_EXTRA_HTML_URL_ES5 = "extra_html_url_es5" CONF_EXTRA_MODULE_URL = "extra_module_url" @@ -66,14 +69,39 @@ PRIMARY_COLOR = "primary-color" _LOGGER = logging.getLogger(__name__) +EXTENDED_THEME_SCHEMA = vol.Schema( + { + # Theme variables that apply to all modes + cv.string: cv.string, + # Mode specific theme variables + vol.Optional(CONF_THEMES_MODES): vol.Schema( + { + vol.Optional(CONF_THEMES_LIGHT): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_THEMES_DARK): vol.Schema({cv.string: cv.string}), + } + ), + } +) + +THEME_SCHEMA = vol.Schema( + { + cv.string: ( + vol.Any( + # Legacy theme scheme + {cv.string: cv.string}, + # New extended schema with mode support + EXTENDED_THEME_SCHEMA, + ) + ) + } +) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Optional(CONF_FRONTEND_REPO): cv.isdir, - vol.Optional(CONF_THEMES): vol.Schema( - {cv.string: {cv.string: cv.string}} - ), + vol.Optional(CONF_THEMES): THEME_SCHEMA, vol.Optional(CONF_EXTRA_MODULE_URL): vol.All( cv.ensure_list, [cv.string] ), diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index fe624452475..9746fc6d838 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -26,6 +26,25 @@ from tests.common import async_capture_events, async_fire_time_changed MOCK_THEMES = { "happy": {"primary-color": "red", "app-header-background-color": "blue"}, "dark": {"primary-color": "black"}, + "light_only": { + "primary-color": "blue", + "modes": { + "light": {"secondary-color": "black"}, + }, + }, + "dark_only": { + "primary-color": "blue", + "modes": { + "dark": {"secondary-color": "white"}, + }, + }, + "light_and_dark": { + "primary-color": "blue", + "modes": { + "light": {"secondary-color": "black"}, + "dark": {"secondary-color": "white"}, + }, + }, } CONFIG_THEMES = {DOMAIN: {CONF_THEMES: MOCK_THEMES}} @@ -279,6 +298,15 @@ async def test_themes_set_dark_theme(hass, themes_ws_client): assert msg["result"]["default_dark_theme"] is None + await hass.services.async_call( + DOMAIN, "set_theme", {"name": "light_and_dark", "mode": "dark"}, blocking=True + ) + + await themes_ws_client.send_json({"id": 8, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() + + assert msg["result"]["default_dark_theme"] == "light_and_dark" + async def test_themes_set_dark_theme_wrong_name(hass, frontend, themes_ws_client): """Test frontend.set_theme service called with mode dark and wrong name.""" From be0a54edb13f121841bb380d420ac205887f9b99 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 25 May 2021 13:29:35 +0200 Subject: [PATCH 712/852] Add strict type annotations to bluetooth_tracker (#50999) --- .strict-typing | 1 + .../components/bluetooth_tracker/const.py | 10 ++- .../bluetooth_tracker/device_tracker.py | 80 ++++++++++++------- mypy.ini | 14 +++- script/hassfest/mypy_config.py | 1 - 5 files changed, 69 insertions(+), 37 deletions(-) diff --git a/.strict-typing b/.strict-typing index 050847891f2..a72118fa843 100644 --- a/.strict-typing +++ b/.strict-typing @@ -15,6 +15,7 @@ homeassistant.components.amazon_polly.* homeassistant.components.ampio.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* +homeassistant.components.bluetooth_tracker.* homeassistant.components.bond.* homeassistant.components.brother.* homeassistant.components.calendar.* diff --git a/homeassistant/components/bluetooth_tracker/const.py b/homeassistant/components/bluetooth_tracker/const.py index b481efa296f..8257e5554ec 100644 --- a/homeassistant/components/bluetooth_tracker/const.py +++ b/homeassistant/components/bluetooth_tracker/const.py @@ -1,3 +1,9 @@ """Constants for the Bluetooth Tracker component.""" -DOMAIN = "bluetooth_tracker" -SERVICE_UPDATE = "update" +from typing import Final + +DOMAIN: Final = "bluetooth_tracker" +SERVICE_UPDATE: Final = "update" + +BT_PREFIX: Final = "BT_" +CONF_REQUEST_RSSI: Final = "request_rssi" +DEFAULT_DEVICE_ID: Final = -1 diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 11037e2bc24..ca1da5987a4 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -2,13 +2,18 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable +from datetime import datetime, timedelta import logging +from typing import Any, Callable, Final import bluetooth # pylint: disable=import-error from bt_proximity import BluetoothRSSI import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, +) from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, @@ -18,24 +23,26 @@ from homeassistant.components.device_tracker.const import ( ) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, + Device, async_load_config, ) from homeassistant.const import CONF_DEVICE_ID -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, SERVICE_UPDATE +from .const import ( + BT_PREFIX, + CONF_REQUEST_RSSI, + DEFAULT_DEVICE_ID, + DOMAIN, + SERVICE_UPDATE, +) -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -BT_PREFIX = "BT_" - -CONF_REQUEST_RSSI = "request_rssi" - -DEFAULT_DEVICE_ID = -1 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TRACK_NEW): cv.boolean, vol.Optional(CONF_REQUEST_RSSI): cv.boolean, @@ -46,9 +53,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def is_bluetooth_device(device) -> bool: +def is_bluetooth_device(device: Device) -> bool: """Check whether a device is a bluetooth device by its mac.""" - return device.mac and device.mac[:3].upper() == BT_PREFIX + return device.mac is not None and device.mac[:3].upper() == BT_PREFIX def discover_devices(device_id: int) -> list[tuple[str, str]]: @@ -61,11 +68,15 @@ def discover_devices(device_id: int) -> list[tuple[str, str]]: device_id=device_id, ) _LOGGER.debug("Bluetooth devices discovered = %d", len(result)) - return result + return result # type: ignore[no-any-return] async def see_device( - hass: HomeAssistant, async_see, mac: str, device_name: str, rssi=None + hass: HomeAssistant, + async_see: Callable[..., Awaitable[None]], + mac: str, + device_name: str, + rssi: tuple[int] | None = None, ) -> None: """Mark a device as seen.""" attributes = {} @@ -88,14 +99,18 @@ async def get_tracking_devices(hass: HomeAssistant) -> tuple[set[str], set[str]] """ yaml_path: str = hass.config.path(YAML_DEVICES) - devices = await async_load_config(yaml_path, hass, 0) + devices = await async_load_config(yaml_path, hass, timedelta(0)) bluetooth_devices = [device for device in devices if is_bluetooth_device(device)] devices_to_track: set[str] = { - device.mac[3:] for device in bluetooth_devices if device.track + device.mac[3:] + for device in bluetooth_devices + if device.track and device.mac is not None } devices_to_not_track: set[str] = { - device.mac[3:] for device in bluetooth_devices if not device.track + device.mac[3:] + for device in bluetooth_devices + if not device.track and device.mac is not None } return devices_to_track, devices_to_not_track @@ -104,16 +119,19 @@ async def get_tracking_devices(hass: HomeAssistant) -> tuple[set[str], set[str]] def lookup_name(mac: str) -> str | None: """Lookup a Bluetooth device name.""" _LOGGER.debug("Scanning %s", mac) - return bluetooth.lookup_name(mac, timeout=5) + return bluetooth.lookup_name(mac, timeout=5) # type: ignore[no-any-return] async def async_setup_scanner( - hass: HomeAssistant, config: dict, async_see, discovery_info=None -): + hass: HomeAssistant, + config: ConfigType, + async_see: Callable[..., Awaitable[None]], + discovery_info: dict[str, Any] | None = None, +) -> bool: """Set up the Bluetooth Scanner.""" device_id: int = config[CONF_DEVICE_ID] - interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - request_rssi = config.get(CONF_REQUEST_RSSI, False) + interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + request_rssi: bool = config.get(CONF_REQUEST_RSSI, False) update_bluetooth_lock = asyncio.Lock() # If track new devices is true discover new devices on startup. @@ -128,21 +146,21 @@ async def async_setup_scanner( if request_rssi: _LOGGER.debug("Detecting RSSI for devices") - async def perform_bluetooth_update(): + async def perform_bluetooth_update() -> None: """Discover Bluetooth devices and update status.""" _LOGGER.debug("Performing Bluetooth devices discovery and update") - tasks = [] + tasks: list[Awaitable[None]] = [] try: if track_new: devices = await hass.async_add_executor_job(discover_devices, device_id) - for mac, device_name in devices: + 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: - device_name = await hass.async_add_executor_job(lookup_name, mac) - if device_name is None: + friendly_name = await hass.async_add_executor_job(lookup_name, mac) + if friendly_name is None: # Could not lookup device name continue @@ -152,7 +170,7 @@ async def async_setup_scanner( rssi = await hass.async_add_executor_job(client.request_rssi) client.close() - tasks.append(see_device(hass, async_see, mac, device_name, rssi)) + tasks.append(see_device(hass, async_see, mac, friendly_name, rssi)) if tasks: await asyncio.wait(tasks) @@ -160,7 +178,7 @@ async def async_setup_scanner( except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") - async def update_bluetooth(now=None): + async def update_bluetooth(now: datetime | None = None) -> None: """Lookup Bluetooth devices and update status.""" # If an update is in progress, we don't do anything if update_bluetooth_lock.locked(): @@ -173,7 +191,7 @@ async def async_setup_scanner( async with update_bluetooth_lock: await perform_bluetooth_update() - async def handle_manual_update_bluetooth(call): + async def handle_manual_update_bluetooth(call: ServiceCall) -> None: """Update bluetooth devices on demand.""" await update_bluetooth() diff --git a/mypy.ini b/mypy.ini index b02a4c055da..39b32b29994 100644 --- a/mypy.ini +++ b/mypy.ini @@ -176,6 +176,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.bluetooth_tracker.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.bond.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -845,9 +856,6 @@ ignore_errors = true [mypy-homeassistant.components.blueprint.*] ignore_errors = true -[mypy-homeassistant.components.bluetooth_tracker.*] -ignore_errors = true - [mypy-homeassistant.components.bmw_connected_drive.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f5337d8e5ed..6310d0117c5 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -28,7 +28,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.azure_devops.*", "homeassistant.components.azure_event_hub.*", "homeassistant.components.blueprint.*", - "homeassistant.components.bluetooth_tracker.*", "homeassistant.components.bmw_connected_drive.*", "homeassistant.components.bsblan.*", "homeassistant.components.cast.*", From e9c787a5eb203c137198384bc8dc73c782611986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 25 May 2021 13:35:18 +0200 Subject: [PATCH 713/852] Use entity class vars in Tibber (#50977) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use entity class vars in Tibber Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tibber/sensor.py Co-authored-by: Franck Nijhof * Update homeassistant/components/tibber/sensor.py Co-authored-by: Franck Nijhof * Use entity class vars in Tibber Signed-off-by: Daniel Hjelseth Høyer * Use entity class vars in Tibber Signed-off-by: Daniel Hjelseth Høyer Co-authored-by: Franck Nijhof --- homeassistant/components/tibber/sensor.py | 137 +++++++--------------- 1 file changed, 42 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 4a706a6a517..58e27b6b653 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -147,23 +147,12 @@ class TibberSensor(SensorEntity): def __init__(self, tibber_home): """Initialize the sensor.""" self._tibber_home = tibber_home - self._state = None - - self._name = tibber_home.info["viewer"]["home"]["appNickname"] - if self._name is None: - self._name = tibber_home.info["viewer"]["home"]["address"].get( + self._home_name = tibber_home.info["viewer"]["home"]["appNickname"] + if self._home_name is None: + self._home_name = tibber_home.info["viewer"]["home"]["address"].get( "address1", "" ) - - @property - def model(self): - """Return the model of the sensor.""" - return None - - @property - def state(self): - """Return the state of the device.""" - return self._state + self._model = None @property def device_id(self): @@ -178,8 +167,8 @@ class TibberSensor(SensorEntity): "name": self.name, "manufacturer": MANUFACTURER, } - if self.model is not None: - device_info["model"] = self.model + if self._model is not None: + device_info["model"] = self._model return device_info @@ -190,14 +179,25 @@ class TibberSensorElPrice(TibberSensor): """Initialize the sensor.""" super().__init__(tibber_home) self._last_updated = None - self._is_available = False - self._extra_state_attributes = {} self._spread_load_constant = randrange(5000) - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._extra_state_attributes + self._attr_available = False + self._attr_extra_state_attributes = { + "app_nickname": None, + "grid_company": None, + "estimated_annual_consumption": None, + "price_level": None, + "max_price": None, + "avg_price": None, + "min_price": None, + "off_peak_1": None, + "peak": None, + "off_peak_2": None, + } + self._attr_icon = ICON + self._attr_name = f"Electricity price {self._home_name}" + self._attr_unique_id = f"{self._tibber_home.home_id}" + self._model = "Price Sensor" async def async_update(self): """Get the latest data and updates the states.""" @@ -206,7 +206,7 @@ class TibberSensorElPrice(TibberSensor): not self._tibber_home.last_data_timestamp or (self._tibber_home.last_data_timestamp - now).total_seconds() < 5 * 3600 + self._spread_load_constant - or not self._is_available + or not self.available ): _LOGGER.debug("Asking for new data") await self._fetch_data() @@ -220,42 +220,13 @@ class TibberSensorElPrice(TibberSensor): return res = self._tibber_home.current_price_data() - self._state, price_level, self._last_updated = res - self._extra_state_attributes["price_level"] = price_level + self._attr_state, price_level, self._last_updated = res + self._attr_extra_state_attributes["price_level"] = price_level attrs = self._tibber_home.current_attributes() - self._extra_state_attributes.update(attrs) - self._is_available = self._state is not None - - @property - def available(self): - """Return True if entity is available.""" - return self._is_available - - @property - def name(self): - """Return the name of the sensor.""" - return f"Electricity price {self._name}" - - @property - def model(self): - """Return the model of the sensor.""" - return "Price Sensor" - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return ICON - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return self._tibber_home.price_unit - - @property - def unique_id(self): - """Return a unique ID.""" - return self.device_id + self._attr_extra_state_attributes.update(attrs) + self._attr_available = self._attr_state is not None + self._attr_unit_of_measurement = self._tibber_home.price_unit @Throttle(MIN_TIME_BETWEEN_UPDATES) async def _fetch_data(self): @@ -265,11 +236,11 @@ class TibberSensorElPrice(TibberSensor): except (asyncio.TimeoutError, aiohttp.ClientError): return data = self._tibber_home.info["viewer"]["home"] - self._extra_state_attributes["app_nickname"] = data["appNickname"] - self._extra_state_attributes["grid_company"] = data["meteringPointData"][ + self._attr_extra_state_attributes["app_nickname"] = data["appNickname"] + self._attr_extra_state_attributes["grid_company"] = data["meteringPointData"][ "gridCompany" ] - self._extra_state_attributes["estimated_annual_consumption"] = data[ + self._attr_extra_state_attributes["estimated_annual_consumption"] = data[ "meteringPointData" ]["estimatedAnnualConsumption"] @@ -277,13 +248,19 @@ class TibberSensorElPrice(TibberSensor): class TibberSensorRT(TibberSensor): """Representation of a Tibber sensor for real time consumption.""" + _attr_should_poll = False + def __init__(self, tibber_home, sensor_name, device_class, unit, initial_state): """Initialize the sensor.""" super().__init__(tibber_home) self._sensor_name = sensor_name - self._device_class = device_class - self._unit = unit - self._state = initial_state + self._model = "Tibber Pulse" + + self._attr_device_class = device_class + self._attr_name = f"{self._sensor_name} {self._home_name}" + self._attr_state = initial_state + self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{self._sensor_name}" + self._attr_unit_of_measurement = unit async def async_added_to_hass(self): """Start listen for real time data.""" @@ -300,42 +277,12 @@ class TibberSensorRT(TibberSensor): """Return True if entity is available.""" return self._tibber_home.rt_subscription_running - @property - def model(self): - """Return the model of the sensor.""" - return "Tibber Pulse" - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._sensor_name} {self._name}" - @callback def _set_state(self, state): """Set sensor state.""" - self._state = state + self._attr_state = state self.async_write_ha_state() - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return self._unit - - @property - def unique_id(self): - """Return a unique ID.""" - return f"{self.device_id}_rt_{self._sensor_name}" - - @property - def device_class(self): - """Return the device class of the sensor.""" - return self._device_class - class TibberRtDataHandler: """Handle Tibber realtime data.""" From bdb8cdf717e786df9708191248cb4e6aabb97649 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 25 May 2021 13:40:22 +0200 Subject: [PATCH 714/852] Fix zwave_js None color value (#50926) --- .coveragerc | 1 - homeassistant/components/zwave_js/light.py | 9 +- tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_light.py | 11 + .../light_color_null_values_state.json | 682 ++++++++++++++++++ 5 files changed, 712 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/zwave_js/light_color_null_values_state.json diff --git a/.coveragerc b/.coveragerc index 3399a10a8b0..c3a56fa27c0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1236,7 +1236,6 @@ omit = homeassistant/components/supla/* homeassistant/components/zwave/util.py homeassistant/components/zwave_js/discovery.py - homeassistant/components/zwave_js/light.py homeassistant/components/zwave_js/sensor.py [report] diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 074332daaed..a1ab78e6ee3 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -388,10 +388,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): green = multi_color.get("green", green_val.value) blue = multi_color.get("blue", blue_val.value) self._supports_color = True - # convert to HS - self._hs_color = color_util.color_RGB_to_hs(red, green, blue) - # Light supports color, set color mode to hs - self._color_mode = COLOR_MODE_HS + if None not in (red, green, blue): + # convert to HS + self._hs_color = color_util.color_RGB_to_hs(red, green, blue) + # Light supports color, set color mode to hs + self._color_mode = COLOR_MODE_HS # color temperature support if ww_val and cw_val: diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index ffb9f13698f..eef79b533d3 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -184,6 +184,12 @@ def bulb_6_multi_color_state_fixture(): return json.loads(load_fixture("zwave_js/bulb_6_multi_color_state.json")) +@pytest.fixture(name="light_color_null_values_state", scope="session") +def light_color_null_values_state_fixture(): + """Load the light color null values node state fixture data.""" + return json.loads(load_fixture("zwave_js/light_color_null_values_state.json")) + + @pytest.fixture(name="eaton_rf9640_dimmer_state", scope="session") def eaton_rf9640_dimmer_state_fixture(): """Load the eaton rf9640 dimmer node state fixture data.""" @@ -429,6 +435,14 @@ def bulb_6_multi_color_fixture(client, bulb_6_multi_color_state): return node +@pytest.fixture(name="light_color_null_values") +def light_color_null_values_fixture(client, light_color_null_values_state): + """Mock a node with current color value item being null.""" + node = Node(client, copy.deepcopy(light_color_null_values_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="eaton_rf9640_dimmer") def eaton_rf9640_dimmer_fixture(client, eaton_rf9640_dimmer_state): """Mock a Eaton RF9640 (V4 compatible) dimmer node.""" diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index 3a99c4f7a23..2d9cf06b095 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -486,3 +486,14 @@ async def test_rgbw_light(hass, client, zen_31, integration): assert args["value"] == 255 client.async_send_command.reset_mock() + + +async def test_light_none_color_value(hass, light_color_null_values, integration): + """Test the light entity can handle None value in current color Value.""" + entity_id = "light.repeater" + state = hass.states.get(entity_id) + + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TRANSITION + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] diff --git a/tests/fixtures/zwave_js/light_color_null_values_state.json b/tests/fixtures/zwave_js/light_color_null_values_state.json new file mode 100644 index 00000000000..46bc9f29b06 --- /dev/null +++ b/tests/fixtures/zwave_js/light_color_null_values_state.json @@ -0,0 +1,682 @@ +{ + "nodeId": 39, + "index": 0, + "installerIcon": 6912, + "userIcon": 6912, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": "unknown", + "manufacturerId": 134, + "productId": 117, + "productType": 4, + "firmwareVersion": "1.5", + "zwavePlusVersion": 1, + "name": "Repeater", + "location": "Dining Room", + "deviceConfig": { + "filename": "/usr/src/app/node_modules/@zwave-js/config/config/devices/0x0086/zw117.json", + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW117", + "description": "Range Extender 6", + "devices": [ + { + "productType": 4, + "productId": 117 + }, + { + "productType": 260, + "productId": 117 + }, + { + "productType": 516, + "productId": 117 + }, + { + "productType": 2564, + "productId": 117 + }, + { + "productType": 7172, + "productId": 117 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "Turn the primary controller of Z-Wave network into inclusion mode, press the Z-Wave Button on Range Extender 6", + "exclusion": "Turn the primary controller of Z-Wave network into exclusion mode, press the Z-Wave Button on Range Extender 6", + "reset": "Press and hold the Z-Wave Button for 20 seconds and then release it.\nUse this procedure only in the event that your primary network controller is missing or inoperable", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2520/Range%20Extender%206%20manual.pdf" + }, + "isEmbedded": true + }, + "label": "ZW117", + "neighbors": [4, 17, 28, 29], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 39, + "index": 0, + "installerIcon": 6912, + "userIcon": 6912, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 15, + "label": "Repeater Slave" + }, + "specific": { + "key": 1, + "label": "Repeater Slave" + }, + "mandatorySupportedCCs": [32], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Remaining duration" + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 2, + "propertyName": "currentColor", + "propertyKeyName": "Red", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Red color.", + "label": "Current value (Red)", + "min": 0, + "max": 255 + }, + "value": null + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 3, + "propertyName": "currentColor", + "propertyKeyName": "Green", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Green color.", + "label": "Current value (Green)", + "min": 0, + "max": 255 + }, + "value": null + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 4, + "propertyName": "currentColor", + "propertyKeyName": "Blue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Blue color.", + "label": "Current value (Blue)", + "min": 0, + "max": 255 + }, + "value": null + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyName": "currentColor", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Current Color" + }, + "value": { + "red": null, + "green": null, + "blue": null + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "hexColor", + "propertyName": "hexColor", + "ccVersion": 1, + "metadata": { + "type": "color", + "readable": true, + "writeable": true, + "label": "RGB Color", + "minLength": 6, + "maxLength": 7 + }, + "value": "00ff00" + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 2, + "propertyName": "targetColor", + "propertyKeyName": "Red", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Red color.", + "label": "Target value (Red)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 3, + "propertyName": "targetColor", + "propertyKeyName": "Green", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Green color.", + "label": "Target value (Green)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 4, + "propertyName": "targetColor", + "propertyKeyName": "Blue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Blue color.", + "label": "Target value (Blue)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Target Color" + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 82, + "propertyName": "LED Indicator", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "LED Indicator", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "On for 2 seconds", + "1": "Disable" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 200, + "propertyName": "Partner ID", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Partner ID", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Aeotec", + "1": "Other" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 252, + "propertyName": "Lock Configuration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock Configuration", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 254, + "propertyName": "Device Tag", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Device Tag", + "default": 0, + "min": 0, + "max": 65535, + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 255, + "propertyName": "Reset to Factory Default Setting", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Reset to Factory Default Setting", + "default": 0, + "min": 1, + "max": 1431655765, + "states": { + "1": "Resets all configuration parameters to default setting", + "1431655765": "Reset the product to factory default setting and exclude from Z-Wave network" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 134 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 117 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.54" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["1.5"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ], + "interviewStage": 6, + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 15, + "label": "Repeater Slave" + }, + "specific": { + "key": 1, + "label": "Repeater Slave" + }, + "mandatorySupportedCCs": [32], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 2, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + } + ] + } From c868353459d848ba87377105a4d3b947a7388812 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Tue, 25 May 2021 14:17:36 +0200 Subject: [PATCH 715/852] Code cleanup in LCN (#48074) --- homeassistant/components/lcn/__init__.py | 4 +- homeassistant/components/lcn/config_flow.py | 5 +-- homeassistant/components/lcn/const.py | 2 + homeassistant/components/lcn/helpers.py | 44 +++------------------ homeassistant/components/lcn/services.py | 15 ++----- homeassistant/components/lcn/services.yaml | 2 +- 6 files changed, 15 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index faf524f6585..75fd91c28f5 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -15,13 +15,11 @@ from homeassistant.const import ( from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import Entity -from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, CONNECTION, DOMAIN +from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, CONNECTION, DOMAIN, PLATFORMS from .helpers import generate_unique_id, import_lcn_config from .schemas import CONFIG_SCHEMA # noqa: F401 from .services import SERVICES -PLATFORMS = ["binary_sensor", "climate", "cover", "light", "scene", "sensor", "switch"] - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py index 0d6216bf1e1..698a4dcedfe 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -89,7 +89,4 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry(entry, data=data) return self.async_abort(reason="existing_configuration_updated") - return self.async_create_entry( - title=f"{host_name}", - data=data, - ) + return self.async_create_entry(title=f"{host_name}", data=data) diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index 4e3e765ace0..3458c78f853 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -10,6 +10,8 @@ from homeassistant.const import ( VOLT, ) +PLATFORMS = ["binary_sensor", "climate", "cover", "light", "scene", "sensor", "switch"] + DOMAIN = "lcn" DATA_LCN = "lcn" DEFAULT_NAME = "pchk" diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 3f93ec95a69..0687491b052 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -224,44 +224,12 @@ def is_address(value): addr = (int(matcher.group("seg_id")), int(matcher.group("id")), is_group) conn_id = matcher.group("conn_id") return addr, conn_id - raise vol.error.Invalid("Not a valid address string.") + raise ValueError(f"{value} is not a valid address string") -def is_relays_states_string(states_string): +def is_states_string(states_string): """Validate the given states string and return states list.""" - if len(states_string) == 8: - states = [] - for state_string in states_string: - if state_string == "1": - state = "ON" - elif state_string == "0": - state = "OFF" - elif state_string == "T": - state = "TOGGLE" - elif state_string == "-": - state = "NOCHANGE" - else: - raise vol.error.Invalid("Not a valid relay state string.") - states.append(state) - return states - raise vol.error.Invalid("Wrong length of relay state string.") - - -def is_key_lock_states_string(states_string): - """Validate the given states string and returns states list.""" - if len(states_string) == 8: - states = [] - for state_string in states_string: - if state_string == "1": - state = "ON" - elif state_string == "0": - state = "OFF" - elif state_string == "T": - state = "TOGGLE" - elif state_string == "-": - state = "NOCHANGE" - else: - raise vol.error.Invalid("Not a valid key lock state string.") - states.append(state) - return states - raise vol.error.Invalid("Wrong length of key lock state string.") + if len(states_string) != 8: + raise ValueError("Invalid length of states string") + states = {"1": "ON", "0": "OFF", "T": "TOGGLE", "-": "NOCHANGE"} + return [states[state_string] for state_string in states_string] diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index c6c33270264..fa74f556593 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -40,12 +40,7 @@ from .const import ( VAR_UNITS, VARIABLES, ) -from .helpers import ( - get_device_connection, - is_address, - is_key_lock_states_string, - is_relays_states_string, -) +from .helpers import get_device_connection, is_address, is_states_string class LcnServiceCall: @@ -150,9 +145,7 @@ class OutputToggle(LcnServiceCall): class Relays(LcnServiceCall): """Set the relays status.""" - schema = LcnServiceCall.schema.extend( - {vol.Required(CONF_STATE): is_relays_states_string} - ) + schema = LcnServiceCall.schema.extend({vol.Required(CONF_STATE): is_states_string}) async def async_call_service(self, service): """Execute service call.""" @@ -330,7 +323,7 @@ class LockKeys(LcnServiceCall): vol.Optional(CONF_TABLE, default="a"): vol.All( vol.Upper, cv.matches_regex(r"^[A-D]$") ), - vol.Required(CONF_STATE): is_key_lock_states_string, + vol.Required(CONF_STATE): is_states_string, vol.Optional(CONF_TIME, default=0): cv.positive_int, vol.Optional(CONF_TIME_UNIT, default=TIME_SECONDS): vol.All( vol.Upper, vol.In(TIME_UNITS) @@ -361,7 +354,7 @@ class LockKeys(LcnServiceCall): else: await device_connection.lock_keys(table_id, states) - handler = device_connection.status_request_handler + handler = device_connection.status_requests_handler await handler.request_status_locked_keys_timeout() diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml index 8ca4a6f3d4e..299e1df5777 100644 --- a/homeassistant/components/lcn/services.yaml +++ b/homeassistant/components/lcn/services.yaml @@ -521,7 +521,7 @@ lock_keys: table: name: Table description: "Table with keys to lock (must be A for interval)." - example: "a5" + example: "a" default: a selector: text: From 9319fc62638814a28e2e18a8f6ad2b31cdfaa144 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 25 May 2021 14:37:34 +0200 Subject: [PATCH 716/852] Update zwave_js stored add-on options (#51063) * Update zwave_js entry data if add-on data changed * Fix tests * Add test --- homeassistant/components/zwave_js/__init__.py | 13 +++++ tests/components/zwave_js/test_init.py | 50 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index bac6433a5e2..520495d5071 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -51,6 +51,8 @@ from .const import ( ATTR_TYPE, ATTR_VALUE, ATTR_VALUE_RAW, + CONF_ADDON_DEVICE, + CONF_ADDON_NETWORK_KEY, CONF_DATA_COLLECTION_OPTED_IN, CONF_INTEGRATION_CREATED_ADDON, CONF_NETWORK_KEY, @@ -580,6 +582,17 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> ) raise ConfigEntryNotReady + addon_options = addon_info.options + addon_device = addon_options[CONF_ADDON_DEVICE] + addon_network_key = addon_options[CONF_ADDON_NETWORK_KEY] + updates = {} + if usb_path != addon_device: + updates[CONF_USB_PATH] = addon_device + if network_key != addon_network_key: + updates[CONF_NETWORK_KEY] = addon_network_key + if updates: + hass.config_entries.async_update_entry(entry, data={**entry.data, **updates}) + @callback def async_ensure_addon_updated(hass: HomeAssistant) -> None: diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index c4f35941a25..65421eba604 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -654,6 +654,48 @@ async def test_addon_info_failure( assert start_addon.call_count == 0 +@pytest.mark.parametrize( + "old_device, new_device, old_network_key, new_network_key", + [("/old_test", "/new_test", "old123", "new123")], +) +async def test_addon_options_changed( + hass, + client, + addon_installed, + addon_running, + install_addon, + addon_options, + start_addon, + old_device, + new_device, + old_network_key, + new_network_key, +): + """Test update config entry data on entry setup if add-on options changed.""" + addon_options["device"] = new_device + addon_options["network_key"] = new_network_key + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + data={ + "url": "ws://host1:3001", + "use_addon": True, + "usb_path": old_device, + "network_key": old_network_key, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert entry.data["usb_path"] == new_device + assert entry.data["network_key"] == new_network_key + assert install_addon.call_count == 0 + assert start_addon.call_count == 0 + + @pytest.mark.parametrize( "addon_version, update_available, update_calls, snapshot_calls, " "update_addon_side_effect, create_shapshot_side_effect", @@ -681,13 +723,15 @@ async def test_update_addon( create_shapshot_side_effect, ): """Test update the Z-Wave JS add-on during entry setup.""" + device = "/test" + network_key = "abc123" + addon_options["device"] = device + addon_options["network_key"] = network_key addon_info.return_value["version"] = addon_version addon_info.return_value["update_available"] = update_available create_shapshot.side_effect = create_shapshot_side_effect update_addon.side_effect = update_addon_side_effect client.connect.side_effect = InvalidServerVersion("Invalid version") - device = "/test" - network_key = "abc123" entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", @@ -729,6 +773,8 @@ async def test_stop_addon( stop_addon.side_effect = stop_addon_side_effect device = "/test" network_key = "abc123" + addon_options["device"] = device + addon_options["network_key"] = network_key entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", From 028a07d86fd56bf101af4717e050750bb5fa6ae0 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 25 May 2021 08:45:17 -0400 Subject: [PATCH 717/852] Wrap up selectors (#50794) Co-authored-by: Franck Nijhof --- homeassistant/components/abode/services.yaml | 2 - .../components/adguard/services.yaml | 3 +- homeassistant/components/ads/services.yaml | 2 - .../components/advantage_air/services.yaml | 1 - .../components/aftership/services.yaml | 4 +- .../components/amcrest/services.yaml | 9 +- .../components/androidtv/services.yaml | 3 - .../components/automation/services.yaml | 2 - .../components/bluesound/services.yaml | 5 -- homeassistant/components/camera/services.yaml | 8 -- homeassistant/components/cast/services.yaml | 1 - .../components/channels/services.yaml | 1 - .../components/climate/services.yaml | 9 -- .../components/counter/services.yaml | 5 -- homeassistant/components/cover/services.yaml | 6 -- homeassistant/components/deconz/services.yaml | 31 ++++--- .../components/denonavr/services.yaml | 1 - .../components/device_tracker/services.yaml | 3 +- .../components/dynalite/services.yaml | 4 - homeassistant/components/dyson/services.yaml | 7 -- homeassistant/components/ecobee/services.yaml | 13 --- .../components/eight_sleep/services.yaml | 3 - homeassistant/components/elkm1/services.yaml | 5 -- .../components/envisalink/services.yaml | 2 - .../components/evohome/services.yaml | 2 - .../components/facebox/services.yaml | 1 - homeassistant/components/fan/services.yaml | 16 +--- homeassistant/components/ffmpeg/services.yaml | 3 - homeassistant/components/flo/services.yaml | 2 - homeassistant/components/foscam/services.yaml | 2 - .../components/frontend/services.yaml | 3 +- .../components/geniushub/services.yaml | 4 - homeassistant/components/group/services.yaml | 1 - .../components/harmony/services.yaml | 1 - .../components/hdmi_cec/services.yaml | 3 - homeassistant/components/hive/services.yaml | 7 -- .../components/homematic/services.yaml | 5 -- .../homematicip_cloud/services.yaml | 11 +-- .../components/humidifier/services.yaml | 1 - homeassistant/components/icloud/services.yaml | 1 - homeassistant/components/ifttt/services.yaml | 1 - homeassistant/components/ihc/services.yaml | 19 +--- .../components/input_datetime/services.yaml | 1 - .../components/input_number/services.yaml | 1 - .../components/input_select/services.yaml | 2 - .../components/insteon/services.yaml | 10 --- homeassistant/components/isy994/services.yaml | 15 +--- homeassistant/components/izone/services.yaml | 8 +- homeassistant/components/keba/services.yaml | 28 +++--- homeassistant/components/kef/services.yaml | 20 +---- homeassistant/components/knx/services.yaml | 1 - homeassistant/components/lcn/services.yaml | 42 +++------ homeassistant/components/lifx/services.yaml | 13 --- homeassistant/components/light/services.yaml | 90 ++++--------------- .../components/litterrobot/services.yaml | 6 -- .../components/local_file/services.yaml | 1 - homeassistant/components/lock/services.yaml | 6 -- .../components/logbook/services.yaml | 1 - homeassistant/components/logger/services.yaml | 5 -- .../components/logi_circle/services.yaml | 6 -- .../components/media_extractor/services.yaml | 1 - .../components/media_player/services.yaml | 6 -- homeassistant/components/mill/services.yaml | 3 - homeassistant/components/modbus/services.yaml | 4 - .../components/motion_blinds/services.yaml | 2 - homeassistant/components/mqtt/services.yaml | 9 -- .../components/mysensors/services.yaml | 1 - homeassistant/components/neato/services.yaml | 3 - .../components/ness_alarm/services.yaml | 2 - homeassistant/components/nest/services.yaml | 3 - .../components/netatmo/services.yaml | 5 +- .../components/netgear_lte/services.yaml | 2 - homeassistant/components/nexia/services.yaml | 2 - homeassistant/components/notify/services.yaml | 8 +- homeassistant/components/nuki/services.yaml | 3 +- homeassistant/components/nx584/services.yaml | 2 - homeassistant/components/nzbget/services.yaml | 1 - homeassistant/components/ombi/services.yaml | 1 - .../components/omnilogic/services.yaml | 1 - homeassistant/components/onvif/services.yaml | 7 -- .../components/openhome/services.yaml | 1 - .../components/opentherm_gw/services.yaml | 20 ++--- homeassistant/components/ozw/services.yaml | 5 -- .../components/profiler/services.yaml | 3 - homeassistant/components/ps4/services.yaml | 2 - homeassistant/components/rachio/services.yaml | 4 - .../components/rainbird/services.yaml | 7 +- .../components/rainmachine/services.yaml | 15 ++-- .../components/recorder/services.yaml | 5 -- homeassistant/components/remote/services.yaml | 31 +++---- homeassistant/components/scene/services.yaml | 10 +-- .../components/screenlogic/services.yaml | 33 ++++--- .../components/sensibo/services.yaml | 1 - .../components/simplisafe/services.yaml | 13 +-- .../components/snapcast/services.yaml | 3 +- homeassistant/components/snips/services.yaml | 1 - homeassistant/components/sonos/services.yaml | 19 ---- .../components/starline/services.yaml | 2 - .../components/surepetcare/services.yaml | 1 - .../components/switcher_kis/services.yaml | 1 - .../components/system_bridge/services.yaml | 2 - .../components/system_log/services.yaml | 3 +- homeassistant/components/tado/services.yaml | 3 - .../components/telegram_bot/services.yaml | 15 ++-- .../components/todoist/services.yaml | 1 - homeassistant/components/tts/services.yaml | 4 +- .../components/wake_on_lan/services.yaml | 2 +- .../components/webostv/services.yaml | 1 - homeassistant/components/wink/services.yaml | 7 +- homeassistant/components/wled/services.yaml | 10 +-- .../components/yeelight/services.yaml | 5 +- homeassistant/components/zha/services.yaml | 36 +++----- homeassistant/components/zwave/services.yaml | 7 +- .../components/zwave_js/services.yaml | 3 - 114 files changed, 183 insertions(+), 628 deletions(-) diff --git a/homeassistant/components/abode/services.yaml b/homeassistant/components/abode/services.yaml index 9b5362c0929..843cc123c69 100644 --- a/homeassistant/components/abode/services.yaml +++ b/homeassistant/components/abode/services.yaml @@ -6,7 +6,6 @@ capture_image: name: Entity description: Entity id of the camera to request an image. required: true - example: camera.downstairs_motion_camera selector: entity: integration: abode @@ -39,7 +38,6 @@ trigger_automation: name: Entity description: Entity id of the automation to trigger. required: true - example: switch.my_automation selector: entity: integration: abode diff --git a/homeassistant/components/adguard/services.yaml b/homeassistant/components/adguard/services.yaml index 2e97d164e3a..5e4c2a157de 100644 --- a/homeassistant/components/adguard/services.yaml +++ b/homeassistant/components/adguard/services.yaml @@ -59,8 +59,7 @@ refresh: fields: force: name: Force - description: Force update (by passes AdGuard Home throttling). - example: '"true" to force, "false" or omit for a regular refresh.' + description: Force update (bypasses AdGuard Home throttling). "true" to force, or "false" to omit for a regular refresh. default: false selector: boolean: diff --git a/homeassistant/components/ads/services.yaml b/homeassistant/components/ads/services.yaml index 5139662a522..f6458029fb4 100644 --- a/homeassistant/components/ads/services.yaml +++ b/homeassistant/components/ads/services.yaml @@ -15,7 +15,6 @@ write_data_by_name: name: ADS type description: The data type of the variable to write to. required: true - example: "int" selector: select: options: @@ -29,7 +28,6 @@ write_data_by_name: name: Value description: The value to write to the variable. required: true - example: 1 selector: number: min: 0 diff --git a/homeassistant/components/advantage_air/services.yaml b/homeassistant/components/advantage_air/services.yaml index 24088421c99..33f39065f8a 100644 --- a/homeassistant/components/advantage_air/services.yaml +++ b/homeassistant/components/advantage_air/services.yaml @@ -10,7 +10,6 @@ set_time_to: name: Minutes description: Minutes until action required: true - example: "60" selector: number: min: 0 diff --git a/homeassistant/components/aftership/services.yaml b/homeassistant/components/aftership/services.yaml index e4d90646aa6..62e339dbda8 100644 --- a/homeassistant/components/aftership/services.yaml +++ b/homeassistant/components/aftership/services.yaml @@ -2,7 +2,7 @@ add_tracking: name: Add tracking - description: Add new tracking to Aftership. + description: Add new tracking number to Aftership. fields: tracking_number: name: Tracking number @@ -26,7 +26,7 @@ add_tracking: remove_tracking: name: Remove tracking - description: Remove a tracking from Aftership. + description: Remove a tracking number from Aftership. fields: tracking_number: name: Tracking number diff --git a/homeassistant/components/amcrest/services.yaml b/homeassistant/components/amcrest/services.yaml index c4a12c59828..7baad96d6d5 100644 --- a/homeassistant/components/amcrest/services.yaml +++ b/homeassistant/components/amcrest/services.yaml @@ -70,12 +70,14 @@ goto_preset: fields: entity_id: description: "Name(s) of the cameras, or 'all' for all cameras." - example: "camera.house_front" + selector: + entity: + integration: amcrest + domain: camera preset: name: Preset description: Preset number. required: true - example: 1 selector: number: min: 1 @@ -94,7 +96,6 @@ set_color_bw: color_bw: name: Color description: Color mode. - example: auto selector: select: options: @@ -138,7 +139,6 @@ ptz_control: name: Movement description: "Direction to move the camera." required: true - example: "right" selector: select: options: @@ -155,7 +155,6 @@ ptz_control: travel_time: name: Travel time description: "Travel time in fractional seconds: from 0 to 1." - example: ".5" default: .2 selector: number: diff --git a/homeassistant/components/androidtv/services.yaml b/homeassistant/components/androidtv/services.yaml index 55b871ff58f..6c8469f46c0 100644 --- a/homeassistant/components/androidtv/services.yaml +++ b/homeassistant/components/androidtv/services.yaml @@ -7,7 +7,6 @@ adb_command: entity_id: description: Name(s) of Android TV / Fire TV entities. required: true - example: "media_player.android_tv_living_room" selector: entity: integration: androidtv @@ -26,7 +25,6 @@ download: entity_id: description: Name of Android TV / Fire TV entity. required: true - example: "media_player.android_tv_living_room" selector: entity: integration: androidtv @@ -52,7 +50,6 @@ upload: entity_id: description: Name(s) of Android TV / Fire TV entities. required: true - example: "media_player.android_tv_living_room" selector: entity: integration: androidtv diff --git a/homeassistant/components/automation/services.yaml b/homeassistant/components/automation/services.yaml index dec5793d1e7..62d0988d770 100644 --- a/homeassistant/components/automation/services.yaml +++ b/homeassistant/components/automation/services.yaml @@ -17,7 +17,6 @@ turn_off: name: Stop actions description: Stop currently running actions. default: true - example: true selector: boolean: @@ -39,7 +38,6 @@ trigger: name: Skip conditions description: Whether or not the conditions will be skipped. default: true - example: true selector: boolean: diff --git a/homeassistant/components/bluesound/services.yaml b/homeassistant/components/bluesound/services.yaml index 992fd34a0bc..7c04cc00f39 100644 --- a/homeassistant/components/bluesound/services.yaml +++ b/homeassistant/components/bluesound/services.yaml @@ -6,7 +6,6 @@ join: name: Master description: Entity ID of the player that should become the master of the group. required: true - example: "media_player.bluesound_livingroom" selector: entity: integration: bluesound @@ -14,7 +13,6 @@ join: entity_id: name: Entity description: Name of entity that will coordinate the grouping. Platform dependent. - example: "media_player.bluesound_livingroom" selector: entity: integration: bluesound @@ -27,7 +25,6 @@ unjoin: entity_id: name: Entity description: Name of entity that will be unjoined from their group. Platform dependent. - example: "media_player.bluesound_livingroom" selector: entity: integration: bluesound @@ -40,7 +37,6 @@ set_sleep_timer: entity_id: name: Entity description: Name(s) of entities that will have a timer set. - example: "media_player.bluesound_livingroom" selector: entity: integration: bluesound @@ -53,7 +49,6 @@ clear_sleep_timer: entity_id: name: Entity description: Name(s) of entities that will have the timer cleared. - example: "media_player.bluesound_livingroom" selector: entity: integration: bluesound diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index 61b6e624b12..024bb927508 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -54,7 +54,6 @@ play_stream: name: Media Player description: Name(s) of media player to stream to. required: true - example: "media_player.living_room_tv" selector: entity: domain: media_player @@ -62,7 +61,6 @@ play_stream: name: Format description: Stream format supported by media player. default: "hls" - example: "hls" selector: select: options: @@ -86,25 +84,19 @@ record: name: Duration description: Target recording length. default: 30 - example: 30 selector: number: min: 1 max: 3600 - step: 1 unit_of_measurement: seconds - mode: slider lookback: name: Lookback description: Target lookback period to include in addition to duration. Only available if there is currently an active HLS stream. default: 0 - example: 4 selector: number: min: 0 max: 300 - step: 1 unit_of_measurement: seconds - mode: slider diff --git a/homeassistant/components/cast/services.yaml b/homeassistant/components/cast/services.yaml index 9b2b0a739b0..f0fbcf4a8d7 100644 --- a/homeassistant/components/cast/services.yaml +++ b/homeassistant/components/cast/services.yaml @@ -6,7 +6,6 @@ show_lovelace_view: name: Entity description: Media Player entity to show the Lovelace view on. required: true - example: "media_player.kitchen" selector: entity: integration: cast diff --git a/homeassistant/components/channels/services.yaml b/homeassistant/components/channels/services.yaml index f5b4639817e..5aa2f1ebda7 100644 --- a/homeassistant/components/channels/services.yaml +++ b/homeassistant/components/channels/services.yaml @@ -26,7 +26,6 @@ seek_by: name: Seconds description: Number of seconds to seek by. Negative numbers seek backwards. required: true - example: 120 selector: number: min: -3600 diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 001f35726ad..7b9d7fe4a72 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -11,7 +11,6 @@ set_aux_heat: name: Auxiliary heating description: New value of auxiliary heater. required: true - example: true selector: boolean: @@ -40,7 +39,6 @@ set_temperature: temperature: name: Temperature description: New target temperature for HVAC. - example: 25 selector: number: min: 0 @@ -51,7 +49,6 @@ set_temperature: name: Target temperature high description: New target high temperature for HVAC. advanced: true - example: 26 selector: number: min: 0 @@ -62,7 +59,6 @@ set_temperature: name: Target temperature low description: New target low temperature for HVAC. advanced: true - example: 20 selector: number: min: 0 @@ -72,7 +68,6 @@ set_temperature: hvac_mode: name: HVAC mode description: HVAC operation mode to set temperature to. - example: "heat" selector: select: options: @@ -95,14 +90,11 @@ set_humidity: name: Humidity description: New target humidity for climate device. required: true - example: 60 selector: number: min: 30 max: 99 - step: 1 unit_of_measurement: "%" - mode: slider set_fan_mode: name: Set fan mode @@ -129,7 +121,6 @@ set_hvac_mode: hvac_mode: name: HVAC mode description: New value of operation mode. - example: "heat" selector: select: options: diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml index cc26541def5..1930ba0d45b 100644 --- a/homeassistant/components/counter/services.yaml +++ b/homeassistant/components/counter/services.yaml @@ -31,7 +31,6 @@ configure: minimum: name: Minimum description: New minimum value for the counter or None to remove minimum. - example: 0 selector: number: min: -9223372036854775807 @@ -40,7 +39,6 @@ configure: maximum: name: Maximum description: New maximum value for the counter or None to remove maximum. - example: 100 selector: number: min: -9223372036854775807 @@ -49,7 +47,6 @@ configure: step: name: Step description: New value for step. - example: 2 selector: number: min: 1 @@ -58,7 +55,6 @@ configure: initial: name: Initial description: New value for initial. - example: 6 selector: number: min: 0 @@ -67,7 +63,6 @@ configure: value: name: Value description: New state value. - example: 3 selector: number: min: 0 diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml index f903463bd33..2f8e20464f3 100644 --- a/homeassistant/components/cover/services.yaml +++ b/homeassistant/components/cover/services.yaml @@ -32,14 +32,11 @@ set_cover_position: name: Position description: Position of the cover required: true - example: 30 selector: number: min: 0 max: 100 - step: 1 unit_of_measurement: "%" - mode: slider stop_cover: name: Stop @@ -80,14 +77,11 @@ set_cover_tilt_position: name: Tilt position description: Tilt position of the cover. required: true - example: 30 selector: number: min: 0 max: 100 - step: 1 unit_of_measurement: "%" - mode: slider stop_cover_tilt: name: Stop tilt diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index cd234376e22..fbaf47b009c 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -7,35 +7,34 @@ configure: entity: name: Entity description: Represents a specific device endpoint in deCONZ. - example: "light.rgb_light" selector: entity: integration: deconz field: name: Path - selector: - text: description: >- String representing a full path to deCONZ endpoint (when entity is not specified) or a subpath of the device path for the entity (when entity is specified). example: '"/lights/1/state" or "/state"' - data: - name: Configuration payload - required: true - selector: - object: - description: JSON object with what data you want to alter. - example: '{"on": true}' - bridgeid: - name: Bridge identifier selector: text: + data: + name: Configuration payload + description: JSON object with what data you want to alter. + required: true + example: '{"on": true}' + selector: + object: + bridgeid: + name: Bridge identifier description: >- Unique string for each deCONZ hardware. It can be found as part of the integration name. Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" + selector: + text: device_refresh: name: Device refresh @@ -43,13 +42,13 @@ device_refresh: fields: bridgeid: name: Bridge identifier - selector: - text: description: >- Unique string for each deCONZ hardware. It can be found as part of the integration name. Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" + selector: + text: remove_orphaned_entries: name: Remove orphaned entries @@ -57,13 +56,13 @@ remove_orphaned_entries: fields: bridgeid: name: Bridge identifier - selector: - text: description: >- Unique string for each deCONZ hardware. It can be found as part of the integration name. Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" + selector: + text: alarm_panel_state: name: Alarm panel state diff --git a/homeassistant/components/denonavr/services.yaml b/homeassistant/components/denonavr/services.yaml index cea14a8b361..ee35732e311 100644 --- a/homeassistant/components/denonavr/services.yaml +++ b/homeassistant/components/denonavr/services.yaml @@ -26,7 +26,6 @@ set_dynamic_eq: dynamic_eq: description: "True/false for enable/disable." default: true - example: true selector: boolean: update_audyssey: diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 9e27a04fabf..c6c2d212e2d 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -37,15 +37,14 @@ see: gps_accuracy: name: GPS accuracy description: Accuracy of GPS coordinates. - example: "80" selector: number: min: 1 max: 100 + unit_of_measurement: "%" battery: name: Battery level description: Battery level of device. - example: "100" selector: number: min: 0 diff --git a/homeassistant/components/dynalite/services.yaml b/homeassistant/components/dynalite/services.yaml index 7aee7627f14..d34335ca1d3 100644 --- a/homeassistant/components/dynalite/services.yaml +++ b/homeassistant/components/dynalite/services.yaml @@ -10,14 +10,12 @@ request_area_preset: area: description: "Area to request the preset reported" required: true - example: 2 selector: number: min: 1 max: 9999 channel: description: "Channel to request the preset to be reported from." - example: 1 default: 1 selector: number: @@ -38,7 +36,6 @@ request_channel_level: name: Area description: "Area for the requested channel" required: true - example: 2 selector: number: min: 1 @@ -47,7 +44,6 @@ request_channel_level: name: Channel description: "Channel to request the level for." required: true - example: 1 selector: number: min: 1 diff --git a/homeassistant/components/dyson/services.yaml b/homeassistant/components/dyson/services.yaml index 6f6bc2d9c8a..10b27c1c5e6 100644 --- a/homeassistant/components/dyson/services.yaml +++ b/homeassistant/components/dyson/services.yaml @@ -12,7 +12,6 @@ set_night_mode: name: Night mode description: Night mode status required: true - example: true selector: boolean: @@ -28,7 +27,6 @@ set_auto_mode: name: Auto Mode description: Auto mode status required: true - example: true selector: boolean: @@ -44,7 +42,6 @@ set_angle: name: Angle low description: The angle at which the oscillation should start required: true - example: 1 selector: number: min: 5 @@ -54,7 +51,6 @@ set_angle: name: Angle high description: The angle at which the oscillation should end required: true - example: 255 selector: number: min: 5 @@ -73,7 +69,6 @@ set_flow_direction_front: name: Flow direction front description: Frontal flow direction required: true - example: true selector: boolean: @@ -89,7 +84,6 @@ set_timer: name: Timer description: The value in minutes to set the timer to, 0 to disable it required: true - example: 30 selector: number: min: 0 @@ -108,7 +102,6 @@ set_speed: name: Speed description: Speed required: true - example: 1 selector: number: min: 1 diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml index d88088849b1..aba57989119 100644 --- a/homeassistant/components/ecobee/services.yaml +++ b/homeassistant/components/ecobee/services.yaml @@ -9,7 +9,6 @@ create_vacation: name: Entity description: ecobee thermostat on which to create the vacation. required: true - example: "climate.kitchen" selector: entity: integration: ecobee @@ -25,7 +24,6 @@ create_vacation: name: Cool temperature description: Cooling temperature during the vacation. required: true - example: 23 selector: number: min: 7 @@ -36,7 +34,6 @@ create_vacation: name: Heat temperature description: Heating temperature during the vacation. required: true - example: 25 selector: number: min: 7 @@ -74,7 +71,6 @@ create_vacation: fan_mode: name: Fan mode description: Fan mode of the thermostat during the vacation. - example: "on" default: "auto" selector: select: @@ -84,7 +80,6 @@ create_vacation: fan_min_on_time: name: Fan minimum on time description: Minimum number of minutes to run the fan each hour (0 to 60) during the vacation. - example: 30 default: 0 selector: number: @@ -121,7 +116,6 @@ resume_program: entity_id: name: Entity description: Name(s) of entities to change. - example: "climate.kitchen" selector: entity: integration: ecobee @@ -129,7 +123,6 @@ resume_program: resume_all: name: Resume all description: Resume all events and return to the scheduled program. - example: true default: false selector: boolean: @@ -141,7 +134,6 @@ set_fan_min_on_time: entity_id: name: Entity description: Name(s) of entities to change. - example: "climate.kitchen" selector: entity: integration: ecobee @@ -150,7 +142,6 @@ set_fan_min_on_time: name: Fan minimum on time description: New value of fan min on time. required: true - example: 5 selector: number: min: 0 @@ -169,7 +160,6 @@ set_dst_mode: name: Daylight savings time enabled description: Enable automatic daylight savings time. required: true - example: "true" selector: boolean: @@ -185,7 +175,6 @@ set_mic_mode: name: Mic enabled description: Enable Alexa mic. required: true - example: "true" selector: boolean: @@ -200,12 +189,10 @@ set_occupancy_modes: auto_away: name: Auto away description: Enable Smart Home/Away mode. - example: "true" selector: boolean: follow_me: name: Follow me description: Enable Follow Me mode. - example: "true" selector: boolean: diff --git a/homeassistant/components/eight_sleep/services.yaml b/homeassistant/components/eight_sleep/services.yaml index 7ef2f22d298..537f04bd306 100644 --- a/homeassistant/components/eight_sleep/services.yaml +++ b/homeassistant/components/eight_sleep/services.yaml @@ -6,7 +6,6 @@ heat_set: name: Duration description: Duration to heat/cool at the target level in seconds. required: true - example: 3600 selector: number: min: 0 @@ -16,7 +15,6 @@ heat_set: name: Entity description: Entity id of the bed state to adjust. required: true - example: sensor.eight_left_bed_state selector: entity: integration: eight_sleep @@ -25,7 +23,6 @@ heat_set: name: Target description: Target cooling/heating level from -100 to 100. required: true - example: 35 selector: number: min: -100 diff --git a/homeassistant/components/elkm1/services.yaml b/homeassistant/components/elkm1/services.yaml index f380e4f4b18..18608f3a476 100644 --- a/homeassistant/components/elkm1/services.yaml +++ b/homeassistant/components/elkm1/services.yaml @@ -89,7 +89,6 @@ alarm_display_message: clear: name: Clear description: 0=clear message, 1=clear message with * key, 2=Display until timeout - example: 1 default: 2 selector: number: @@ -98,14 +97,12 @@ alarm_display_message: beep: name: Beep description: 0=no beep, 1=beep - example: 1 default: 0 selector: boolean: timeout: name: Timeout description: Time to display message, 0=forever, max 65535 - example: 4242 default: 0 selector: number: @@ -164,7 +161,6 @@ speak_word: name: Word number description: Word number to speak. required: true - example: 142 selector: number: min: 1 @@ -197,7 +193,6 @@ sensor_counter_set: name: Value description: Value to set the counter to. required: true - example: 4242 selector: number: min: 0 diff --git a/homeassistant/components/envisalink/services.yaml b/homeassistant/components/envisalink/services.yaml index 57c64e0c54a..b15a3b94e01 100644 --- a/homeassistant/components/envisalink/services.yaml +++ b/homeassistant/components/envisalink/services.yaml @@ -8,7 +8,6 @@ alarm_keypress: name: Entity description: Name of the alarm control panel to trigger. required: true - example: "alarm_control_panel.downstairs" selector: entity: integration: envisalink @@ -40,7 +39,6 @@ invoke_custom_function: name: PGM description: The PGM number to trigger on the alarm panel. required: true - example: "2" selector: number: min: 1 diff --git a/homeassistant/components/evohome/services.yaml b/homeassistant/components/evohome/services.yaml index 5a6993f6ad7..bdcb116e4e3 100644 --- a/homeassistant/components/evohome/services.yaml +++ b/homeassistant/components/evohome/services.yaml @@ -66,7 +66,6 @@ set_zone_override: name: Setpoint description: The temperature to be used instead of the scheduled setpoint. required: true - example: 5.0 selector: number: min: 4.0 @@ -89,7 +88,6 @@ clear_zone_override: name: Entity description: The entity_id of the zone. required: true - example: climate.bathroom selector: entity: integration: evohome diff --git a/homeassistant/components/facebox/services.yaml b/homeassistant/components/facebox/services.yaml index 9c89c5e5c41..3f968cf385a 100644 --- a/homeassistant/components/facebox/services.yaml +++ b/homeassistant/components/facebox/services.yaml @@ -5,7 +5,6 @@ teach_face: entity_id: name: Entity description: The facebox entity to teach. - example: "image_processing.facebox" selector: entity: integration: facebox diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 06245e68395..ee39229699d 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -40,14 +40,11 @@ set_percentage: name: Percentage description: Percentage speed setting. required: true - example: 25 selector: number: min: 0 max: 100 - step: 1 unit_of_measurement: "%" - mode: slider turn_on: name: Turn on @@ -60,17 +57,16 @@ turn_on: name: Speed description: Speed setting. example: "high" + selector: + text: percentage: name: Percentage description: Percentage speed setting. - example: 75 selector: number: min: 0 max: 100 - step: 1 unit_of_measurement: "%" - mode: slider preset_mode: name: Preset mode description: Preset mode setting. @@ -96,7 +92,6 @@ oscillate: name: Oscillating description: Flag to turn on/off oscillation. required: true - example: true selector: boolean: @@ -118,7 +113,6 @@ set_direction: name: Direction description: The direction to rotate. required: true - example: "forward" selector: select: options: @@ -136,14 +130,11 @@ increase_speed: advanced: true required: false description: Increase speed by a percentage. - example: 50 selector: number: min: 0 max: 100 - step: 1 unit_of_measurement: "%" - mode: slider decrease_speed: name: Decrease speed @@ -156,11 +147,8 @@ decrease_speed: advanced: true required: false description: Decrease speed by a percentage. - example: 50 selector: number: min: 0 max: 100 - step: 1 unit_of_measurement: "%" - mode: slider diff --git a/homeassistant/components/ffmpeg/services.yaml b/homeassistant/components/ffmpeg/services.yaml index a00a820ebc8..1fdde46e55c 100644 --- a/homeassistant/components/ffmpeg/services.yaml +++ b/homeassistant/components/ffmpeg/services.yaml @@ -5,7 +5,6 @@ restart: entity_id: name: Entity description: Name of entity that will restart. Platform dependent. - example: binary_sensor.ffmpeg_noise selector: entity: integration: ffmpeg @@ -17,7 +16,6 @@ start: entity_id: name: Entity description: Name of entity that will start. Platform dependent. - example: binary_sensor.ffmpeg_noise selector: entity: integration: ffmpeg @@ -29,7 +27,6 @@ stop: entity_id: name: Entity description: Name of entity that will stop. Platform dependent. - example: binary_sensor.ffmpeg_noise selector: entity: integration: ffmpeg diff --git a/homeassistant/components/flo/services.yaml b/homeassistant/components/flo/services.yaml index 237fb4a7bf9..fb3dbb3ee0a 100644 --- a/homeassistant/components/flo/services.yaml +++ b/homeassistant/components/flo/services.yaml @@ -12,7 +12,6 @@ set_sleep_mode: name: Sleep minutes description: The time to sleep in minutes. default: true - example: 120 selector: select: options: @@ -23,7 +22,6 @@ set_sleep_mode: name: Revert to mode description: The mode to revert to after sleep_minutes has elapsed. default: true - example: "home" selector: select: options: diff --git a/homeassistant/components/foscam/services.yaml b/homeassistant/components/foscam/services.yaml index 61326d0e8b6..04dba8ccbaa 100644 --- a/homeassistant/components/foscam/services.yaml +++ b/homeassistant/components/foscam/services.yaml @@ -9,7 +9,6 @@ ptz: movement: description: "Direction of the movement." required: true - example: "up" selector: select: options: @@ -23,7 +22,6 @@ ptz: - 'up' travel_time: description: "Travel time in seconds." - example: 0.125 default: 0.125 selector: number: diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml index 0f4948f4bf9..478202a4a0a 100644 --- a/homeassistant/components/frontend/services.yaml +++ b/homeassistant/components/frontend/services.yaml @@ -13,9 +13,8 @@ set_theme: text: mode: name: Mode - description: The mode the theme is for, either 'dark' or 'light'. + description: The mode the theme is for. default: "light" - example: "dark" selector: select: options: diff --git a/homeassistant/components/geniushub/services.yaml b/homeassistant/components/geniushub/services.yaml index 27d7189f953..1edcf243cc0 100644 --- a/homeassistant/components/geniushub/services.yaml +++ b/homeassistant/components/geniushub/services.yaml @@ -10,7 +10,6 @@ set_zone_mode: name: Entity description: The zone's entity_id. required: true - example: climate.kitchen selector: entity: integration: geniushub @@ -19,7 +18,6 @@ set_zone_mode: name: Mode description: "One of: off, timer or footprint." required: true - example: timer selector: select: options: @@ -36,7 +34,6 @@ set_zone_override: name: Entity description: The zone's entity_id. required: true - example: climate.bathroom selector: entity: integration: geniushub @@ -45,7 +42,6 @@ set_zone_override: name: Temperature description: The target temperature. required: true - example: 19.2 selector: number: min: 4 diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index f1e64a60022..3e7f1eb203d 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -41,7 +41,6 @@ set: all: name: All description: Enable this option if the group should only turn on when all entities are on. - example: true selector: boolean: diff --git a/homeassistant/components/harmony/services.yaml b/homeassistant/components/harmony/services.yaml index e548285ae83..fd53912397a 100644 --- a/homeassistant/components/harmony/services.yaml +++ b/homeassistant/components/harmony/services.yaml @@ -18,7 +18,6 @@ change_channel: name: Channel description: Channel number to change to required: true - example: "200" selector: number: min: 1 diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml index e5ee0d0a95a..943450a6796 100644 --- a/homeassistant/components/hdmi_cec/services.yaml +++ b/homeassistant/components/hdmi_cec/services.yaml @@ -62,7 +62,6 @@ volume: down: name: Down description: Decreases volume x levels. - example: 3 selector: number: min: 1 @@ -70,7 +69,6 @@ volume: mute: name: Mute description: Mutes audio system. - example: toggle selector: select: options: @@ -80,7 +78,6 @@ volume: up: name: Up description: Increases volume x levels. - example: 3 selector: number: min: 1 diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml index f69e9efa19f..d0de9645c6a 100644 --- a/homeassistant/components/hive/services.yaml +++ b/homeassistant/components/hive/services.yaml @@ -16,7 +16,6 @@ boost_heating: temperature: name: Temperature description: Set the target temperature for the boost period. - example: 20.5 default: 25.0 selector: number: @@ -24,7 +23,6 @@ boost_heating: max: 35 step: 0.5 unit_of_measurement: ° - mode: slider boost_heating_on: name: Boost Heating On description: Set the boost mode ON defining the period of time and the desired target temperature for the boost. @@ -43,7 +41,6 @@ boost_heating_on: temperature: name: Temperature description: Set the target temperature for the boost period. - example: 20.5 default: 25.0 selector: number: @@ -51,7 +48,6 @@ boost_heating_on: max: 35 step: 0.5 unit_of_measurement: ° - mode: slider boost_heating_off: name: Boost Heating Off description: Set the boost mode OFF. @@ -60,7 +56,6 @@ boost_heating_off: name: Entity ID description: Select entity_id to turn boost off. required: true - example: climate.heating selector: entity: integration: hive @@ -73,7 +68,6 @@ boost_hot_water: name: Entity ID description: Select entity_id to boost. required: true - example: water_heater.hot_water selector: entity: integration: hive @@ -89,7 +83,6 @@ boost_hot_water: name: Mode description: Set the boost function on or off. required: true - example: "on" selector: select: options: diff --git a/homeassistant/components/homematic/services.yaml b/homeassistant/components/homematic/services.yaml index c1afa3cf1a8..15099c790fb 100644 --- a/homeassistant/components/homematic/services.yaml +++ b/homeassistant/components/homematic/services.yaml @@ -15,7 +15,6 @@ virtualkey: name: Channel description: Channel for calling a keypress. required: true - example: 1 selector: number: min: 1 @@ -41,7 +40,6 @@ set_variable_value: entity_id: name: Entity description: Name(s) of homematic central to set value. - example: "homematic.ccu2" selector: entity: domain: homematic @@ -75,7 +73,6 @@ set_device_value: name: Channel description: Channel for calling a keypress required: true - example: 1 selector: number: min: 1 @@ -130,7 +127,6 @@ set_install_mode: mode: name: Mode description: 1= Normal mode / 2= Remove exists old links - example: 1 default: 1 selector: number: @@ -139,7 +135,6 @@ set_install_mode: time: name: Time description: Time to run in install mode - example: 1 default: 60 selector: number: diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index 3f493ef11a9..ae8a6f34049 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -8,7 +8,11 @@ activate_eco_mode_with_duration: name: Duration description: The duration of eco mode in minutes. required: true - example: 60 + selector: + number: + min: 1 + max: 1440 + unit_of_measurement: "minutes" accesspoint_id: name: Accesspoint ID description: The ID of the Homematic IP Access Point @@ -49,7 +53,6 @@ activate_vacation: name: Temperature description: the set temperature during the vacation mode. required: true - example: 18.5 default: 18 selector: number: @@ -99,9 +102,8 @@ set_active_climate_profile: text: climate_profile_index: name: Climate profile index - description: The index of the climate profile (1 based) + description: The index of the climate profile. required: true - example: 1 selector: number: min: 1 @@ -127,7 +129,6 @@ dump_hap_config: anonymize: name: Anonymize description: Should the Configuration be anonymized? - example: true default: true selector: boolean: diff --git a/homeassistant/components/humidifier/services.yaml b/homeassistant/components/humidifier/services.yaml index a11c5fb1198..c05dad680a3 100644 --- a/homeassistant/components/humidifier/services.yaml +++ b/homeassistant/components/humidifier/services.yaml @@ -24,7 +24,6 @@ set_humidity: humidity: description: New target humidity for humidifier device. required: true - example: 50 selector: number: min: 0 diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml index 60410701da0..ddeae448f8a 100644 --- a/homeassistant/components/icloud/services.yaml +++ b/homeassistant/components/icloud/services.yaml @@ -57,7 +57,6 @@ display_message: sound: name: Sound description: To make a sound when displaying the message. - example: "true" selector: boolean: diff --git a/homeassistant/components/ifttt/services.yaml b/homeassistant/components/ifttt/services.yaml index e81faca1acd..9c02284d4f8 100644 --- a/homeassistant/components/ifttt/services.yaml +++ b/homeassistant/components/ifttt/services.yaml @@ -7,7 +7,6 @@ push_alarm_state: entity_id: description: Name of the alarm control panel which state has to be updated. required: true - example: "alarm_control_panel.downstairs" selector: entity: domain: alarm_control_panel diff --git a/homeassistant/components/ihc/services.yaml b/homeassistant/components/ihc/services.yaml index 06ef0930e97..33f6c8ca31d 100644 --- a/homeassistant/components/ihc/services.yaml +++ b/homeassistant/components/ihc/services.yaml @@ -8,8 +8,7 @@ set_runtime_value_bool: name: Controller ID description: | If you have multiple controller, this is the index of you controller - starting with 0 - example: 0 + starting with 0. default: 0 selector: number: @@ -19,7 +18,6 @@ set_runtime_value_bool: name: IHC ID description: The integer IHC resource ID. required: true - example: 123456 selector: number: min: 0 @@ -29,7 +27,6 @@ set_runtime_value_bool: name: Value description: The boolean value to set. required: true - example: true selector: boolean: @@ -41,8 +38,7 @@ set_runtime_value_int: name: Controller ID description: | If you have multiple controller, this is the index of you controller - starting with 0 - example: 0 + starting with 0. default: 0 selector: number: @@ -52,7 +48,6 @@ set_runtime_value_int: name: IHC ID description: The integer IHC resource ID. required: true - example: 123456 selector: number: min: 0 @@ -62,7 +57,6 @@ set_runtime_value_int: name: Value description: The integer value to set. required: true - example: 50 selector: number: min: 0 @@ -77,8 +71,7 @@ set_runtime_value_float: name: Controller ID description: | If you have multiple controller, this is the index of you controller - starting with 0 - example: 0 + starting with 0. default: 0 selector: number: @@ -88,7 +81,6 @@ set_runtime_value_float: name: IHC ID description: The integer IHC resource ID. required: true - example: 123456 selector: number: min: 0 @@ -98,7 +90,6 @@ set_runtime_value_float: name: Value description: The float value to set. required: true - example: 1.47 selector: number: min: 0 @@ -114,8 +105,7 @@ pulse: name: Controller ID description: | If you have multiple controller, this is the index of you controller - starting with 0 - example: 0 + starting with 0. default: 0 selector: number: @@ -125,7 +115,6 @@ pulse: name: IHC ID description: The integer IHC resource ID. required: true - example: 123456 selector: number: min: 0 diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index 519b4a085ad..51b1d6b00c1 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -28,7 +28,6 @@ set_datetime: description: The target date & time the entity should be set to as expressed by a UNIX timestamp. - example: 1598027400 selector: number: min: 0 diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml index 477adbb0dbe..41164a7ccf5 100644 --- a/homeassistant/components/input_number/services.yaml +++ b/homeassistant/components/input_number/services.yaml @@ -23,7 +23,6 @@ set_value: name: Value description: The target value the entity should be set to. required: true - example: 42 selector: number: min: 0 diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index b42497e12bf..8b8828eaa92 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -9,7 +9,6 @@ select_next: name: Cycle description: If the option should cycle from the last to the first. default: true - example: true selector: boolean: @@ -39,7 +38,6 @@ select_previous: name: Cycle description: If the option should cycle from the first to the last. default: true - example: true selector: boolean: diff --git a/homeassistant/components/insteon/services.yaml b/homeassistant/components/insteon/services.yaml index b2f8467475e..21bc0f535f6 100644 --- a/homeassistant/components/insteon/services.yaml +++ b/homeassistant/components/insteon/services.yaml @@ -6,7 +6,6 @@ add_all_link: name: Group description: All-Link group number. required: true - example: 1 selector: number: min: 0 @@ -15,7 +14,6 @@ add_all_link: name: Mode description: Linking mode controller - IM is controller responder - IM is responder required: true - example: "controller" selector: select: options: @@ -29,7 +27,6 @@ delete_all_link: name: Group description: All-Link group number. required: true - example: 1 selector: number: min: 0 @@ -48,7 +45,6 @@ load_all_link_database: reload: name: Reload description: Reload all records. If true the current records are cleared from memory (does not effect the device) and the records are reloaded. If false the existing records are left in place and only missing records are added. Default is false. - example: "true" default: false selector: boolean: @@ -60,7 +56,6 @@ print_all_link_database: name: Entity description: Name of the device to print required: true - example: "light.1a2b3c" selector: entity: integration: insteon @@ -75,7 +70,6 @@ x10_all_units_off: name: Housecode description: X10 house code required: true - example: c selector: select: options: @@ -103,7 +97,6 @@ x10_all_lights_on: name: Housecode description: X10 house code required: true - example: c selector: select: options: @@ -131,7 +124,6 @@ x10_all_lights_off: name: Housecode description: X10 house code required: true - example: c selector: select: options: @@ -159,7 +151,6 @@ scene_on: name: Group description: INSTEON group or scene number required: true - example: 26 selector: number: min: 0 @@ -172,7 +163,6 @@ scene_off: name: Group description: INSTEON group or scene number required: true - example: 26 selector: number: min: 0 diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 73e0a675e49..923dfe1fd6f 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -20,7 +20,6 @@ send_raw_node_command: value: name: Value description: The integer value to be sent with the command. - example: 255 selector: number: min: 0 @@ -35,7 +34,6 @@ send_raw_node_command: unit_of_measurement: name: Unit of measurement description: The ISY Unit of Measurement (UOM) to send with the command, if required. - example: 67 selector: number: min: 0 @@ -53,7 +51,6 @@ send_node_command: name: Command description: The command to be sent to the device. required: true - example: "fast_on" selector: select: options: @@ -150,7 +147,6 @@ set_on_level: name: Value description: integer value to set. required: true - example: 255 selector: number: min: 0 @@ -167,7 +163,6 @@ set_ramp_rate: name: Value description: Integer value to set, see PyISY/ISY documentation for values to actual ramp times. required: true - example: 30 selector: number: min: 0 @@ -195,7 +190,6 @@ set_variable: address: name: Address description: The address of the variable for which to set the value. - example: 5 selector: number: min: 0 @@ -203,7 +197,6 @@ set_variable: type: name: Type description: The variable type, 1 = Integer, 2 = State. - example: 2 selector: number: min: 1 @@ -217,7 +210,6 @@ set_variable: init: name: Init description: If True, the initial (init) value will be updated instead of the current value. - example: false default: false selector: boolean: @@ -225,7 +217,6 @@ set_variable: name: Value description: The integer value to be sent. required: true - example: 255 selector: number: min: 0 @@ -246,15 +237,18 @@ send_program_command: name: Address description: The address of the program to control (use either address or name). example: "04B1" + selector: + text: name: name: Name description: The name of the program to control (use either address or name). example: "My Program" + selector: + text: command: name: Command description: The ISY Program Command to be sent. required: true - example: "run" selector: select: options: @@ -279,7 +273,6 @@ run_network_resource: address: name: Address description: The address of the network resource to execute (use either address or name). - example: 121 selector: number: min: 0 diff --git a/homeassistant/components/izone/services.yaml b/homeassistant/components/izone/services.yaml index d03ad66421a..5cecbb68a9f 100644 --- a/homeassistant/components/izone/services.yaml +++ b/homeassistant/components/izone/services.yaml @@ -8,16 +8,14 @@ airflow_min: fields: airflow: name: Percent - description: Airflow percent in 5% increments + description: Airflow percent. required: true - example: 95 selector: number: min: 0 max: 100 step: 5 unit_of_measurement: "%" - mode: slider airflow_max: name: Set maximum airflow description: Set the airflow maximum percent for a zone @@ -28,13 +26,11 @@ airflow_max: fields: airflow: name: Percent - description: Airflow percent in 5% increments + description: Airflow percent. required: true - example: 95 selector: number: min: 0 max: 100 step: 5 unit_of_measurement: "%" - mode: slider diff --git a/homeassistant/components/keba/services.yaml b/homeassistant/components/keba/services.yaml index 10e03f83b08..8e5e8cd91f8 100644 --- a/homeassistant/components/keba/services.yaml +++ b/homeassistant/components/keba/services.yaml @@ -22,13 +22,13 @@ set_energy: energy: name: Energy description: > - The energy target to stop charging in kWh. Setting 0 disables the limit. - example: 10.0 + The energy target to stop charging. Setting 0 disables the limit. selector: number: min: 0 max: 100 step: 0.1 + unit_of_measurement: "kWh" set_current: name: Set current @@ -37,16 +37,16 @@ set_current: current: name: Current description: > - The maximum current used for the charging process in A. Allowed are values between - 6 A and 63 A. Invalid values are discardedand the default is set to 6 A. - The value is also depending on the DIP-switchsettings and the used cable of the - charging station - example: 16 + The maximum current used for the charging process. + The value is depending on the DIP-switch settings and the used cable of the + charging station. + default: 6 selector: number: - min: 0 - max: 100 + min: 6 + max: 63 step: 0.1 + unit_of_measurement: "A" enable: name: Enable @@ -67,7 +67,6 @@ set_failsafe: name: Failsafe timeout description: > Timeout after which the failsafe mode is triggered, if set_current was not executed during this time. - example: 30 default: 30 selector: number: @@ -77,19 +76,18 @@ set_failsafe: failsafe_fallback: name: Failsafe fallback description: > - Fallback current in A to be set after timeout. - example: 6 + Fallback current to be set after timeout. default: 6 selector: number: - min: 0 - max: 100 + min: 6 + max: 63 step: 0.1 + unit_of_measurement: "A" failsafe_persist: name: Failsafe persist description: > If failsafe_persist is 0, the failsafe option is only until charging station reboot. If failsafe_persist is 1, the failsafe option will survive a reboot. - example: 0 default: 0 selector: number: diff --git a/homeassistant/components/kef/services.yaml b/homeassistant/components/kef/services.yaml index 6822c41dbc1..291a8f45cdb 100644 --- a/homeassistant/components/kef/services.yaml +++ b/homeassistant/components/kef/services.yaml @@ -18,31 +18,26 @@ set_mode: desk_mode: name: Desk mode description: Desk mode. - example: true selector: boolean: wall_mode: name: Wall mode description: Wall mode. - example: true selector: boolean: phase_correction: name: Phase correction description: Phase correction. - example: true selector: boolean: high_pass: name: High pass description: High-pass mode". - example: true selector: boolean: sub_polarity: name: Subwoofer polarity description: Sub polarity. - example: "+" selector: select: options: @@ -51,7 +46,6 @@ set_mode: bass_extension: name: Base extension description: Bass extension. - example: "Extra" selector: select: options: @@ -62,11 +56,11 @@ set_mode: set_desk_db: name: Set desk dB description: Set the "Desk mode" slider of the speaker in dB. + target: + entity: + integration: kef + domain: media_player fields: - entity_id: - name: Entity - description: The entity_id of the KEF speaker. - example: media_player.kef_lsx db_value: name: dB value description: Value of the slider @@ -89,7 +83,6 @@ set_wall_db: db_value: name: dB value description: Value of the slider. - example: 0.0 selector: number: min: -6 @@ -108,7 +101,6 @@ set_treble_db: db_value: name: dB value description: Value of the slider. - example: 0.0 selector: number: min: -2 @@ -127,7 +119,6 @@ set_high_hz: hz_value: name: Hertz value description: Value of the slider. - example: 95 selector: number: min: 50 @@ -146,7 +137,6 @@ set_low_hz: hz_value: name: Hertz value description: Value of the slider. - example: 80 selector: number: min: 40 @@ -165,10 +155,8 @@ set_sub_db: db_value: name: dB value description: Value of the slider. - example: 0 selector: number: min: -10 max: 10 - step: 1 unit_of_measurement: dB diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 6090057feca..1ea5d9b6faa 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -73,7 +73,6 @@ exposure_register: name: "Entity" description: "Entity id whose state or attribute shall be exposed." required: true - example: "light.kitchen" selector: entity: attribute: diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml index 299e1df5777..52aa8863872 100644 --- a/homeassistant/components/lcn/services.yaml +++ b/homeassistant/components/lcn/services.yaml @@ -15,7 +15,6 @@ output_abs: name: Output description: Output port required: true - example: "output1" selector: select: options: @@ -25,17 +24,16 @@ output_abs: - "output4" brightness: name: Brightness - description: Absolute brightness in percent. + description: Absolute brightness. required: true - example: 50 selector: number: min: 0 max: 100 + unit_of_measurement: "%" transition: name: Transition description: Transition time. - example: 5 default: 0 selector: number: @@ -59,7 +57,6 @@ output_rel: name: Output description: Output port required: true - example: "output1" selector: select: options: @@ -69,9 +66,8 @@ output_rel: - "output4" brightness: name: Brightness - description: Relative brightness in percent. + description: Relative brightness. required: true - example: 50 selector: number: min: -100 @@ -93,7 +89,6 @@ output_toggle: name: Output description: Output port required: true - example: "output1" selector: select: options: @@ -104,7 +99,6 @@ output_toggle: transition: name: Transition description: Transition time. - example: 5 default: 0 selector: number: @@ -126,7 +120,7 @@ relays: text: state: name: State - description: Relays states as string (1=on, 2=off, t=toggle, -=nochange) + description: Relays states as string (1=on, 2=off, t=toggle, -=no change) required: true example: "t---001-" selector: @@ -147,7 +141,6 @@ led: name: LED description: Led required: true - example: "led6" selector: select: options: @@ -167,7 +160,6 @@ led: name: State description: Led state required: true - example: "blink" selector: select: options: @@ -191,7 +183,6 @@ var_abs: name: Variable description: Variable or setpoint name required: true - example: "var1" default: native selector: select: @@ -218,8 +209,7 @@ var_abs: - "var12" value: name: Value - description: Value to set - example: "50" + description: Value to set. default: 0 selector: number: @@ -227,8 +217,7 @@ var_abs: max: 100000 unit_of_measurement: name: Unit of measurement - description: Unit of value - example: "celsius" + description: Unit of value. selector: select: options: @@ -265,11 +254,12 @@ var_reset: description: Module address required: true example: "myhome.s0.m7" + selector: + text: variable: name: Variable - description: Variable or setpoint name + description: Variable or setpoint name. required: true - example: "var1" selector: select: options: @@ -309,7 +299,6 @@ var_rel: name: Variable description: Variable or setpoint name required: true - example: "var1" selector: select: options: @@ -353,7 +342,6 @@ var_rel: value: name: Value description: Shift value - example: "50" default: 0 selector: number: @@ -362,7 +350,6 @@ var_rel: unit_of_measurement: name: Unit of measurement description: Unit of value - example: "celsius" default: native selector: select: @@ -393,7 +380,6 @@ var_rel: value_reference: name: Reference value description: Reference value for setpoint and threshold - example: "current" default: current selector: select: @@ -416,7 +402,6 @@ lock_regulator: name: Setpoint description: Setpoint name required: true - example: "r1varsetpoint" selector: select: options: @@ -440,7 +425,6 @@ lock_regulator: state: name: State description: New setpoint state - example: true default: false selector: boolean: @@ -465,8 +449,7 @@ send_keys: text: state: name: State - description: "Key state upon sending (optional, must be hit for deferred)" - example: "hit" + description: "Key state upon sending (must be hit for deferred)" default: hit selector: select: @@ -478,7 +461,6 @@ send_keys: time: name: Time description: Send delay. - example: 10 default: 0 selector: number: @@ -487,7 +469,6 @@ send_keys: time_unit: name: Time unit description: Time unit of send delay. - example: "s" default: s selector: select: @@ -535,7 +516,6 @@ lock_keys: time: name: Time description: Lock interval. - example: 10 default: 0 selector: number: @@ -544,7 +524,6 @@ lock_keys: time_unit: name: Time unit description: Time unit of lock interval. - example: "s" default: s selector: select: @@ -579,7 +558,6 @@ dyn_text: name: Row description: Text row. required: true - example: 1 selector: number: min: 1 diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index 5f4784630aa..e5a1cf4b5d8 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -9,7 +9,6 @@ set_state: infrared: name: infrared description: Automatic infrared level when light brightness is low. - example: 255 selector: number: min: 0 @@ -23,7 +22,6 @@ set_state: transition: name: Transition description: Duration it takes to get to the final state. - example: 10 selector: number: min: 0 @@ -32,7 +30,6 @@ set_state: power: name: Power description: Turn the light on or off. Leave out to keep the power as it is. - example: false selector: boolean: @@ -47,7 +44,6 @@ effect_pulse: mode: name: Mode description: "Decides how colors are changed." - example: strobe selector: select: options: @@ -59,7 +55,6 @@ effect_pulse: brightness: name: Brightness description: Number indicating brightness of the temporary color. - example: 120 selector: number: min: 0 @@ -79,7 +74,6 @@ effect_pulse: period: name: Period description: Duration of the effect. - example: 3 default: 1.0 selector: number: @@ -90,7 +84,6 @@ effect_pulse: cycles: name: Cycles description: Number of times the effect should run. - example: 2 default: 1 selector: number: @@ -99,7 +92,6 @@ effect_pulse: power_on: name: Power on description: Powered off lights are temporarily turned on during the effect. - example: false default: true selector: boolean: @@ -115,7 +107,6 @@ effect_colorloop: brightness: name: Brightness description: Number indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. - example: 120 selector: number: min: 0 @@ -123,7 +114,6 @@ effect_colorloop: period: name: Period description: Duration between color changes. - example: 180 default: 60 selector: number: @@ -134,7 +124,6 @@ effect_colorloop: change: name: Change description: Hue movement per period, in degrees on a color wheel. - example: 45 default: 20 selector: number: @@ -144,7 +133,6 @@ effect_colorloop: spread: name: Spread description: Maximum hue difference between participating lights, in degrees on a color wheel. - example: 0 default: 30 selector: number: @@ -154,7 +142,6 @@ effect_colorloop: power_on: name: Power on description: Powered off lights are temporarily turned on during the effect. - example: false default: true selector: boolean: diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 8ad01bcdd8c..e2a8a94a74a 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -11,15 +11,12 @@ turn_on: fields: transition: name: Transition - description: Duration in seconds it takes to get to next state. - example: 60 + description: Duration it takes to get to next state. selector: number: min: 0 max: 300 - step: 1 unit_of_measurement: seconds - mode: slider rgb_color: name: RGB-color description: A list containing three integers between 0 and 255 representing the RGB (red, green, blue) color for the light. @@ -45,7 +42,6 @@ turn_on: name: Color name description: A human readable color name. advanced: true - example: "red" selector: select: options: @@ -216,76 +212,59 @@ turn_on: name: Color temperature (mireds) description: Color temperature for the light in mireds. advanced: true - example: 250 selector: number: min: 153 max: 500 - step: 1 unit_of_measurement: mireds - mode: slider kelvin: name: Color temperature (Kelvin) description: Color temperature for the light in Kelvin. advanced: true - example: 4000 selector: number: min: 2000 max: 6500 step: 100 unit_of_measurement: K - mode: slider brightness: name: Brightness value description: - Number between 0..255 indicating brightness, where 0 turns the light + Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. advanced: true - example: 120 selector: number: min: 0 max: 255 - step: 1 - mode: slider brightness_pct: name: Brightness description: - Number between 0..100 indicating percentage of full brightness, where 0 + Number indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. - example: 47 selector: number: min: 0 max: 100 - step: 1 unit_of_measurement: "%" - mode: slider brightness_step: name: Brightness step value - description: Change brightness by an amount. Should be between -255..255. + description: Change brightness by an amount. advanced: true - example: -25.5 selector: number: min: -225 max: 255 - step: 1 - mode: slider brightness_step_pct: name: Brightness step - description: Change brightness by a percentage. Should be between -100..100. - example: -10 + description: Change brightness by a percentage. selector: number: min: -100 max: 100 - step: 1 unit_of_measurement: "%" - mode: slider profile: name: Profile description: Name of a light profile to use. @@ -295,12 +274,8 @@ turn_on: text: flash: name: Flash - description: If the light should flash. Valid values are short and long. + description: If the light should flash. advanced: true - example: short - values: - - short - - long selector: select: options: @@ -309,12 +284,12 @@ turn_on: effect: name: Effect description: Light effect. - example: random - values: - - colorloop - - random selector: - text: + select: + options: + - colorloop + - random + - white turn_off: name: Turn off @@ -325,23 +300,16 @@ turn_off: fields: transition: name: Transition - description: Duration in seconds it takes to get to next state. - example: 60 + description: Duration it takes to get to next state. selector: number: min: 0 max: 300 - step: 1 unit_of_measurement: seconds - mode: slider flash: name: Flash - description: If the light should flash. Valid values are short and long. + description: If the light should flash. advanced: true - example: short - values: - - short - - long selector: select: options: @@ -359,15 +327,12 @@ toggle: fields: transition: name: Transition - description: Duration in seconds it takes to get to next state. - example: 60 + description: Duration it takes to get to next state. selector: number: min: 0 max: 300 - step: 1 unit_of_measurement: seconds - mode: slider rgb_color: name: RGB-color description: Color for the light in RGB-format. @@ -379,7 +344,6 @@ toggle: name: Color name description: A human readable color name. advanced: true - example: "red" selector: select: options: @@ -550,64 +514,50 @@ toggle: name: Color temperature (mireds) description: Color temperature for the light in mireds. advanced: true - example: 250 selector: number: min: 153 max: 500 - step: 1 unit_of_measurement: mireds - mode: slider kelvin: name: Color temperature (Kelvin) description: Color temperature for the light in Kelvin. advanced: true - example: 4000 selector: number: min: 2000 max: 6500 step: 100 unit_of_measurement: K - mode: slider white_value: name: White level description: Number indicating level of white. advanced: true - example: "250" selector: number: min: 0 max: 255 - step: 1 - mode: slider brightness: name: Brightness value description: Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. advanced: true - example: 120 selector: number: min: 0 max: 255 - step: 1 - mode: slider brightness_pct: name: Brightness description: - Number between 0..100 indicating percentage of full brightness, where 0 + Number indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. - example: 47 selector: number: min: 0 max: 100 - step: 1 unit_of_measurement: "%" - mode: slider profile: name: Profile description: Name of a light profile to use. @@ -617,12 +567,8 @@ toggle: text: flash: name: Flash - description: If the light should flash. Valid values are short and long. + description: If the light should flash. advanced: true - example: short - values: - - short - - long selector: select: options: @@ -631,9 +577,5 @@ toggle: effect: name: Effect description: Light effect. - example: random - values: - - colorloop - - random selector: text: diff --git a/homeassistant/components/litterrobot/services.yaml b/homeassistant/components/litterrobot/services.yaml index 8caf0fcb73c..0071c525567 100644 --- a/homeassistant/components/litterrobot/services.yaml +++ b/homeassistant/components/litterrobot/services.yaml @@ -18,7 +18,6 @@ set_sleep_mode: name: Enabled description: Whether sleep mode should be enabled. required: true - example: true selector: boolean: start_time: @@ -40,11 +39,6 @@ set_wait_time: name: Minutes description: Minutes to wait. required: true - example: 7 - values: - - 3 - - 7 - - 15 default: 7 selector: select: diff --git a/homeassistant/components/local_file/services.yaml b/homeassistant/components/local_file/services.yaml index 08a954594a9..f4382decb0f 100644 --- a/homeassistant/components/local_file/services.yaml +++ b/homeassistant/components/local_file/services.yaml @@ -6,7 +6,6 @@ update_file_path: name: Entity description: Name of the entity_id of the camera to update. required: true - example: "camera.local_file" selector: entity: domain: camera diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index 5d5e05240e8..5e371f29ab8 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -7,7 +7,6 @@ clear_usercode: node_id: name: Node ID description: Node id of the lock. - example: 18 selector: number: min: 1 @@ -15,7 +14,6 @@ clear_usercode: code_slot: name: Code slot description: Code slot to clear code from. - example: 1 selector: number: min: 1 @@ -28,7 +26,6 @@ get_usercode: node_id: name: Node ID description: Node id of the lock. - example: 18 selector: number: min: 1 @@ -36,7 +33,6 @@ get_usercode: code_slot: name: Code slot description: Code slot to retrieve a code from. - example: 1 selector: number: min: 1 @@ -76,14 +72,12 @@ set_usercode: fields: node_id: description: Node id of the lock. - example: 18 selector: number: min: 1 max: 255 code_slot: description: Code slot to set the code. - example: 1 selector: number: min: 1 diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml index e72d4c0a01f..3f688628032 100644 --- a/homeassistant/components/logbook/services.yaml +++ b/homeassistant/components/logbook/services.yaml @@ -19,7 +19,6 @@ log: entity_id: name: Entity ID description: Entity to reference in custom logbook entry. - example: "light.kitchen" selector: entity: domain: diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml index 51fe5fe331c..1995a027b0b 100644 --- a/homeassistant/components/logger/services.yaml +++ b/homeassistant/components/logger/services.yaml @@ -5,7 +5,6 @@ set_default_level: level: name: Level description: Default severity level for all integrations. - example: debug selector: select: options: @@ -25,7 +24,6 @@ set_level: description: "Example on how to change the logging level for a Home Assistant Core integrations." - example: debug selector: select: options: @@ -40,7 +38,6 @@ set_level: name: Home Assistant components mqtt description: "Example on how to change the logging level for an Integration." - example: warning selector: select: options: @@ -55,7 +52,6 @@ set_level: name: Custom components "my_integation" description: "Example on how to change the logging level for a Custom Integration." - example: debug selector: select: options: @@ -70,7 +66,6 @@ set_level: name: aioHttp description: "Example on how to change the logging level for a Python module." - example: error selector: select: options: diff --git a/homeassistant/components/logi_circle/services.yaml b/homeassistant/components/logi_circle/services.yaml index 00f2a7090fe..248701a5d45 100644 --- a/homeassistant/components/logi_circle/services.yaml +++ b/homeassistant/components/logi_circle/services.yaml @@ -7,7 +7,6 @@ set_config: entity_id: name: Entity description: Name(s) of entities to apply the operation mode to. - example: "camera.living_room_camera" selector: entity: integration: logi_circle @@ -16,7 +15,6 @@ set_config: name: Mode description: "Operation mode. Allowed values: LED, RECORDING_MODE." required: true - example: "RECORDING_MODE" selector: select: options: @@ -26,7 +24,6 @@ set_config: name: Value description: "Operation value." required: true - example: true selector: boolean: @@ -37,7 +34,6 @@ livestream_snapshot: entity_id: name: Entity description: Name(s) of entities to create snapshots from. - example: "camera.living_room_camera" selector: entity: integration: logi_circle @@ -57,7 +53,6 @@ livestream_record: entity_id: name: Entity description: Name(s) of entities to create recordings from. - example: "camera.living_room_camera" selector: entity: integration: logi_circle @@ -73,7 +68,6 @@ livestream_record: name: Duration description: Recording duration. required: true - example: 60 selector: number: min: 1 diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml index b3ef02c74e3..5b1f86e9dfe 100644 --- a/homeassistant/components/media_extractor/services.yaml +++ b/homeassistant/components/media_extractor/services.yaml @@ -16,7 +16,6 @@ play_media: name: Media content type description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC. required: true - example: "music" selector: select: options: diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 6136580ff2c..f897ae8ad7f 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -46,7 +46,6 @@ volume_mute: name: Muted description: True/false for mute/unmute. required: true - example: true selector: boolean: @@ -61,13 +60,11 @@ volume_set: name: Level description: Volume level to set as float. required: true - example: 0.6 selector: number: min: 0 max: 1 step: 0.01 - mode: slider media_play_pause: name: Play/Pause @@ -122,7 +119,6 @@ media_seek: name: Position description: Position to seek to. The format is platform dependent. required: true - example: 100 selector: number: min: 0 @@ -202,7 +198,6 @@ shuffle_set: name: Shuffle description: True/false for enabling/disabling shuffle. required: true - example: true selector: boolean: @@ -217,7 +212,6 @@ repeat_set: name: Repeat mode description: Repeat mode to set. required: true - example: "off" selector: select: options: diff --git a/homeassistant/components/mill/services.yaml b/homeassistant/components/mill/services.yaml index 37b878580c9..6d475243dd5 100644 --- a/homeassistant/components/mill/services.yaml +++ b/homeassistant/components/mill/services.yaml @@ -12,7 +12,6 @@ set_room_temperature: away_temp: name: Away temperature description: Away temp. - example: 12 selector: number: min: 0 @@ -21,7 +20,6 @@ set_room_temperature: comfort_temp: name: Comfort temperature description: Comfort temp. - example: 22 selector: number: min: 0 @@ -30,7 +28,6 @@ set_room_temperature: sleep_temp: name: Sleep temperature description: Sleep temp. - example: 17 selector: number: min: 0 diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index a3aa26a1a41..855303aef07 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -6,7 +6,6 @@ write_coil: name: Address description: Address of the register to write to. required: true - example: 0 selector: number: min: 1 @@ -22,7 +21,6 @@ write_coil: name: Unit description: Address of the modbus unit. required: true - example: 21 selector: number: min: 1 @@ -42,7 +40,6 @@ write_register: name: Address description: Address of the holding register to write to. required: true - example: 0 selector: number: min: 1 @@ -51,7 +48,6 @@ write_register: name: Unit description: Address of the modbus unit. required: true - example: 21 selector: number: min: 1 diff --git a/homeassistant/components/motion_blinds/services.yaml b/homeassistant/components/motion_blinds/services.yaml index 08ee4098e27..1ee60923332 100644 --- a/homeassistant/components/motion_blinds/services.yaml +++ b/homeassistant/components/motion_blinds/services.yaml @@ -12,7 +12,6 @@ set_absolute_position: name: Absolute position description: Absolute position to move to. required: true - example: 70 selector: number: min: 1 @@ -20,7 +19,6 @@ set_absolute_position: width: name: Width description: Specify the width that is covered, only for TDBU Combined entities. - example: 30 selector: number: min: 1 diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index 21d3915628a..7b57570a3ef 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -29,11 +29,6 @@ publish: name: QoS description: Quality of Service to use. advanced: true - example: 2 - values: - - 0 - - 1 - - 2 default: 0 selector: select: @@ -45,7 +40,6 @@ publish: name: Retain description: If message should have the retain flag set. default: false - example: true selector: boolean: @@ -64,15 +58,12 @@ dump: duration: name: Duration description: how long we should listen for messages in seconds - example: 5 default: 5 selector: number: min: 1 max: 300 - step: 1 unit_of_measurement: "seconds" - mode: slider reload: name: Reload diff --git a/homeassistant/components/mysensors/services.yaml b/homeassistant/components/mysensors/services.yaml index e0fa5bf8e89..7293a676a76 100644 --- a/homeassistant/components/mysensors/services.yaml +++ b/homeassistant/components/mysensors/services.yaml @@ -5,7 +5,6 @@ send_ir_code: entity_id: name: Entity description: Name of entity that should have the IR code set and be turned on. Platform dependent. - example: "switch.living_room_1_1" selector: entity: integration: mysensors diff --git a/homeassistant/components/neato/services.yaml b/homeassistant/components/neato/services.yaml index eb0c7bffba9..cbfff7808ee 100644 --- a/homeassistant/components/neato/services.yaml +++ b/homeassistant/components/neato/services.yaml @@ -10,7 +10,6 @@ custom_cleaning: name: Set cleaning mode description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." default: 2 - example: 2 selector: number: min: 1 @@ -20,7 +19,6 @@ custom_cleaning: name: Set navigation mode description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." default: 1 - example: 1 selector: number: min: 1 @@ -30,7 +28,6 @@ custom_cleaning: name: Use cleaning map description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." default: 4 - example: 2 selector: number: min: 2 diff --git a/homeassistant/components/ness_alarm/services.yaml b/homeassistant/components/ness_alarm/services.yaml index 8e4219a7921..ad320285d5b 100644 --- a/homeassistant/components/ness_alarm/services.yaml +++ b/homeassistant/components/ness_alarm/services.yaml @@ -8,7 +8,6 @@ aux: name: Output ID description: The aux output you wish to change. required: true - example: 1 selector: number: min: 1 @@ -16,7 +15,6 @@ aux: state: name: State description: The On/Off State. If P14xE 8E is enabled then a value of true will pulse output x for the time specified in P14(x+4)E. - example: true default: true selector: boolean: diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index b2ae06c3430..98aacf60524 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -7,7 +7,6 @@ set_away_mode: away_mode: name: Away mode description: New mode to set. - example: "away" required: true selector: select: @@ -28,14 +27,12 @@ set_eta: eta: name: ETA description: Estimated time of arrival from now. - example: "00:10:30" required: true selector: time: eta_window: name: ETA window description: Estimated time of arrival window. - example: "00:05" default: "00:01" selector: time: diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 4cbb7cba2ba..e61e893e199 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -9,8 +9,7 @@ set_camera_light: fields: camera_light_mode: name: Camera light mode - description: Outdoor camera light mode (on/off/auto) - example: auto + description: Outdoor camera light mode. required: true selector: select: @@ -65,7 +64,7 @@ set_person_away: domain: camera fields: person: - description: Person's name (optional) + description: Person's name. example: Bob selector: text: diff --git a/homeassistant/components/netgear_lte/services.yaml b/homeassistant/components/netgear_lte/services.yaml index 116c2f61a2e..a708287612b 100644 --- a/homeassistant/components/netgear_lte/services.yaml +++ b/homeassistant/components/netgear_lte/services.yaml @@ -29,7 +29,6 @@ set_option: failover: name: Failover description: Failover mode. - example: auto selector: select: options: @@ -39,7 +38,6 @@ set_option: autoconnect: name: Auto-connect description: Auto-connect mode. - example: home selector: select: options: diff --git a/homeassistant/components/nexia/services.yaml b/homeassistant/components/nexia/services.yaml index 0b822dce186..740c865e274 100644 --- a/homeassistant/components/nexia/services.yaml +++ b/homeassistant/components/nexia/services.yaml @@ -10,7 +10,6 @@ set_aircleaner_mode: name: Air cleaner mode description: 'The air cleaner mode to set.' required: true - example: allergy selector: select: options: @@ -30,7 +29,6 @@ set_humidify_setpoint: name: Humidify description: "The humidification setpoint." required: true - example: 45 selector: number: min: 35 diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 6bbd15c94ca..a4ac45e9df8 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -56,11 +56,17 @@ apns_register: Notification Service). fields: push_id: + name: Push ID description: The device token, a 64 character hex string (256 bits). The device token is provided to you by your client app, which receives the token after registering itself with the remote notification service. example: "72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62" + selector: + text: name: - description: A friendly name for the device (optional). + name: Name + description: A friendly name for the device. example: "Sam's iPhone" + selector: + text: diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml index 85e0e67ea50..d923885efc6 100644 --- a/homeassistant/components/nuki/services.yaml +++ b/homeassistant/components/nuki/services.yaml @@ -7,9 +7,8 @@ lock_n_go: domain: lock fields: unlatch: - name: unlatch + name: Unlatch description: Whether to unlatch the lock. - example: false default: false selector: boolean: diff --git a/homeassistant/components/nx584/services.yaml b/homeassistant/components/nx584/services.yaml index 25ef4c20702..a5c49f6d6a6 100644 --- a/homeassistant/components/nx584/services.yaml +++ b/homeassistant/components/nx584/services.yaml @@ -12,7 +12,6 @@ bypass_zone: name: Zone description: The number of the zone to be bypassed. required: true - example: "1" selector: number: min: 1 @@ -30,7 +29,6 @@ unbypass_zone: name: Zone description: The number of the zone to be un-bypassed. required: true - example: "1" selector: number: min: 1 diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml index 290b3761ab8..8fe8780dce9 100644 --- a/homeassistant/components/nzbget/services.yaml +++ b/homeassistant/components/nzbget/services.yaml @@ -15,7 +15,6 @@ set_speed: speed: name: Speed description: Speed limit. 0 is unlimited. - example: 1000 default: 1000 selector: number: diff --git a/homeassistant/components/ombi/services.yaml b/homeassistant/components/ombi/services.yaml index c6f154d073e..5a44c7bba02 100644 --- a/homeassistant/components/ombi/services.yaml +++ b/homeassistant/components/ombi/services.yaml @@ -25,7 +25,6 @@ submit_tv_request: season: name: Season description: Which season(s) to request. - example: "latest" default: latest selector: select: diff --git a/homeassistant/components/omnilogic/services.yaml b/homeassistant/components/omnilogic/services.yaml index b886fe7f7f7..94ba0d2982e 100644 --- a/homeassistant/components/omnilogic/services.yaml +++ b/homeassistant/components/omnilogic/services.yaml @@ -10,7 +10,6 @@ set_pump_speed: name: Speed description: Speed for the VSP between min and max speed. required: true - example: 85 selector: number: min: 0 diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml index ee5af2ae77e..83b2d112d2d 100644 --- a/homeassistant/components/onvif/services.yaml +++ b/homeassistant/components/onvif/services.yaml @@ -9,7 +9,6 @@ ptz: tilt: name: Tilt description: "Tilt direction." - example: "UP" selector: select: options: @@ -18,7 +17,6 @@ ptz: pan: name: Pan description: "Pan direction." - example: "RIGHT" selector: select: options: @@ -27,7 +25,6 @@ ptz: zoom: name: Zoom description: "Zoom." - example: "ZOOM_IN" selector: select: options: @@ -37,7 +34,6 @@ ptz: name: Distance description: "Distance coefficient. Sets how much PTZ should be executed in one request." default: 0.1 - example: 0.1 selector: number: min: 0 @@ -47,7 +43,6 @@ ptz: name: Speed description: "Speed coefficient. Sets how fast PTZ will be executed." default: 0.5 - example: 0.5 selector: number: min: 0 @@ -57,7 +52,6 @@ ptz: name: Continuous duration description: "Set ContinuousMove delay in seconds before stopping the move" default: 0.5 - example: 0.5 selector: number: min: 0 @@ -74,7 +68,6 @@ ptz: name: Move Mode description: "PTZ moving mode." default: "RelativeMove" - example: "ContinuousMove" selector: select: options: diff --git a/homeassistant/components/openhome/services.yaml b/homeassistant/components/openhome/services.yaml index 29b07500c3f..0fa95145287 100644 --- a/homeassistant/components/openhome/services.yaml +++ b/homeassistant/components/openhome/services.yaml @@ -12,7 +12,6 @@ invoke_pin: name: PIN description: Which pin to invoke required: true - example: 4 selector: number: min: 0 diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index fe3ecc157c5..02f2e71053f 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -26,12 +26,13 @@ set_central_heating_ovrd: description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" + selector: + text: ch_override: name: Central heating override description: > The desired boolean value for the central heating override. required: true - example: "on" selector: boolean: @@ -44,6 +45,8 @@ set_clock: description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" + selector: + text: date: name: Date description: Optional date from which the day of the week will be extracted. Defaults to today. @@ -77,7 +80,6 @@ set_control_setpoint: Values between 0 and 90 are accepted, but not all boilers support this range. A value of 0 disables the central heating setpoint override. required: true - example: "37.5" selector: number: min: 0 @@ -129,7 +131,6 @@ set_hot_water_setpoint: The domestic hot water setpoint to set on the gateway. Not all boilers support this feature. Values between 0 and 90 are accepted, but not all boilers support this range. Check the values of the slave_dhw_min_setp and slave_dhw_max_setp sensors to see the supported range on your boiler. - example: "60" selector: number: min: 0 @@ -152,7 +153,6 @@ set_gpio_mode: name: ID description: The ID of the GPIO pin. required: true - example: "B" selector: select: options: @@ -164,7 +164,6 @@ set_gpio_mode: 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/integrations/opentherm_gw/#gpio-modes for an explanation of the values. required: true - example: "5" selector: number: min: 0 @@ -185,7 +184,6 @@ set_led_mode: name: ID description: The ID of the LED. required: true - example: "C" selector: select: options: @@ -198,10 +196,9 @@ set_led_mode: mode: name: Mode description: > - The function to assign to the LED. One of "R", "X", "T", "B", "O", "F", "H", "W", "C", "E", "M" or "P". + The function to assign to the LED. See https://www.home-assistant.io/integrations/opentherm_gw/#led-modes for an explanation of the values. required: true - example: "F" selector: select: options: @@ -237,7 +234,6 @@ set_max_modulation: The modulation level to provide to the gateway. Provide a value of -1 to clear the override and forward the value from the thermostat again. required: true - example: "42" selector: number: min: -1 @@ -263,11 +259,11 @@ set_outside_temperature: Values between -40.0 and 64.0 will be accepted, but not all thermostats can display the full range. Any value above 64.0 will clear a previously configured value (suggestion: 99) required: true - example: "-2.3" selector: number: min: -40 max: 99 + unit_of_measurement: "°" set_setback_temperature: name: Set setback temperature @@ -282,11 +278,11 @@ set_setback_temperature: text: temperature: name: Temperature - description: The setback temperature to configure on the gateway. Values between 0.0 and 30.0 are accepted. + description: The setback temperature to configure on the gateway. required: true - example: "16.0" selector: number: min: 0 max: 30 step: 0.1 + unit_of_measurement: "°" diff --git a/homeassistant/components/ozw/services.yaml b/homeassistant/components/ozw/services.yaml index 2919aceceb6..c9c23023134 100644 --- a/homeassistant/components/ozw/services.yaml +++ b/homeassistant/components/ozw/services.yaml @@ -12,7 +12,6 @@ add_node: instance_id: name: Instance ID description: The OZW Instance/Controller to use. - default: 1 selector: number: min: 1 @@ -52,7 +51,6 @@ set_config_parameter: name: Node ID description: Node id of the device to set config parameter to. required: true - example: 10 selector: number: min: 1 @@ -61,7 +59,6 @@ set_config_parameter: name: Parameter description: Parameter number to set. required: true - example: 8 selector: number: min: 1 @@ -94,7 +91,6 @@ clear_usercode: name: Code slot description: Code slot to clear code from. required: true - example: 1 selector: number: min: 1 @@ -112,7 +108,6 @@ set_usercode: name: Code slot description: Code slot to set the code. required: true - example: 1 selector: number: min: 1 diff --git a/homeassistant/components/profiler/services.yaml b/homeassistant/components/profiler/services.yaml index ff634e02ac5..8d9ae35ed10 100644 --- a/homeassistant/components/profiler/services.yaml +++ b/homeassistant/components/profiler/services.yaml @@ -5,7 +5,6 @@ start: seconds: name: Seconds description: The number of seconds to run the profiler. - example: 60.0 default: 60.0 selector: number: @@ -19,7 +18,6 @@ memory: seconds: name: Seconds description: The number of seconds to run the memory profiler. - example: 60.0 default: 60.0 selector: number: @@ -33,7 +31,6 @@ start_log_objects: scan_interval: name: Scan interval description: The number of seconds between logging objects. - example: 60.0 default: 30.0 selector: number: diff --git a/homeassistant/components/ps4/services.yaml b/homeassistant/components/ps4/services.yaml index fe7641357bf..f1f20506edb 100644 --- a/homeassistant/components/ps4/services.yaml +++ b/homeassistant/components/ps4/services.yaml @@ -6,7 +6,6 @@ send_command: name: Entity description: Name of entity to send command. required: true - example: "media_player.playstation_4" selector: entity: integration: ps4 @@ -15,7 +14,6 @@ send_command: name: Command description: Button to press. required: true - example: "ps" selector: select: options: diff --git a/homeassistant/components/rachio/services.yaml b/homeassistant/components/rachio/services.yaml index 93f63fcb9c3..bcd853b3ded 100644 --- a/homeassistant/components/rachio/services.yaml +++ b/homeassistant/components/rachio/services.yaml @@ -10,12 +10,10 @@ set_zone_moisture_percent: name: Percent description: Set the desired zone moisture percentage. required: true - example: 50 selector: number: min: 0 max: 100 - mode: slider unit_of_measurement: "%" start_multiple_zone_schedule: name: Start multiple zones @@ -45,13 +43,11 @@ pause_watering: duration: name: Duration description: The time to pause running schedules. - example: 30 default: 60 selector: number: min: 1 max: 60 - mode: slider unit_of_measurement: "minutes" resume_watering: name: Resume watering diff --git a/homeassistant/components/rainbird/services.yaml b/homeassistant/components/rainbird/services.yaml index 795fe5343d2..e1fa1879549 100644 --- a/homeassistant/components/rainbird/services.yaml +++ b/homeassistant/components/rainbird/services.yaml @@ -6,7 +6,6 @@ start_irrigation: name: Entity description: Name of a single irrigation to turn on required: true - example: "switch.sprinkler_1" selector: entity: integration: rainbird @@ -15,4 +14,8 @@ start_irrigation: name: Duration description: Duration for this sprinkler to be turned on required: true - example: 1 + selector: + number: + min: 1 + max: 1440 + unit_of_measurement: "minutes" diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index fa270692142..c12e0938059 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -11,7 +11,6 @@ disable_program: name: Program ID description: The program to disable. required: true - example: 3 selector: number: min: 1 @@ -28,7 +27,6 @@ disable_zone: name: Zone ID description: The zone to disable. required: true - example: 3 selector: number: min: 1 @@ -45,7 +43,6 @@ enable_program: name: Program ID description: The program to enable. required: true - example: 3 selector: number: min: 1 @@ -62,7 +59,6 @@ enable_zone: name: Zone ID description: The zone to enable. required: true - example: 3 selector: number: min: 1 @@ -79,7 +75,6 @@ pause_watering: name: Seconds description: The time to pause. required: true - example: 30 selector: number: min: 1 @@ -97,7 +92,6 @@ start_program: name: Program ID description: The program to start. required: true - example: 3 selector: number: min: 1 @@ -114,7 +108,6 @@ start_zone: name: Zone ID description: The zone to start. required: true - example: 3 selector: number: min: 1 @@ -122,8 +115,12 @@ start_zone: zone_run_time: name: Zone run time description: The number of seconds to run the zone. - example: 120 default: 600 + selector: + number: + min: 1 + max: 86400 + mode: box stop_all: name: Stop all description: Stop all watering activities. @@ -143,7 +140,6 @@ stop_program: name: Program ID description: The program to stop. required: true - example: 3 selector: number: min: 1 @@ -160,7 +156,6 @@ stop_zone: name: Zone ID description: The zone to stop. required: true - example: 3 selector: number: min: 1 diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index 67879867cc7..b2ea33fe2dd 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -7,19 +7,15 @@ purge: keep_days: name: Days to keep description: Number of history days to keep in database after purge. - example: 2 selector: number: min: 0 max: 365 - step: 1 unit_of_measurement: days - mode: slider repack: name: Repack description: Attempt to save disk space by rewriting the entire database file. - example: true default: false selector: boolean: @@ -27,7 +23,6 @@ purge: apply_filter: name: Apply filter description: Apply entity_id and event_type filter in addition to time based purge. - example: true default: false selector: boolean: diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index a36e33aa77d..3130484d10b 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -8,6 +8,7 @@ turn_on: domain: remote fields: activity: + name: Activity description: Activity ID or Activity Name to start. example: "BedroomTV" selector: @@ -38,6 +39,8 @@ send_command: name: Device description: Device ID to send command to. example: "32756745" + selector: + text: command: name: Command description: A single command or a list of commands to send. @@ -47,37 +50,32 @@ send_command: text: num_repeats: name: Repeats - description: An optional value that specifies the number of times you want to repeat the command(s). - example: "5" + description: The number of times you want to repeat the command(s). default: 1 selector: number: min: 0 max: 255 - step: 1 - mode: slider delay_secs: name: Delay Seconds - description: Specify the number of seconds you want to wait in between repeated commands. - example: "0.75" + description: The time you want to wait in between repeated commands. default: 0.4 selector: number: min: 0 max: 60 step: 0.1 - mode: slider + unit_of_measurement: seconds hold_secs: name: Hold Seconds - description: An optional value that specifies the number of seconds you want to have it held before the release is send. - example: "2.5" + description: The time you want to have it held before the release is send. default: 0 selector: number: min: 0 max: 60 step: 0.1 - mode: slider + unit_of_measurement: seconds learn_command: name: Learn Command @@ -87,8 +85,11 @@ learn_command: domain: remote fields: device: + name: Device description: Device ID to learn command from. example: "television" + selector: + text: command: name: Command description: A single command or a list of commands to learn. @@ -98,7 +99,6 @@ learn_command: command_type: name: Command Type description: The type of command to be learned. - example: "rf" default: "ir" selector: select: @@ -108,19 +108,17 @@ learn_command: alternative: name: Alternative description: If code must be stored as alternative (useful for discrete remotes). - example: "True" selector: boolean: timeout: name: Timeout - description: Timeout, in seconds, for the command to be learned. - example: "30" + description: Timeout for the command to be learned. selector: number: min: 0 max: 60 step: 5 - mode: slider + unit_of_measurement: seconds delete_command: name: Delete Command @@ -130,8 +128,11 @@ delete_command: domain: remote fields: device: + name: Device description: Name of the device from which commands will be deleted. example: "television" + selector: + text: command: name: Command description: A single command or a list of commands to delete. diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index eb7d6bb2ed3..3b780a939cc 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -10,16 +10,13 @@ turn_on: transition: name: Transition description: - Transition duration in seconds it takes to bring devices to the state + Transition duration it takes to bring devices to the state defined in the scene. - example: 2.5 selector: number: min: 0 max: 300 - step: 1 unit_of_measurement: seconds - mode: slider reload: name: Reload @@ -43,16 +40,13 @@ apply: transition: name: Transition description: - Transition duration in seconds it takes to bring devices to the state + Transition duration it takes to bring devices to the state defined in the scene. - example: 2.5 selector: number: min: 0 max: 300 - step: 1 unit_of_measurement: seconds - mode: slider create: name: Create diff --git a/homeassistant/components/screenlogic/services.yaml b/homeassistant/components/screenlogic/services.yaml index 7b54b9541d2..439d020a432 100644 --- a/homeassistant/components/screenlogic/services.yaml +++ b/homeassistant/components/screenlogic/services.yaml @@ -10,29 +10,28 @@ set_color_mode: name: Color Mode description: The ScreenLogic color mode to set required: true - example: "romance" selector: select: options: - all_off - all_on - - color_set - - color_sync - - color_swim - - party - - romance - - caribbean - american - - sunset + - blue + - caribbean + - color_set + - color_swim + - color_sync + - green + - hold + - magenta + - next_mode + - party + - recall + - red + - reset + - romance - royal - save - - recall - - blue - - green - - red - - white - - magenta + - sunset - thumper - - next_mode - - reset - - hold + - white diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml index 23a53313dc5..586ad3b4168 100644 --- a/homeassistant/components/sensibo/services.yaml +++ b/homeassistant/components/sensibo/services.yaml @@ -5,7 +5,6 @@ assume_state: entity_id: name: Entity description: Name(s) of entities to change. - example: "climate.kitchen" selector: entity: integration: sensibo diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 865ba4c8b2c..b9ee798f464 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -49,7 +49,6 @@ set_system_properties: alarm_duration: name: Alarm duration description: The length of a triggered alarm - example: 300 selector: number: min: 30 @@ -58,7 +57,6 @@ set_system_properties: alarm_volume: name: Alarm volume description: The volume level of a triggered alarm - example: 2 selector: select: options: @@ -69,7 +67,6 @@ set_system_properties: chime_volume: name: Chime volume description: The volume level of the door chime - example: 2 selector: select: options: @@ -80,45 +77,43 @@ set_system_properties: entry_delay_away: name: Entry delay away description: How long to delay when entering while "away" - example: 45 selector: number: min: 30 max: 255 + unit_of_measurement: seconds entry_delay_home: name: Entry delay home description: How long to delay when entering while "home" - example: 45 selector: number: min: 0 max: 255 + unit_of_measurement: seconds exit_delay_away: name: Exit delay away description: How long to delay when exiting while "away" - example: 45 selector: number: min: 45 max: 255 + unit_of_measurement: seconds exit_delay_home: name: Exit delay home description: How long to delay when exiting while "home" - example: 45 selector: number: min: 0 max: 255 + unit_of_measurement: seconds light: name: Light description: Whether the armed light should be visible - example: true selector: boolean: voice_prompt_volume: name: Voice prompt volume description: The volume level of the voice prompt - example: 2 selector: select: options: diff --git a/homeassistant/components/snapcast/services.yaml b/homeassistant/components/snapcast/services.yaml index 79839c33df2..f80b22dba7e 100644 --- a/homeassistant/components/snapcast/services.yaml +++ b/homeassistant/components/snapcast/services.yaml @@ -6,7 +6,6 @@ join: name: Master description: Entity ID of the player to synchronize to. required: true - example: "media_player.living_room" selector: entity: integration: snapcast @@ -56,8 +55,8 @@ set_latency: name: Latency description: Latency in master required: true - example: 14 selector: number: min: 1 max: 1000 + unit_of_measurement: "ms" diff --git a/homeassistant/components/snips/services.yaml b/homeassistant/components/snips/services.yaml index f4a36b6e781..407eab996c7 100644 --- a/homeassistant/components/snips/services.yaml +++ b/homeassistant/components/snips/services.yaml @@ -52,7 +52,6 @@ say_action: can_be_enqueued: name: Can be enqueued description: If True, session waits for an open session to end, if False session is dropped if one is running - example: true default: true selector: boolean: diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 09197fb87ae..365bdc29b37 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -7,7 +7,6 @@ join: description: Entity ID of the player that should become the coordinator of the group. required: true - example: "media_player.living_room_sonos" selector: entity: integration: sonos @@ -16,7 +15,6 @@ join: name: Entity description: Name of entity that will join the master. required: true - example: "media_player.living_room_sonos" selector: entity: integration: sonos @@ -37,7 +35,6 @@ snapshot: entity_id: name: Entity description: Name of entity that will be snapshot. - example: "media_player.living_room_sonos" selector: entity: integration: sonos @@ -45,7 +42,6 @@ snapshot: with_group: name: With group description: True or False. Also snapshot the group layout. - example: "true" default: true selector: boolean: @@ -57,7 +53,6 @@ restore: entity_id: name: Entity description: Name of entity that will be restored. - example: "media_player.living_room_sonos" selector: entity: integration: sonos @@ -65,7 +60,6 @@ restore: with_group: name: With group description: True or False. Also restore the group layout. - example: "true" default: true selector: boolean: @@ -80,14 +74,11 @@ set_sleep_timer: sleep_time: name: Sleep Time description: Number of seconds to set the timer. - example: "900" selector: number: min: 0 max: 7200 - step: 1 unit_of_measurement: seconds - mode: slider clear_sleep_timer: name: Clear timer @@ -112,19 +103,16 @@ set_option: night_sound: name: Night sound description: Enable Night Sound mode - example: "true" selector: boolean: speech_enhance: name: Speech enhance description: Enable Speech Enhancement mode - example: "true" selector: boolean: status_light: name: Status light description: Enable Status (LED) Light - example: "true" selector: boolean: @@ -138,7 +126,6 @@ play_queue: queue_position: name: Queue position description: Position of the song in the queue to start playing from. - example: "0" selector: number: min: 0 @@ -155,7 +142,6 @@ remove_from_queue: queue_position: name: Queue position description: Position in the queue to remove. - example: "0" selector: number: min: 0 @@ -172,7 +158,6 @@ update_alarm: alarm_id: name: Alarm ID description: ID for the alarm to be updated. - example: "1" required: true selector: number: @@ -188,22 +173,18 @@ update_alarm: volume: name: Volume description: Set alarm volume level. - example: "0.75" selector: number: min: 0 max: 1 step: 0.01 - mode: slider enabled: name: Alarm enabled description: Enable or disable the alarm. - example: "true" selector: boolean: include_linked_zones: name: Include linked zones description: Enable or disable including grouped rooms. - example: "true" selector: boolean: diff --git a/homeassistant/components/starline/services.yaml b/homeassistant/components/starline/services.yaml index 970010ffea0..4c3e4d360e8 100644 --- a/homeassistant/components/starline/services.yaml +++ b/homeassistant/components/starline/services.yaml @@ -10,7 +10,6 @@ set_scan_interval: scan_interval: name: Scan interval description: Update frequency. - example: 180 selector: number: min: 10 @@ -25,7 +24,6 @@ set_scan_obd_interval: scan_interval: name: Scan interval description: Update frequency. - example: 10800 selector: number: min: 180 diff --git a/homeassistant/components/surepetcare/services.yaml b/homeassistant/components/surepetcare/services.yaml index 6542cfce188..77887a18b87 100644 --- a/homeassistant/components/surepetcare/services.yaml +++ b/homeassistant/components/surepetcare/services.yaml @@ -13,7 +13,6 @@ set_lock_state: name: Lock state description: New lock state. required: true - example: "unlocked" selector: select: options: diff --git a/homeassistant/components/switcher_kis/services.yaml b/homeassistant/components/switcher_kis/services.yaml index eed3cac0268..b4b2728fc2e 100644 --- a/homeassistant/components/switcher_kis/services.yaml +++ b/homeassistant/components/switcher_kis/services.yaml @@ -26,7 +26,6 @@ turn_on_with_timer: name: Timer description: 'Time to turn on.' required: true - example: '30' selector: number: min: 1 diff --git a/homeassistant/components/system_bridge/services.yaml b/homeassistant/components/system_bridge/services.yaml index 0ee12b39846..3f79f441415 100644 --- a/homeassistant/components/system_bridge/services.yaml +++ b/homeassistant/components/system_bridge/services.yaml @@ -5,7 +5,6 @@ send_command: bridge: name: Bridge description: The server to send the command to. - example: "" required: true selector: device: @@ -32,7 +31,6 @@ open: bridge: name: Bridge description: The server to talk to. - example: "" required: true selector: device: diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml index a762c31f205..b6444bcecc5 100644 --- a/homeassistant/components/system_log/services.yaml +++ b/homeassistant/components/system_log/services.yaml @@ -15,9 +15,8 @@ write: text: level: name: Level - description: "Log level: debug, info, warning, error, critical." + description: "Log level." default: error - example: debug selector: select: options: diff --git a/homeassistant/components/tado/services.yaml b/homeassistant/components/tado/services.yaml index f73eaa8a183..d3aaa71cbbc 100644 --- a/homeassistant/components/tado/services.yaml +++ b/homeassistant/components/tado/services.yaml @@ -18,7 +18,6 @@ set_climate_timer: name: Temperature description: Temperature to set climate entity to required: true - example: 25 selector: number: min: 0 @@ -45,7 +44,6 @@ set_water_heater_timer: temperature: name: Temperature description: Temperature to set heater to - example: 25 selector: number: min: 0 @@ -64,7 +62,6 @@ set_climate_temperature_offset: offset: name: Offset description: Offset you would like (depending on your device). - example: -1.2 default: 0 selector: number: diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index de537aac5ad..dc3e9dde2d3 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -44,8 +44,13 @@ send_message: boolean: timeout: name: Timeout - description: Timeout for send message. Will help with timeout errors (poor internet connection, etc) - example: "1000" + description: Timeout for send message. Will help with timeout errors (poor internet connection, etc)s + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds + keyboard: name: Keyboard description: List of rows of commands, comma-separated, to make a custom keyboard. Empty list clears a previously set keyboard. @@ -108,7 +113,6 @@ send_photo: parse_mode: name: Parse mode description: "Parser for the message text." - example: "html" selector: select: options: @@ -128,7 +132,6 @@ send_photo: timeout: name: Timeout description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc) - example: "1000" selector: number: min: 1 @@ -276,13 +279,11 @@ send_animation: disable_notification: name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. - example: true selector: boolean: verify_ssl: name: Verify SSL description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. - example: false selector: boolean: timeout: @@ -512,7 +513,6 @@ send_document: parse_mode: name: Parse mode description: "Parser for the message text." - example: "html" selector: select: options: @@ -651,7 +651,6 @@ edit_message: parse_mode: name: Parse mode description: "Parser for the message text." - example: "html" selector: select: options: diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index 7f2ddd0091d..85e975e94ff 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -25,7 +25,6 @@ new_task: priority: name: Priority description: The priority of this task, from 1 (normal) to 4 (urgent). - example: 2 selector: number: min: 1 diff --git a/homeassistant/components/tts/services.yaml b/homeassistant/components/tts/services.yaml index f5a5154a029..f93a81cf2b2 100644 --- a/homeassistant/components/tts/services.yaml +++ b/homeassistant/components/tts/services.yaml @@ -6,8 +6,7 @@ say: fields: entity_id: name: Entity - description: Name(s) of media player entities. - example: "media_player.floor" + description: Name(s) of media player entities.\ required: true selector: entity: @@ -22,7 +21,6 @@ say: cache: name: Cache description: Control file cache of this message. - example: "true" default: false selector: boolean: diff --git a/homeassistant/components/wake_on_lan/services.yaml b/homeassistant/components/wake_on_lan/services.yaml index 7540451d061..ea374a88b8f 100644 --- a/homeassistant/components/wake_on_lan/services.yaml +++ b/homeassistant/components/wake_on_lan/services.yaml @@ -18,7 +18,7 @@ send_magic_packet: broadcast_port: name: Broadcast port description: Port where to send the magic packet. - example: 9 + default: 9 selector: number: min: 1 diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml index 86e1c52aef6..f9d56cd1921 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -63,7 +63,6 @@ select_sound_output: name: Entity description: Name(s) of the webostv entities to change sound output on. required: true - example: "media_player.living_room_tv" selector: entity: integration: webostv diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml index f7f21125f27..851f3bb9a43 100644 --- a/homeassistant/components/wink/services.yaml +++ b/homeassistant/components/wink/services.yaml @@ -147,8 +147,7 @@ siren_set_auto_shutoff: auto_shutoff: name: Auto shutoff description: >- - The time in seconds to sound the siren. One of [None, -1, 30, 60, 120] - (None and -1 are forever. Use None for gocontrol, and -1 for Dome) + The time in seconds to sound the siren. (None and -1 are forever. Use None for gocontrol, and -1 for Dome) required: true selector: select: @@ -382,7 +381,7 @@ set_lock_alarm_state: domain: lock enabled: name: Enabled - description: enable or disable. true or false. + description: enable or disable. required: true selector: boolean: @@ -400,7 +399,7 @@ set_lock_beeper_state: domain: lock enabled: name: Enabled - description: enable or disable. true or false. + description: enable or disable. required: true selector: boolean: diff --git a/homeassistant/components/wled/services.yaml b/homeassistant/components/wled/services.yaml index 3ade18cb70e..f8d636686be 100644 --- a/homeassistant/components/wled/services.yaml +++ b/homeassistant/components/wled/services.yaml @@ -15,13 +15,10 @@ effect: intensity: name: Effect intensity description: Intensity of the effect. Number between 0 and 255. - example: 100 selector: number: min: 0 max: 255 - step: 1 - mode: slider palette: name: Color palette description: Name or ID of the WLED light palette. @@ -30,20 +27,16 @@ effect: text: speed: name: Effect speed - description: Speed of the effect. Number between 0 (slow) and 255 (fast). - example: 150 + description: Speed of the effect. selector: number: min: 0 max: 255 - step: 1 - mode: slider reverse: name: Reverse effect description: Reverse the effect. Either true to reverse or false otherwise. default: false - example: false selector: boolean: @@ -58,7 +51,6 @@ preset: preset: name: Preset ID description: ID of the WLED preset - example: 6 selector: number: min: -1 diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml index 92b184d0497..b365f273e31 100644 --- a/homeassistant/components/yeelight/services.yaml +++ b/homeassistant/components/yeelight/services.yaml @@ -75,7 +75,6 @@ set_color_temp_scene: kelvin: name: Kelvin description: Color temperature for the light in Kelvin. - example: 4000 selector: number: min: 1700 @@ -89,6 +88,7 @@ set_color_temp_scene: number: min: 0 max: 100 + unit_of_measurement: "%" set_color_flow_scene: name: Set color flow scene description: starts a color flow. If the light is off, it will be turned on. @@ -108,7 +108,6 @@ set_color_flow_scene: action: name: Action description: The action to take after the flow stops. - example: "stay" default: 'recover' selector: select: @@ -133,7 +132,6 @@ set_auto_delay_off_scene: minutes: name: Minutes description: The time to wait before automatically turning the light off. - example: 5 selector: number: min: 1 @@ -146,6 +144,7 @@ set_auto_delay_off_scene: number: min: 0 max: 100 + unit_of_measurement: "%" start_flow: name: Start flow description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index 63f30c2e3f1..0e645da365e 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -7,7 +7,6 @@ permit: duration: name: Duration description: Time to permit joins, in seconds - example: 60 default: 60 selector: number: @@ -77,24 +76,28 @@ set_zigbee_cluster_attribute: description: IEEE address for the device required: true example: "00:0d:6f:00:05:7d:2d:34" + selector: + text: endpoint_id: name: Endpoint ID description: Endpoint id for the cluster required: true - example: 1 + selector: + number: + min: 1 + max: 65535 + mode: box cluster_id: name: Cluster ID description: ZCL cluster to retrieve attributes for required: true - example: 6 selector: number: min: 1 max: 65535 cluster_type: name: Cluster Type - description: type of the cluster (in or out) - example: "out" + description: type of the cluster default: "in" selector: select: @@ -140,7 +143,6 @@ issue_zigbee_cluster_command: name: Endpoint ID description: Endpoint id for the cluster required: true - example: 1 selector: number: min: 1 @@ -149,15 +151,13 @@ issue_zigbee_cluster_command: name: Cluster ID description: ZCL cluster to retrieve attributes for required: true - example: 6 selector: number: min: 1 max: 65535 cluster_type: name: Cluster Type - description: type of the cluster (in or out) - example: "out" + description: type of the cluster default: "in" selector: select: @@ -168,16 +168,14 @@ issue_zigbee_cluster_command: name: Command description: id of the command to execute required: true - example: 0 selector: number: min: 1 max: 65535 command_type: name: Command Type - description: type of the command to execute (client or server) + description: type of the command to execute required: true - example: "server" selector: select: options: @@ -212,15 +210,13 @@ issue_zigbee_group_command: name: Cluster ID description: ZCL cluster to send command to required: true - example: 6 selector: number: min: 1 max: 65535 cluster_type: name: Cluster Type - description: type of the cluster (in or out) - example: "out" + description: type of the cluster default: "in" selector: select: @@ -231,7 +227,6 @@ issue_zigbee_group_command: name: Command description: id of the command to execute required: true - example: 0 selector: number: min: 1 @@ -265,7 +260,6 @@ warning_device_squawk: name: 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 default: 0 selector: number: @@ -276,7 +270,6 @@ warning_device_squawk: name: 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 default: 1 selector: number: @@ -287,7 +280,6 @@ warning_device_squawk: name: 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 default: 2 selector: number: @@ -311,7 +303,6 @@ warning_device_warn: name: Mode description: >- The Warning Mode field is used as an 4-bit enumeration, can have one of the values 0-6 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 default: 3 selector: number: @@ -322,7 +313,6 @@ warning_device_warn: name: 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. "0" means no strobe, "1" means strobe. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. - example: 1 default: 1 selector: number: @@ -333,7 +323,6 @@ warning_device_warn: name: 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 default: 2 selector: number: @@ -344,7 +333,6 @@ warning_device_warn: name: Duration description: >- Requested duration of warning, in seconds (16 bit). If both Strobe and Warning Mode are "0" this field SHALL be ignored. - example: 2 default: 5 selector: number: @@ -355,7 +343,6 @@ warning_device_warn: name: 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: 50 default: 0 selector: number: @@ -366,7 +353,6 @@ warning_device_warn: name: 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 default: 2 selector: number: diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index db74292ff8a..d3063ef5d43 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -15,7 +15,6 @@ change_association: name: Node ID description: Node id of the node to set association for. required: true - example: 10 selector: number: min: 1 @@ -24,7 +23,6 @@ change_association: name: Target node ID description: Node id of the node to associate to. required: true - example: 42 selector: number: min: 1 @@ -197,7 +195,10 @@ set_poll_intensity: name: Node ID description: ID of the node to set polling to. required: true - example: 10 + selector: + number: + min: 1 + max: 255 value_id: name: Value ID description: ID of the value to set polling to. diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index f9d90f94779..84877189298 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -108,8 +108,6 @@ refresh_value: refresh_all_values: name: Refresh all values? description: Whether to refresh all values (true) or just the primary value (false) - required: false - example: true default: false selector: boolean: @@ -159,7 +157,6 @@ set_value: wait_for_result: name: Wait for result? description: Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device. - example: false required: false selector: boolean: From 7c9d8cfdecb55f5906bacb66db6ddc32748fddc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 25 May 2021 14:47:09 +0200 Subject: [PATCH 718/852] Miflora, add STATE_CLASS_MEASUREMENT (#50971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Miflora, add STATE_CLASS_MEASUREMENT Signed-off-by: Daniel Hjelseth Høyer * Miflora, add STATE_CLASS_MEASUREMENT Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/miflora/sensor.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 0c22a943bb3..a7aab41bea9 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -8,7 +8,11 @@ from btlewrap import BluetoothBackendException from miflora import miflora_poller import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( CONDUCTIVITY, CONF_FORCE_UPDATE, @@ -197,6 +201,11 @@ class MiFloraSensor(SensorEntity): """Return the device class.""" return self._device_class + @property + def state_class(self): + """Return the state class of this entity.""" + return STATE_CLASS_MEASUREMENT + @property def unit_of_measurement(self): """Return the units of measurement.""" From cbe4df18931288b691111658c5b7a97e374d8396 Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Tue, 25 May 2021 20:36:03 +0700 Subject: [PATCH 719/852] SSDP Discovery for NDMS2 routers (#47312) Co-authored-by: Franck Nijhof --- .../components/keenetic_ndms2/config_flow.py | 61 +++++++++-- .../components/keenetic_ndms2/manifest.json | 10 ++ .../components/keenetic_ndms2/strings.json | 5 +- .../keenetic_ndms2/translations/en.json | 7 +- .../keenetic_ndms2/translations/ru.json | 3 +- homeassistant/generated/ssdp.py | 10 ++ tests/components/keenetic_ndms2/__init__.py | 11 +- .../keenetic_ndms2/test_config_flow.py | 100 ++++++++++++++++-- 8 files changed, 185 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index 4377a1094fc..c82524a3410 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -1,10 +1,13 @@ """Config flow for Keenetic NDMS2.""" from __future__ import annotations +from urllib.parse import urlparse + from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -14,7 +17,9 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CONF_CONSIDER_HOME, @@ -39,19 +44,22 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> KeeneticOptionsFlowHandler: """Get the options flow for this handler.""" return KeeneticOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: - self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + host = self.context.get(CONF_HOST) or user_input[CONF_HOST] + self._async_abort_entries_match({CONF_HOST: host}) _client = Client( TelnetConnection( - user_input[CONF_HOST], + host, user_input[CONF_PORT], user_input[CONF_USERNAME], user_input[CONF_PASSWORD], @@ -66,13 +74,19 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except ConnectionException: errors["base"] = "cannot_connect" else: - return self.async_create_entry(title=router_info.name, data=user_input) + return self.async_create_entry( + title=router_info.name, data={CONF_HOST: host, **user_input} + ) + + host_schema = ( + {vol.Required(CONF_HOST): str} if CONF_HOST not in self.context else {} + ) return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_HOST): str, + **host_schema, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_PORT, default=DEFAULT_TELNET_PORT): int, @@ -81,10 +95,37 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input=None): + async def async_step_import( + self, user_input: ConfigType | None = None + ) -> FlowResult: """Import a config entry.""" return await self.async_step_user(user_input) + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + """Handle a discovered device.""" + friendly_name = discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") + + # Filter out items not having "keenetic" in their name + if "keenetic" not in friendly_name.lower(): + return self.async_abort(reason="not_keenetic_ndms2") + + # Filters out items having no/empty UDN + if not discovery_info.get(ssdp.ATTR_UPNP_UDN): + return self.async_abort(reason="no_udn") + + host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname + await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) + + self._async_abort_entries_match({CONF_HOST: host}) + + self.context[CONF_HOST] = host + self.context["title_placeholders"] = { + "name": friendly_name, + "host": host, + } + + return await self.async_step_user() + class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): """Handle options.""" @@ -94,7 +135,7 @@ class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): self.config_entry = config_entry self._interface_options = {} - async def async_step_init(self, _user_input=None): + async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: """Manage the options.""" router: KeeneticRouter = self.hass.data[DOMAIN][self.config_entry.entry_id][ ROUTER @@ -111,7 +152,7 @@ class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): } return await self.async_step_user() - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Manage the device tracker options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 7e1e7166da9..3f01c9091c7 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -4,6 +4,16 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": ["ndms2_client==0.1.1"], + "ssdp": [ + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Keenetic Ltd." + }, + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "ZyXEL Communications Corp." + } + ], "codeowners": ["@foxel"], "iot_class": "local_polling" } diff --git a/homeassistant/components/keenetic_ndms2/strings.json b/homeassistant/components/keenetic_ndms2/strings.json index 0dc1c9c302f..13e3fabfbff 100644 --- a/homeassistant/components/keenetic_ndms2/strings.json +++ b/homeassistant/components/keenetic_ndms2/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name} ({host})", "step": { "user": { "title": "Set up Keenetic NDMS2 Router", @@ -15,7 +16,9 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "no_udn": "SSDP discovery info has no UDN", + "not_keenetic_ndms2": "Discovered item is not a Keenetic router" } }, "options": { diff --git a/homeassistant/components/keenetic_ndms2/translations/en.json b/homeassistant/components/keenetic_ndms2/translations/en.json index 5a946751ff4..aafcf284e86 100644 --- a/homeassistant/components/keenetic_ndms2/translations/en.json +++ b/homeassistant/components/keenetic_ndms2/translations/en.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "no_udn": "SSDP discovery info has no UDN", + "not_keenetic_ndms2": "Discovered item is no a Keenetic router" }, "error": { "cannot_connect": "Failed to connect" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { @@ -32,4 +35,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/keenetic_ndms2/translations/ru.json b/homeassistant/components/keenetic_ndms2/translations/ru.json index 191dfbb1f04..fefcf6a4093 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ru.json +++ b/homeassistant/components/keenetic_ndms2/translations/ru.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { @@ -32,4 +33,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 4141de31f73..0f6c01a0605 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -136,6 +136,16 @@ SSDP = { "manufacturer": "Universal Devices Inc." } ], + "keenetic_ndms2": [ + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Keenetic Ltd." + }, + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "ZyXEL Communications Corp." + } + ], "konnected": [ { "manufacturer": "konnected.io" diff --git a/tests/components/keenetic_ndms2/__init__.py b/tests/components/keenetic_ndms2/__init__.py index 1fce0dbe2a6..9f96e56cdd0 100644 --- a/tests/components/keenetic_ndms2/__init__.py +++ b/tests/components/keenetic_ndms2/__init__.py @@ -1,4 +1,5 @@ """Tests for the Keenetic NDMS2 component.""" +from homeassistant.components import ssdp from homeassistant.components.keenetic_ndms2 import const from homeassistant.const import ( CONF_HOST, @@ -9,9 +10,11 @@ from homeassistant.const import ( ) MOCK_NAME = "Keenetic Ultra 2030" +MOCK_IP = "0.0.0.0" +SSDP_LOCATION = f"http://{MOCK_IP}/" MOCK_DATA = { - CONF_HOST: "0.0.0.0", + CONF_HOST: MOCK_IP, CONF_USERNAME: "user", CONF_PASSWORD: "pass", CONF_PORT: 23, @@ -25,3 +28,9 @@ MOCK_OPTIONS = { const.CONF_INCLUDE_ASSOCIATED: True, const.CONF_INTERFACES: ["Home", "VPS0"], } + +MOCK_SSDP_DISCOVERY_INFO = { + ssdp.ATTR_SSDP_LOCATION: SSDP_LOCATION, + ssdp.ATTR_UPNP_UDN: "uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_NAME, +} diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 7561fb03839..7e7d4882544 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -7,11 +7,12 @@ from ndms2_client.client import InterfaceInfo, RouterInfo import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components import keenetic_ndms2 as keenetic +from homeassistant.components import keenetic_ndms2 as keenetic, ssdp from homeassistant.components.keenetic_ndms2 import const +from homeassistant.const import CONF_HOST, CONF_SOURCE from homeassistant.core import HomeAssistant -from . import MOCK_DATA, MOCK_NAME, MOCK_OPTIONS +from . import MOCK_DATA, MOCK_NAME, MOCK_OPTIONS, MOCK_SSDP_DISCOVERY_INFO from tests.common import MockConfigEntry @@ -43,7 +44,7 @@ def mock_keenetic_connect_failed(): yield -async def test_flow_works(hass: HomeAssistant, connect): +async def test_flow_works(hass: HomeAssistant, connect) -> None: """Test config flow.""" result = await hass.config_entries.flow.async_init( @@ -67,7 +68,7 @@ async def test_flow_works(hass: HomeAssistant, connect): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_works(hass: HomeAssistant, connect): +async def test_import_works(hass: HomeAssistant, connect) -> None: """Test config flow.""" with patch( @@ -86,7 +87,7 @@ async def test_import_works(hass: HomeAssistant, connect): assert len(mock_setup_entry.mock_calls) == 1 -async def test_options(hass): +async def test_options(hass: HomeAssistant) -> None: """Test updating options.""" entry = MockConfigEntry(domain=keenetic.DOMAIN, data=MOCK_DATA) entry.add_to_hass(hass) @@ -127,7 +128,7 @@ async def test_options(hass): assert result2["data"] == MOCK_OPTIONS -async def test_host_already_configured(hass, connect): +async def test_host_already_configured(hass: HomeAssistant, connect) -> None: """Test host already configured.""" entry = MockConfigEntry( @@ -147,7 +148,7 @@ async def test_host_already_configured(hass, connect): assert result2["reason"] == "already_configured" -async def test_connection_error(hass, connect_error): +async def test_connection_error(hass: HomeAssistant, connect_error) -> None: """Test error when connection is unsuccessful.""" result = await hass.config_entries.flow.async_init( @@ -158,3 +159,88 @@ async def test_connection_error(hass, connect_error): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} + + +async def test_ssdp_works(hass: HomeAssistant, connect) -> None: + """Test host already configured and discovered.""" + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True + ) as mock_setup_entry: + user_input = MOCK_DATA.copy() + user_input.pop(CONF_HOST) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == MOCK_NAME + assert result2["data"] == MOCK_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_ssdp_already_configured(hass: HomeAssistant) -> None: + """Test host already configured and discovered.""" + + entry = MockConfigEntry( + domain=keenetic.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS + ) + entry.add_to_hass(hass) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: + """Discovered device has no UDN.""" + + discovery_info = { + **MOCK_SSDP_DISCOVERY_INFO, + } + discovery_info.pop(ssdp.ATTR_UPNP_UDN) + + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_udn" + + +async def test_ssdp_reject_non_keenetic(hass: HomeAssistant) -> None: + """Discovered device does not look like a keenetic router.""" + + discovery_info = { + **MOCK_SSDP_DISCOVERY_INFO, + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Suspicious device", + } + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_keenetic_ndms2" From 67804ee5df3d7d09c8d357b7a11902c7f198e6fa Mon Sep 17 00:00:00 2001 From: Jeroen Peters Date: Tue, 25 May 2021 15:36:19 +0200 Subject: [PATCH 720/852] Bump yeelight to 0.6.3 (#51065) --- homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 9d82a4fe56e..0bf6249b647 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.6.2"], + "requirements": ["yeelight==0.6.3"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index e02fdf5d5ad..896fbae232b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2397,7 +2397,7 @@ yalesmartalarmclient==0.3.3 yalexs==1.1.11 # homeassistant.components.yeelight -yeelight==0.6.2 +yeelight==0.6.3 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb53e0c013d..fd922ab360e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1297,7 +1297,7 @@ xmltodict==0.12.0 yalexs==1.1.11 # homeassistant.components.yeelight -yeelight==0.6.2 +yeelight==0.6.3 # homeassistant.components.onvif zeep[async]==4.0.0 From ae8652217c058ba51410475431516603461c4a63 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 May 2021 15:46:54 +0200 Subject: [PATCH 721/852] Change utility_meter last_reset timestamps to UTC (#51067) --- homeassistant/components/utility_meter/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 94a9e0d9175..509c0562f97 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -131,7 +131,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self._sensor_source_id = source_entity self._state = 0 self._last_period = 0 - self._last_reset = dt_util.now() + self._last_reset = dt_util.utcnow() self._collecting = None if name: self._name = name @@ -237,7 +237,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): if self._tariff_entity != entity_id: return _LOGGER.debug("Reset utility meter <%s>", self.entity_id) - self._last_reset = dt_util.now() + self._last_reset = dt_util.utcnow() self._last_period = str(self._state) self._state = 0 self.async_write_ha_state() @@ -284,8 +284,8 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self._state = Decimal(state.state) self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._last_period = state.attributes.get(ATTR_LAST_PERIOD) - self._last_reset = dt_util.parse_datetime( - state.attributes.get(ATTR_LAST_RESET) + self._last_reset = dt_util.as_utc( + dt_util.parse_datetime(state.attributes.get(ATTR_LAST_RESET)) ) if state.attributes.get(ATTR_STATUS) == COLLECTING: # Fake cancellation function to init the meter in similar state From 9a208e37614c7baee3c21b59c4d595e06ed391d0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 25 May 2021 15:51:42 +0200 Subject: [PATCH 722/852] Upgrade pre-commit to 2.13.0 (#51068) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index b12842c78dd..02b041d6074 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ coverage==5.5 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.812 -pre-commit==2.12.1 +pre-commit==2.13.0 pylint==2.8.2 pipdeptree==1.0.0 pylint-strict-informational==0.1 From de74028958d3fbf01119fe1259596c60206cdbce Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 25 May 2021 16:09:23 +0200 Subject: [PATCH 723/852] Disable ee_brightbox integration (#51069) --- homeassistant/components/ee_brightbox/manifest.json | 1 + requirements_all.txt | 3 --- requirements_test_all.txt | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/ee_brightbox/manifest.json b/homeassistant/components/ee_brightbox/manifest.json index c477b9fb339..b7aae9f5a87 100644 --- a/homeassistant/components/ee_brightbox/manifest.json +++ b/homeassistant/components/ee_brightbox/manifest.json @@ -1,6 +1,7 @@ { "domain": "ee_brightbox", "name": "EE Bright Box", + "disabled": "Library has incompatible requirements.", "documentation": "https://www.home-assistant.io/integrations/ee_brightbox", "requirements": ["eebrightbox==0.0.4"], "codeowners": [], diff --git a/requirements_all.txt b/requirements_all.txt index 896fbae232b..16576c9d07f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -532,9 +532,6 @@ ebusdpy==0.0.16 # homeassistant.components.ecoal_boiler ecoaliface==0.4.0 -# homeassistant.components.ee_brightbox -eebrightbox==0.0.4 - # homeassistant.components.elgato elgato==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd922ab360e..b77a1c301d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -290,9 +290,6 @@ dsmr_parser==0.29 # homeassistant.components.dynalite dynalite_devices==0.1.46 -# homeassistant.components.ee_brightbox -eebrightbox==0.0.4 - # homeassistant.components.elgato elgato==2.1.0 From 26563e3ea49531f697d7ef73a95b40188dd1f1c3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 25 May 2021 17:03:37 +0200 Subject: [PATCH 724/852] Add statistics websocket endpoint (#51044) Co-authored-by: Erik --- homeassistant/components/history/__init__.py | 48 ++++++++ tests/components/history/test_init.py | 117 +++++++++++++++++++ 2 files changed, 165 insertions(+) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 9b009e0fcea..c92718a87e4 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -11,9 +11,11 @@ from aiohttp import web from sqlalchemy import not_, or_ import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder import history from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.statistics import statistics_during_period from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( CONF_DOMAINS, @@ -101,10 +103,56 @@ async def async_setup(hass, config): hass.components.frontend.async_register_built_in_panel( "history", "history", "hass:poll-box" ) + hass.components.websocket_api.async_register_command( + ws_get_statistics_during_period + ) return True +@websocket_api.websocket_command( + { + vol.Required("type"): "history/statistics_during_period", + vol.Required("start_time"): str, + vol.Optional("end_time"): str, + vol.Optional("statistic_id"): str, + } +) +@websocket_api.async_response +async def ws_get_statistics_during_period( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle statistics websocket command.""" + start_time_str = msg["start_time"] + end_time_str = msg.get("end_time") + + start_time = dt_util.parse_datetime(start_time_str) + if start_time: + start_time = dt_util.as_utc(start_time) + else: + connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") + return + + if end_time_str: + end_time = dt_util.parse_datetime(end_time_str) + if end_time: + end_time = dt_util.as_utc(end_time) + else: + connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") + return + else: + end_time = None + + statistics = await hass.async_add_executor_job( + statistics_during_period, + hass, + start_time, + end_time, + msg.get("statistic_id"), + ) + connection.send_result(msg["id"], {"statistics": statistics}) + + class HistoryPeriodView(HomeAssistantView): """Handle history period requests.""" diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index bf6f392b649..36dd3f30156 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -826,3 +826,120 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(hass, hass_clien assert len(response_json) == 2 assert response_json[0][0]["entity_id"] == "light.kitchen" assert response_json[1][0]["entity_id"] == "light.cow" + + +async def test_statistics_during_period(hass, hass_ws_client): + """Test statistics_during_period.""" + now = dt_util.utcnow() + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + hass.states.async_set( + "sensor.test", + 10, + attributes={"device_class": "temperature", "state_class": "measurement"}, + ) + await hass.async_block_till_done() + + await hass.async_add_executor_job(trigger_db_commit, hass) + await hass.async_block_till_done() + + hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(period="hourly", start=now) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"statistics": {}} + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "statistics": { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": now.isoformat(), + "mean": 10.0, + "min": 10.0, + "max": 10.0, + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + } + + +async def test_statistics_during_period_bad_start_time(hass, hass_ws_client): + """Test statistics_during_period.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_statistics_during_period_bad_end_time(hass, hass_ws_client): + """Test statistics_during_period.""" + now = dt_util.utcnow() + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "end_time": "dogs", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" From 5d79a8fb05c697a61c3419ea46b24309f23d69a5 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 25 May 2021 17:05:57 +0200 Subject: [PATCH 725/852] Add statistics websocket endpoint (#51044) Co-authored-by: Erik From c98f9619598afae5442a746d88822cc00e040416 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 25 May 2021 17:06:24 +0200 Subject: [PATCH 726/852] Add statistics websocket endpoint (#51044) Co-authored-by: Erik From f4dc72c0bdfa64ae35b2bacbf0af2df68a846e2d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 25 May 2021 17:14:43 +0200 Subject: [PATCH 727/852] Add statistics websocket endpoint (#51044) Co-authored-by: Erik From e9ff4b1342111f0cf97f035e8e3dad750e9e503f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 25 May 2021 17:35:40 +0200 Subject: [PATCH 728/852] Fix alexa not discovering devices when sound mode device present (#49628) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/alexa/entities.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 344ba2b7d21..723d115b923 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -40,6 +40,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import network +from homeassistant.helpers.entity import entity_sources from homeassistant.util.decorator import Registry from .capabilities import ( @@ -615,7 +616,13 @@ class MediaPlayerCapabilities(AlexaEntity): if supported & media_player.const.SUPPORT_PLAY_MEDIA: yield AlexaChannelController(self.entity) - if supported & media_player.const.SUPPORT_SELECT_SOUND_MODE: + # AlexaEqualizerController is disabled for denonavr + # since it blocks alexa from discovering any devices. + domain = entity_sources(self.hass).get(self.entity_id, {}).get("domain") + if ( + supported & media_player.const.SUPPORT_SELECT_SOUND_MODE + and domain != "denonavr" + ): inputs = AlexaEqualizerController.get_valid_inputs( self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST, []) ) From 4875035ff8f4b7bf66cd0e7b0a9c68d152b3015a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 25 May 2021 11:37:12 -0400 Subject: [PATCH 729/852] Add zwave_js WS API commands for node ping and metadata (#51049) --- homeassistant/components/zwave_js/api.py | 56 ++ tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_api.py | 147 +++- .../wallmote_central_scene_state.json | 698 ++++++++++++++++++ 4 files changed, 907 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/zwave_js/wallmote_central_scene_state.json diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 32c23fe760b..8a08da13bfc 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -127,6 +127,8 @@ def async_register_api(hass: HomeAssistant) -> None: """Register all of our api endpoints.""" websocket_api.async_register_command(hass, websocket_network_status) websocket_api.async_register_command(hass, websocket_node_status) + websocket_api.async_register_command(hass, websocket_node_metadata) + websocket_api.async_register_command(hass, websocket_ping_node) websocket_api.async_register_command(hass, websocket_add_node) websocket_api.async_register_command(hass, websocket_stop_inclusion) websocket_api.async_register_command(hass, websocket_remove_node) @@ -209,6 +211,60 @@ async def websocket_node_status( ) +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/node_metadata", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_node_metadata( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Get the metadata of a Z-Wave JS node.""" + data = { + "node_id": node.node_id, + "exclusion": node.device_config.metadata.exclusion, + "inclusion": node.device_config.metadata.inclusion, + "manual": node.device_config.metadata.manual, + "wakeup": node.device_config.metadata.wakeup, + "reset": node.device_config.metadata.reset, + "device_database_url": node.device_database_url, + } + connection.send_result( + msg[ID], + data, + ) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/ping_node", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_ping_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Ping a Z-Wave JS node.""" + result = await node.async_ping() + connection.send_result( + msg[ID], + result, + ) + + @websocket_api.require_admin @websocket_api.websocket_command( { diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index eef79b533d3..3f5b1fbe88d 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -371,6 +371,12 @@ def zem_31_state_fixture(): return json.loads(load_fixture("zwave_js/zen_31_state.json")) +@pytest.fixture(name="wallmote_central_scene_state", scope="session") +def wallmote_central_scene_state_fixture(): + """Load the wallmote central scene node state fixture data.""" + return json.loads(load_fixture("zwave_js/wallmote_central_scene_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state): """Mock a client.""" @@ -711,3 +717,11 @@ def zen_31_fixture(client, zen_31_state): node = Node(client, copy.deepcopy(zen_31_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="wallmote_central_scene") +def wallmote_central_scene_fixture(client, wallmote_central_scene_state): + """Mock a wallmote central scene node.""" + node = Node(client, copy.deepcopy(wallmote_central_scene_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 141998526ee..596ed9c0ed9 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -59,7 +59,7 @@ async def test_network_status(hass, integration, hass_ws_client): assert msg["error"]["code"] == ERR_NOT_LOADED -async def test_node_status(hass, integration, multisensor_6, hass_ws_client): +async def test_node_status(hass, multisensor_6, integration, hass_ws_client): """Test the node status websocket command.""" entry = integration ws_client = await hass_ws_client(hass) @@ -113,8 +113,139 @@ async def test_node_status(hass, integration, multisensor_6, hass_ws_client): assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_client): + """Test the node metadata websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + node = wallmote_central_scene + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/node_metadata", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result[NODE_ID] == 35 + assert result["inclusion"] == ( + "To add the ZP3111 to the Z-Wave network (inclusion), place the Z-Wave " + "primary controller into inclusion mode. Press the Program Switch of ZP3111 " + "for sending the NIF. After sending NIF, Z-Wave will send the auto inclusion, " + "otherwise, ZP3111 will go to sleep after 20 seconds." + ) + assert result["exclusion"] == ( + "To remove the ZP3111 from the Z-Wave network (exclusion), place the Z-Wave " + "primary controller into \u201cexclusion\u201d mode, and following its " + "instruction to delete the ZP3111 to the controller. Press the Program Switch " + "of ZP3111 once to be excluded." + ) + assert result["reset"] == ( + "Remove cover to triggered tamper switch, LED flash once & send out Alarm " + "Report. Press Program Switch 10 times within 10 seconds, ZP3111 will send " + "the \u201cDevice Reset Locally Notification\u201d command and reset to the " + "factory default. (Remark: This is to be used only in the case of primary " + "controller being inoperable or otherwise unavailable.)" + ) + assert result["manual"] == ( + "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf" + ) + assert not result["wakeup"] + assert ( + result["device_database_url"] + == "https://devices.zwave-js.io/?jumpTo=0x0086:0x0002:0x0082:0.0" + ) + + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/node_metadata", + ENTRY_ID: entry.entry_id, + NODE_ID: 99999, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/node_metadata", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_ping_node( + hass, wallmote_central_scene, integration, client, hass_ws_client +): + """Test the ping_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + node = wallmote_central_scene + + client.async_send_command.return_value = {"responded": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/ping_node", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/ping_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 99999, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/ping_node", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + async def test_add_node( - hass, integration, client, hass_ws_client, nortek_thermostat_added_event + hass, nortek_thermostat_added_event, integration, client, hass_ws_client ): """Test the add_node websocket command.""" entry = integration @@ -324,10 +455,10 @@ async def test_remove_node( async def test_replace_failed_node( hass, + nortek_thermostat, integration, client, hass_ws_client, - nortek_thermostat, nortek_thermostat_added_event, nortek_thermostat_removed_event, ): @@ -475,10 +606,10 @@ async def test_replace_failed_node( async def test_remove_failed_node( hass, + nortek_thermostat, integration, client, hass_ws_client, - nortek_thermostat, nortek_thermostat_removed_event, ): """Test the remove_failed_node websocket command.""" @@ -538,7 +669,7 @@ async def test_remove_failed_node( async def test_refresh_node_info( - hass, client, integration, hass_ws_client, multisensor_6 + hass, client, multisensor_6, integration, hass_ws_client ): """Test that the refresh_node_info WS API call works.""" entry = integration @@ -637,7 +768,7 @@ async def test_refresh_node_info( async def test_refresh_node_values( - hass, client, integration, hass_ws_client, multisensor_6 + hass, client, multisensor_6, integration, hass_ws_client ): """Test that the refresh_node_values WS API call works.""" entry = integration @@ -690,7 +821,7 @@ async def test_refresh_node_values( async def test_refresh_node_cc_values( - hass, client, integration, hass_ws_client, multisensor_6 + hass, client, multisensor_6, integration, hass_ws_client ): """Test that the refresh_node_cc_values WS API call works.""" entry = integration @@ -920,7 +1051,7 @@ async def test_set_config_parameter( assert msg["error"]["code"] == ERR_NOT_LOADED -async def test_get_config_parameters(hass, integration, multisensor_6, hass_ws_client): +async def test_get_config_parameters(hass, multisensor_6, integration, hass_ws_client): """Test the get config parameters websocket command.""" entry = integration ws_client = await hass_ws_client(hass) diff --git a/tests/fixtures/zwave_js/wallmote_central_scene_state.json b/tests/fixtures/zwave_js/wallmote_central_scene_state.json new file mode 100644 index 00000000000..22eb05c9ce5 --- /dev/null +++ b/tests/fixtures/zwave_js/wallmote_central_scene_state.json @@ -0,0 +1,698 @@ +{ + "nodeId": 35, + "index": 0, + "installerIcon": 7172, + "userIcon": 7172, + "status": 1, + "ready": true, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 24, + "label": "Wall Controller" + }, + "specific": { + "key": 1, + "label": "Basic Wall Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "isListening": false, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 134, + "productId": 130, + "productType": 258, + "firmwareVersion": "2.3", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 4, + "name": "mbr_wallmote_quad", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0002:0x0082:0.0", + "deviceConfig": { + "filename": "/usr/src/app/node_modules/@zwave-js/config/config/devices/0x0086/zw130.json", + "manufacturerId": 134, + "manufacturer": "AEON Labs", + "label": "ZW130", + "description": "WallMote Quad", + "devices": [ + { + "productType": 2, + "productId": 130 + }, + { + "productType": 258, + "productId": 130 + }, + { + "productType": 514, + "productId": 130 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "To add the ZP3111 to the Z-Wave network (inclusion), place the Z-Wave primary controller into inclusion mode. Press the Program Switch of ZP3111 for sending the NIF. After sending NIF, Z-Wave will send the auto inclusion, otherwise, ZP3111 will go to sleep after 20 seconds.", + "exclusion": "To remove the ZP3111 from the Z-Wave network (exclusion), place the Z-Wave primary controller into \u201cexclusion\u201d mode, and following its instruction to delete the ZP3111 to the controller. Press the Program Switch of ZP3111 once to be excluded.", + "reset": "Remove cover to triggered tamper switch, LED flash once & send out Alarm Report. Press Program Switch 10 times within 10 seconds, ZP3111 will send the \u201cDevice Reset Locally Notification\u201d command and reset to the factory default. (Remark: This is to be used only in the case of primary controller being inoperable or otherwise unavailable.)", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf" + }, + "isEmbedded": true + }, + "label": "ZW130", + "neighbors": [1, 14, 15, 16, 22, 30, 31, 5, 6, 7, 8], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": true, + "individualEndpointCount": 4, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "interviewStage": "NodeInfo", + "commandClasses": [ + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 2, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 132, + "name": "Wake Up", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ], + "endpoints": [ + { + "nodeId": 35, + "index": 0, + "installerIcon": 7172, + "userIcon": 7172 + }, + { + "nodeId": 35, + "index": 1, + "installerIcon": 7169, + "userIcon": 7169 + }, + { + "nodeId": 35, + "index": 2, + "installerIcon": 7169, + "userIcon": 7169 + }, + { + "nodeId": 35, + "index": 3, + "installerIcon": 7169, + "userIcon": 7169 + }, + { + "nodeId": 35, + "index": 4, + "installerIcon": 7169, + "userIcon": 7169 + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "slowRefresh", + "propertyName": "slowRefresh", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Send held down notifications at a slow rate", + "description": "When this is true, KeyHeldDown notifications are sent every 55s. When this is false, the notifications are sent every 200ms." + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "004", + "propertyName": "scene", + "propertyKeyName": "004", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Scene 004", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown" + } + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "001", + "propertyName": "scene", + "propertyKeyName": "001", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Scene 001", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown" + } + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "002", + "propertyName": "scene", + "propertyKeyName": "002", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Scene 002", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown" + } + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "003", + "propertyName": "scene", + "propertyKeyName": "003", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Scene 003", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown" + } + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Touch sound", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Touch sound", + "description": "Enable/disable the touch sound.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Touch vibration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Touch vibration", + "description": "Enable/disable the touch vibration.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Button slide", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Button slide", + "description": "Enable/disable the function of button slide.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Notification report", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 3, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Central scene", + "3": "Central scene and config" + }, + "label": "Notification report", + "description": "Which notification to be sent to the associated devices.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 39, + "propertyName": "Low battery value", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 50, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Low battery value", + "description": "Set the low battery value", + "isFromConfig": true + }, + "value": 20 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 255, + "propertyName": "Reset the WallMote Quad", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1431655765, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Reset to factory default", + "1431655765": "Reset and remove" + }, + "label": "Reset the WallMote Quad", + "description": "Reset the WallMote Quad to factory default.", + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Battery load status", + "propertyName": "Power Management", + "propertyKeyName": "Battery load status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Battery load status", + "states": { + "0": "idle", + "12": "Battery is charging" + }, + "ccSpecific": { + "notificationType": 8 + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Battery level status", + "propertyName": "Power Management", + "propertyKeyName": "Battery level status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Battery level status", + "states": { + "0": "idle", + "13": "Battery is fully charged", + "14": "Charge battery soon", + "15": "Charge battery now" + }, + "ccSpecific": { + "notificationType": 8 + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 134 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 258 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 130 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "min": 0, + "max": 864000, + "label": "Wake Up interval", + "steps": 240, + "default": 0 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.62" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["2.3"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} From 727ca79b93ba656bad04afc1191d02ddbdc5cc7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Rutkai?= Date: Tue, 25 May 2021 17:38:09 +0200 Subject: [PATCH 730/852] Updating IBM Watson SDK, replacing TTS API endpoint (#50909) --- homeassistant/components/watson_tts/manifest.json | 2 +- homeassistant/components/watson_tts/tts.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json index e833ac02638..679ea1ef5c3 100644 --- a/homeassistant/components/watson_tts/manifest.json +++ b/homeassistant/components/watson_tts/manifest.json @@ -2,7 +2,7 @@ "domain": "watson_tts", "name": "IBM Watson TTS", "documentation": "https://www.home-assistant.io/integrations/watson_tts", - "requirements": ["ibm-watson==4.0.1"], + "requirements": ["ibm-watson==5.1.0"], "codeowners": ["@rutkai"], "iot_class": "cloud_push" } diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index eeab72b73d0..cdcbbc6ed2a 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) CONF_URL = "watson_url" CONF_APIKEY = "watson_apikey" -DEFAULT_URL = "https://stream.watsonplatform.net/text-to-speech/api" +DEFAULT_URL = "https://api.us-south.text-to-speech.watson.cloud.ibm.com" CONF_VOICE = "voice" CONF_OUTPUT_FORMAT = "output_format" diff --git a/requirements_all.txt b/requirements_all.txt index 16576c9d07f..715b80e5266 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -810,7 +810,7 @@ iammeter==0.1.7 iaqualink==0.3.4 # homeassistant.components.watson_tts -ibm-watson==4.0.1 +ibm-watson==5.1.0 # homeassistant.components.watson_iot ibmiotf==0.3.4 From c0234df136525f935150b25e10ad918cb749eecb Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 25 May 2021 11:48:21 -0400 Subject: [PATCH 731/852] Remove device_registry fixture from zwave_js tests (#51072) --- tests/components/zwave_js/conftest.py | 8 ------ tests/components/zwave_js/test_init.py | 40 ++++++++++---------------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 3f5b1fbe88d..2b6abacbf91 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -10,8 +10,6 @@ from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node from zwave_js_server.version import VersionInfo -from homeassistant.helpers.device_registry import async_get as async_get_device_registry - from tests.common import MockConfigEntry, load_fixture # Add-on fixtures @@ -137,12 +135,6 @@ def create_snapshot_fixture(): yield create_shapshot -@pytest.fixture(name="device_registry") -async def device_registry_fixture(hass): - """Return the device registry.""" - return async_get_device_registry(hass) - - @pytest.fixture(name="controller_state", scope="session") def controller_state_fixture(): """Load the controller state fixture data.""" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 65421eba604..67d5a416a91 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -128,10 +128,9 @@ async def test_listen_failure(hass, client, error): assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_on_node_added_ready( - hass, multisensor_6_state, client, integration, device_registry -): +async def test_on_node_added_ready(hass, multisensor_6_state, client, integration): """Test we handle a ready node added event.""" + dev_reg = dr.async_get(hass) node = Node(client, multisensor_6_state) event = {"node": node} air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" @@ -139,7 +138,7 @@ async def test_on_node_added_ready( state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert not state # entity and device not yet added - assert not device_registry.async_get_device( + assert not dev_reg.async_get_device( identifiers={(DOMAIN, air_temperature_device_id)} ) @@ -150,9 +149,7 @@ async def test_on_node_added_ready( assert state # entity and device added assert state.state != STATE_UNAVAILABLE - assert device_registry.async_get_device( - identifiers={(DOMAIN, air_temperature_device_id)} - ) + assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) async def test_unique_id_migration_dupes( @@ -473,10 +470,9 @@ async def test_old_entity_migration_notification_binary_sensor( ) -async def test_on_node_added_not_ready( - hass, multisensor_6_state, client, integration, device_registry -): +async def test_on_node_added_not_ready(hass, multisensor_6_state, client, integration): """Test we handle a non ready node added event.""" + dev_reg = dr.async_get(hass) node_data = deepcopy(multisensor_6_state) # Copy to allow modification in tests. node = Node(client, node_data) node.data["ready"] = False @@ -486,7 +482,7 @@ async def test_on_node_added_not_ready( state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert not state # entity and device not yet added - assert not device_registry.async_get_device( + assert not dev_reg.async_get_device( identifiers={(DOMAIN, air_temperature_device_id)} ) @@ -496,9 +492,7 @@ async def test_on_node_added_not_ready( state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert not state # entity not yet added but device added in registry - assert device_registry.async_get_device( - identifiers={(DOMAIN, air_temperature_device_id)} - ) + assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) node.data["ready"] = True node.emit("ready", event) @@ -510,10 +504,9 @@ async def test_on_node_added_not_ready( assert state.state != STATE_UNAVAILABLE -async def test_existing_node_ready( - hass, client, multisensor_6, integration, device_registry -): +async def test_existing_node_ready(hass, client, multisensor_6, integration): """Test we handle a ready node that exists during integration setup.""" + dev_reg = dr.async_get(hass) node = multisensor_6 air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" @@ -521,9 +514,7 @@ async def test_existing_node_ready( assert state # entity and device added assert state.state != STATE_UNAVAILABLE - assert device_registry.async_get_device( - identifiers={(DOMAIN, air_temperature_device_id)} - ) + assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) async def test_null_name(hass, client, null_name_check, integration): @@ -532,8 +523,9 @@ async def test_null_name(hass, client, null_name_check, integration): assert hass.states.get(f"switch.node_{node.node_id}") -async def test_existing_node_not_ready(hass, client, multisensor_6, device_registry): +async def test_existing_node_not_ready(hass, client, multisensor_6): """Test we handle a non ready node that exists during integration setup.""" + dev_reg = dr.async_get(hass) node = multisensor_6 node.data = deepcopy(node.data) # Copy to allow modification in tests. node.data["ready"] = False @@ -548,7 +540,7 @@ async def test_existing_node_not_ready(hass, client, multisensor_6, device_regis state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert not state # entity not yet added - assert device_registry.async_get_device( # device should be added + assert dev_reg.async_get_device( # device should be added identifiers={(DOMAIN, air_temperature_device_id)} ) @@ -560,9 +552,7 @@ async def test_existing_node_not_ready(hass, client, multisensor_6, device_regis assert state # entity and device added assert state.state != STATE_UNAVAILABLE - assert device_registry.async_get_device( - identifiers={(DOMAIN, air_temperature_device_id)} - ) + assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) async def test_start_addon( From d2804433d3991f9d6d4d029c3808989e78d725c5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 May 2021 17:49:24 +0200 Subject: [PATCH 732/852] Select onoff and brightness color modes last for light groups (#51054) --- homeassistant/components/group/light.py | 8 ++- tests/components/group/test_light.py | 75 +++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 4357085ef8a..9567735c9eb 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -27,6 +27,8 @@ from homeassistant.components.light import ( ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_ONOFF, PLATFORM_SCHEMA, SUPPORT_EFFECT, SUPPORT_FLASH, @@ -386,8 +388,12 @@ class LightGroup(GroupEntity, light.LightEntity): self._color_mode = None all_color_modes = list(_find_state_attributes(on_states, ATTR_COLOR_MODE)) if all_color_modes: - # Report the most common color mode. + # Report the most common color mode, select brightness and onoff last color_mode_count = Counter(itertools.chain(all_color_modes)) + if COLOR_MODE_ONOFF in color_mode_count: + color_mode_count[COLOR_MODE_ONOFF] = -1 + if COLOR_MODE_BRIGHTNESS in color_mode_count: + color_mode_count[COLOR_MODE_BRIGHTNESS] = 0 self._color_mode = color_mode_count.most_common(1)[0][0] self._supported_color_modes = None diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 14489450610..06ad1b1101b 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -26,6 +26,7 @@ from homeassistant.components.light import ( COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, + COLOR_MODE_ONOFF, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, DOMAIN as LIGHT_DOMAIN, @@ -856,6 +857,80 @@ async def test_color_mode(hass, enable_custom_integrations): assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_HS +async def test_color_mode2(hass, enable_custom_integrations): + """Test onoff color_mode and brightness are given lowest priority.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test4", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test5", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test6", STATE_ON)) + + entity = platform.ENTITIES[0] + entity.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity.color_mode = COLOR_MODE_COLOR_TEMP + + entity = platform.ENTITIES[1] + entity.supported_color_modes = {COLOR_MODE_BRIGHTNESS} + entity.color_mode = COLOR_MODE_BRIGHTNESS + + entity = platform.ENTITIES[2] + entity.supported_color_modes = {COLOR_MODE_BRIGHTNESS} + entity.color_mode = COLOR_MODE_BRIGHTNESS + + entity = platform.ENTITIES[3] + entity.supported_color_modes = {COLOR_MODE_ONOFF} + entity.color_mode = COLOR_MODE_ONOFF + + entity = platform.ENTITIES[4] + entity.supported_color_modes = {COLOR_MODE_ONOFF} + entity.color_mode = COLOR_MODE_ONOFF + + entity = platform.ENTITIES[5] + entity.supported_color_modes = {COLOR_MODE_ONOFF} + entity.color_mode = COLOR_MODE_ONOFF + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": [ + "light.test1", + "light.test2", + "light.test3", + "light.test4", + "light.test5", + "light.test6", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": ["light.test1"]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_BRIGHTNESS + + async def test_supported_features(hass): """Test supported features reporting.""" await async_setup_component( From 7b5e63132cb9bd35300aaa7378d07e9141557d5b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 May 2021 17:50:50 +0200 Subject: [PATCH 733/852] Prevent parallel reload of automations (#50008) --- .../components/automation/__init__.py | 13 +++++- homeassistant/helpers/service.py | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index a338f6cf161..eb77880687d 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -54,7 +54,10 @@ from homeassistant.helpers.script import ( Script, ) from homeassistant.helpers.script_variables import ScriptVariables -from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.service import ( + ReloadServiceHelper, + async_register_admin_service, +) from homeassistant.helpers.trace import ( TraceElement, script_execution_set, @@ -253,8 +256,14 @@ async def async_setup(hass, config): await _async_process_config(hass, conf, component) hass.bus.async_fire(EVENT_AUTOMATION_RELOADED, context=service_call.context) + reload_helper = ReloadServiceHelper(reload_service_handler) + async_register_admin_service( - hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) + hass, + DOMAIN, + SERVICE_RELOAD, + reload_helper.execute_service, + schema=vol.Schema({}), ) return True diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 31befb36531..cbbbbc24643 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -783,3 +783,43 @@ def verify_domain_control( return check_permissions return decorator + + +class ReloadServiceHelper: + """Helper for reload services to minimize unnecessary reloads.""" + + def __init__(self, service_func: Callable[[ServiceCall], Awaitable]): + """Initialize ReloadServiceHelper.""" + self._service_func = service_func + self._service_running = False + self._service_condition = asyncio.Condition() + + async def execute_service(self, service_call: ServiceCall) -> None: + """Execute the service. + + If a previous reload task if currently in progress, wait for it to finish first. + Once the previous reload task has finished, one of the waiting tasks will be + assigned to execute the reload, the others will wait for the reload to finish. + """ + + do_reload = False + async with self._service_condition: + if self._service_running: + # A previous reload task is already in progress, wait for it to finish + await self._service_condition.wait() + + async with self._service_condition: + if not self._service_running: + # This task will do the reload + self._service_running = True + do_reload = True + else: + # Another task will perform the reload, wait for it to finish + await self._service_condition.wait() + + if do_reload: + # Reload, then notify other tasks + await self._service_func(service_call) + async with self._service_condition: + self._service_running = False + self._service_condition.notify_all() From fb61ef500ceaf3935c31463d251bb5f0dc5320de Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 25 May 2021 18:12:42 +0200 Subject: [PATCH 734/852] Add TV channel trait to google assistant (#49676) --- .../components/google_assistant/const.py | 1 + .../components/google_assistant/trait.py | 61 ++++++++++++++++++- .../components/google_assistant/test_trait.py | 53 ++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index d6badf2e7ba..3294ff54c2e 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -99,6 +99,7 @@ ERR_PROTOCOL_ERROR = "protocolError" ERR_UNKNOWN_ERROR = "unknownError" ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported" ERR_UNSUPPORTED_INPUT = "unsupportedInput" +ERR_NO_AVAILABLE_CHANNEL = "noAvailableChannel" ERR_ALREADY_DISARMED = "alreadyDisarmed" ERR_ALREADY_ARMED = "alreadyArmed" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 64f803dab25..8286e527159 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -23,6 +23,7 @@ from homeassistant.components import ( ) from homeassistant.components.climate import const as climate from homeassistant.components.humidifier import const as humidifier +from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_CODE, @@ -71,6 +72,7 @@ from .const import ( ERR_ALREADY_DISARMED, ERR_ALREADY_STOPPED, ERR_CHALLENGE_NOT_SETUP, + ERR_NO_AVAILABLE_CHANNEL, ERR_NOT_SUPPORTED, ERR_UNSUPPORTED_INPUT, ERR_VALUE_OUT_OF_RANGE, @@ -99,6 +101,7 @@ TRAIT_ARMDISARM = f"{PREFIX_TRAITS}ArmDisarm" TRAIT_HUMIDITY_SETTING = f"{PREFIX_TRAITS}HumiditySetting" TRAIT_TRANSPORT_CONTROL = f"{PREFIX_TRAITS}TransportControl" TRAIT_MEDIA_STATE = f"{PREFIX_TRAITS}MediaState" +TRAIT_CHANNEL = f"{PREFIX_TRAITS}Channel" PREFIX_COMMANDS = "action.devices.commands." COMMAND_ONOFF = f"{PREFIX_COMMANDS}OnOff" @@ -137,7 +140,7 @@ COMMAND_MEDIA_SEEK_TO_POSITION = f"{PREFIX_COMMANDS}mediaSeekToPosition" COMMAND_MEDIA_SHUFFLE = f"{PREFIX_COMMANDS}mediaShuffle" COMMAND_MEDIA_STOP = f"{PREFIX_COMMANDS}mediaStop" COMMAND_SET_HUMIDITY = f"{PREFIX_COMMANDS}SetHumidity" - +COMMAND_SELECT_CHANNEL = f"{PREFIX_COMMANDS}selectChannel" TRAITS = [] @@ -2070,3 +2073,59 @@ class MediaStateTrait(_Trait): "activityState": self.activity_lookup.get(self.state.state, "INACTIVE"), "playbackState": self.playback_lookup.get(self.state.state, "STOPPED"), } + + +@register_trait +class ChannelTrait(_Trait): + """Trait to get media playback state. + + https://developers.google.com/actions/smarthome/traits/channel + """ + + name = TRAIT_CHANNEL + commands = [COMMAND_SELECT_CHANNEL] + + @staticmethod + def supported(domain, features, device_class, _): + """Test if state is supported.""" + if ( + domain == media_player.DOMAIN + and (features & media_player.SUPPORT_PLAY_MEDIA) + and device_class == media_player.DEVICE_CLASS_TV + ): + return True + + return False + + def sync_attributes(self): + """Return attributes for a sync request.""" + return {"availableChannels": [], "commandOnlyChannels": True} + + def query_attributes(self): + """Return channel query attributes.""" + return {} + + async def execute(self, command, data, params, challenge): + """Execute an setChannel command.""" + if command == COMMAND_SELECT_CHANNEL: + channel_number = params.get("channelNumber") + else: + raise SmartHomeError(ERR_NOT_SUPPORTED, "Unsupported command") + + if not channel_number: + raise SmartHomeError( + ERR_NO_AVAILABLE_CHANNEL, + "Channel is not available", + ) + + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_MEDIA_CONTENT_ID: channel_number, + media_player.ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + }, + blocking=True, + context=data.context, + ) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 3d506be644d..c3678e7f99a 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -26,6 +26,10 @@ from homeassistant.components.climate import const as climate from homeassistant.components.google_assistant import const, error, helpers, trait from homeassistant.components.google_assistant.error import SmartHomeError from homeassistant.components.humidifier import const as humidifier +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, + SERVICE_PLAY_MEDIA, +) from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -2653,3 +2657,52 @@ async def test_media_state(hass, state): "activityState": trt.activity_lookup.get(state), "playbackState": trt.playback_lookup.get(state), } + + +async def test_channel(hass): + """Test Channel trait support.""" + assert helpers.get_google_type(media_player.DOMAIN, None) is not None + assert trait.ChannelTrait.supported( + media_player.DOMAIN, + media_player.SUPPORT_PLAY_MEDIA, + media_player.DEVICE_CLASS_TV, + None, + ) + assert ( + trait.ChannelTrait.supported( + media_player.DOMAIN, media_player.SUPPORT_PLAY_MEDIA, None, None + ) + is False + ) + assert trait.ChannelTrait.supported(media_player.DOMAIN, 0, None, None) is False + + trt = trait.ChannelTrait(hass, State("media_player.demo", STATE_ON), BASIC_CONFIG) + + assert trt.sync_attributes() == { + "availableChannels": [], + "commandOnlyChannels": True, + } + assert trt.query_attributes() == {} + + media_player_calls = async_mock_service( + hass, media_player.DOMAIN, SERVICE_PLAY_MEDIA + ) + await trt.execute( + trait.COMMAND_SELECT_CHANNEL, BASIC_DATA, {"channelNumber": "1"}, {} + ) + assert len(media_player_calls) == 1 + assert media_player_calls[0].data == { + ATTR_ENTITY_ID: "media_player.demo", + media_player.ATTR_MEDIA_CONTENT_ID: "1", + media_player.ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + } + + with pytest.raises(SmartHomeError, match="Channel is not available"): + await trt.execute( + trait.COMMAND_SELECT_CHANNEL, BASIC_DATA, {"channelCode": "Channel 3"}, {} + ) + assert len(media_player_calls) == 1 + + with pytest.raises(SmartHomeError, match="Unsupported command"): + await trt.execute("Unknown command", BASIC_DATA, {"channelNumber": "1"}, {}) + assert len(media_player_calls) == 1 From 1de4971d5498d1d5fd15ce5f8f057eb8a39871ce Mon Sep 17 00:00:00 2001 From: brucemiranda Date: Tue, 25 May 2021 17:25:12 +0100 Subject: [PATCH 735/852] Add ebusd boiler StateNumber and Modulation Percentage sensors (#49732) * Added Boiler StateNumber and ModulationPercentage * Update const.py * Clean whitespace Co-authored-by: Martin Hjelmare --- homeassistant/components/ebusd/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index c15cf8d4eaf..c4ff789202d 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,6 +1,7 @@ """Constants for ebus component.""" from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, + PERCENTAGE, PRESSURE_BAR, TEMP_CELSIUS, TIME_SECONDS, @@ -136,5 +137,7 @@ SENSOR_TYPES = { ], "RoomThermostat": ["DCRoomthermostat", None, "mdi:toggle-switch", 2], "HeatingPartLoad": ["PartloadHcKW", ENERGY_KILO_WATT_HOUR, "mdi:flash", 0], + "StateNumber": ["StateNumber", None, "mdi:fire", 3], + "ModulationPercentage": ["ModulationTempDesired", PERCENTAGE, "mdi:percent", 0], }, } From 9bf6ea60db41560612d781b9c7303c301bc010f4 Mon Sep 17 00:00:00 2001 From: Aaron David Schneider Date: Tue, 25 May 2021 18:32:25 +0200 Subject: [PATCH 736/852] Add Sonos alarm to sonos component (#50719) * add sonos_alarm * bug fix for _update_device * fix pylint and black and co * small bug fix in speaker.available_alarms * cleanup and add _LOGGER.debug statements, fix pylint * fix pylint * _alarm_id to alarm_id * fixed rare bug due to raceconditions * Part 2 of raceconditionfix * address review suggestions * readd check for not yet subscribed * - platforms_ready fix - add alarmClock to pytest mock * fixture for ListAlarms * cleanup mock and match UUID for test * add simple tests for sonos_alarm * extend test for attributes * typhint fix * typo * use get_alarms() directly * refactor available_alarms * fix attributes * some cleanup * change logic of fetch_alarms_for_speaker and rename to update_alarms_for_speaker * update_alarms_for_speaker is now a method * Update homeassistant/components/sonos/switch.py Co-authored-by: jjlawren * Update homeassistant/components/sonos/speaker.py Co-authored-by: jjlawren Co-authored-by: jjlawren --- homeassistant/components/sonos/__init__.py | 12 ++ homeassistant/components/sonos/const.py | 5 +- .../components/sonos/media_player.py | 3 +- homeassistant/components/sonos/speaker.py | 53 ++++- homeassistant/components/sonos/switch.py | 202 ++++++++++++++++++ tests/components/sonos/conftest.py | 27 ++- tests/components/sonos/test_switch.py | 47 ++++ 7 files changed, 343 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/sonos/switch.py create mode 100644 tests/components/sonos/test_switch.py diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index c513a73b6e8..a904ae58db6 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -9,6 +9,7 @@ import socket import pysonos from pysonos import events_asyncio +from pysonos.alarms import Alarm from pysonos.core import SoCo from pysonos.exceptions import SoCoException import voluptuous as vol @@ -30,6 +31,7 @@ from .const import ( DISCOVERY_INTERVAL, DOMAIN, PLATFORMS, + SONOS_ALARM_UPDATE, SONOS_GROUP_UPDATE, SONOS_SEEN, ) @@ -70,6 +72,7 @@ class SonosData: # OrderedDict behavior used by SonosFavorites self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict() self.favorites: dict[str, SonosFavorites] = {} + self.alarms: dict[str, Alarm] = {} self.topology_condition = asyncio.Condition() self.discovery_thread = None self.hosts_heartbeat = None @@ -174,6 +177,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def _async_signal_update_groups(event): async_dispatcher_send(hass, SONOS_GROUP_UPDATE) + @callback + def _async_signal_update_alarms(event): + async_dispatcher_send(hass, SONOS_ALARM_UPDATE) + async def setup_platforms_and_discovery(): await asyncio.gather( *[ @@ -189,6 +196,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: EVENT_HOMEASSISTANT_START, _async_signal_update_groups ) ) + entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, _async_signal_update_alarms + ) + ) _LOGGER.debug("Adding discovery job") await hass.async_add_executor_job(_discovery) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index e14024c32d2..c32f981e345 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -20,10 +20,11 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_TRACK, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" -PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN} +PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN} SONOS_ARTIST = "artists" SONOS_ALBUM = "albums" @@ -131,12 +132,14 @@ PLAYABLE_MEDIA_TYPES = [ MEDIA_TYPE_TRACK, ] +SONOS_CREATE_ALARM = "sonos_create_alarm" SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_ENTITY_UPDATE = "sonos_entity_update" SONOS_GROUP_UPDATE = "sonos_group_update" SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated" +SONOS_ALARM_UPDATE = "sonos_alarm_update" SONOS_STATE_UPDATED = "sonos_state_updated" SONOS_SEEN = "sonos_seen" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 06b9c49257a..9ca4b21425b 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -588,8 +588,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Set the alarm clock on the player.""" alarm = None for one_alarm in alarms.get_alarms(self.coordinator.soco): - # pylint: disable=protected-access - if one_alarm._alarm_id == str(alarm_id): + if one_alarm.alarm_id == str(alarm_id): alarm = one_alarm if alarm is None: _LOGGER.warning("Did not find alarm with id %s", alarm_id) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 97fc8dcdbcc..708b29d5c55 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -11,6 +11,7 @@ from typing import Any, Callable import urllib.parse import async_timeout +from pysonos.alarms import get_alarms from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo from pysonos.data_structures import DidlAudioBroadcast from pysonos.events_base import Event as SonosEvent, SubscriptionBase @@ -21,6 +22,7 @@ from pysonos.snapshot import Snapshot from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as ent_reg from homeassistant.helpers.dispatcher import ( @@ -37,6 +39,8 @@ from .const import ( PLATFORMS, SCAN_INTERVAL, SEEN_EXPIRE_TIME, + SONOS_ALARM_UPDATE, + SONOS_CREATE_ALARM, SONOS_CREATE_BATTERY, SONOS_CREATE_MEDIA_PLAYER, SONOS_ENTITY_CREATED, @@ -193,12 +197,17 @@ class SonosSpeaker: else: self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) + if new_alarms := self.update_alarms_for_speaker(): + dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) + else: + self._platforms_ready.add(SWITCH_DOMAIN) + dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) async def async_handle_new_entity(self, entity_type: str) -> None: """Listen to new entities to trigger first subscription.""" self._platforms_ready.add(entity_type) - if self._platforms_ready == PLATFORMS: + if self._platforms_ready == PLATFORMS and not self._subscriptions: self._resubscription_lock = asyncio.Lock() await self.async_subscribe() self._is_ready = True @@ -244,6 +253,7 @@ class SonosSpeaker: self._subscribe( self.soco.deviceProperties, self.async_dispatch_properties ), + self._subscribe(self.soco.alarmClock, self.async_dispatch_alarms), ) return True except SoCoException as ex: @@ -266,6 +276,11 @@ class SonosSpeaker: """Update properties from event.""" self.hass.async_create_task(self.async_update_device_properties(event)) + @callback + def async_dispatch_alarms(self, event: SonosEvent | None = None) -> None: + """Update alarms from event.""" + self.hass.async_create_task(self.async_update_alarms(event)) + @callback def async_dispatch_groups(self, event: SonosEvent | None = None) -> None: """Update groups from event.""" @@ -365,6 +380,42 @@ class SonosSpeaker: self.async_write_entity_states() + def update_alarms_for_speaker(self) -> set[str]: + """Update current alarm instances. + + Updates hass.data[DATA_SONOS].alarms and returns a list of all alarms that are new. + """ + new_alarms = set() + stored_alarms = self.hass.data[DATA_SONOS].alarms + updated_alarms = get_alarms(self.soco) + + for alarm in updated_alarms: + if alarm.zone.uid == self.soco.uid and alarm.alarm_id not in list( + stored_alarms.keys() + ): + new_alarms.add(alarm.alarm_id) + stored_alarms[alarm.alarm_id] = alarm + + for alarm_id, alarm in list(stored_alarms.items()): + if alarm not in updated_alarms: + stored_alarms.pop(alarm_id) + + return new_alarms + + async def async_update_alarms(self, event: SonosEvent | None = None) -> None: + """Update device properties using the provided SonosEvent.""" + if event is None: + return + + if new_alarms := await self.hass.async_add_executor_job( + self.update_alarms_for_speaker + ): + async_dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) + + async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE, self) + + self.async_write_entity_states() + async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" self._last_battery_event = dt_util.utcnow() diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py new file mode 100644 index 00000000000..967bc21da59 --- /dev/null +++ b/homeassistant/components/sonos/switch.py @@ -0,0 +1,202 @@ +"""Entity representing a Sonos Alarm.""" +from __future__ import annotations + +import datetime +import logging + +from pysonos.exceptions import SoCoUPnPException + +from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity +from homeassistant.const import ATTR_TIME +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + DATA_SONOS, + DOMAIN as SONOS_DOMAIN, + SONOS_ALARM_UPDATE, + SONOS_CREATE_ALARM, +) +from .entity import SonosEntity +from .speaker import SonosSpeaker + +_LOGGER = logging.getLogger(__name__) + +ATTR_DURATION = "duration" +ATTR_ID = "alarm_id" +ATTR_PLAY_MODE = "play_mode" +ATTR_RECURRENCE = "recurrence" +ATTR_SCHEDULED_TODAY = "scheduled_today" +ATTR_VOLUME = "volume" +ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Sonos from a config entry.""" + + configured_alarms = set() + + async def _async_create_entity(speaker: SonosSpeaker, new_alarms: set) -> None: + for alarm_id in new_alarms: + if alarm_id not in configured_alarms: + _LOGGER.debug("Creating alarm with id %s", alarm_id) + entity = SonosAlarmEntity(alarm_id, speaker) + async_add_entities([entity]) + configured_alarms.add(alarm_id) + config_entry.async_on_unload( + async_dispatcher_connect( + hass, SONOS_ALARM_UPDATE, entity.async_update + ) + ) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_entity) + ) + + +class SonosAlarmEntity(SonosEntity, SwitchEntity): + """Representation of a Sonos Alarm entity.""" + + def __init__(self, alarm_id: str, speaker: SonosSpeaker) -> None: + """Initialize the switch.""" + super().__init__(speaker) + + self._alarm_id = alarm_id + self.entity_id = ENTITY_ID_FORMAT.format(f"sonos_alarm_{self.alarm_id}") + + @property + def alarm(self): + """Return the ID of the alarm.""" + return self.hass.data[DATA_SONOS].alarms[self.alarm_id] + + @property + def alarm_id(self): + """Return the ID of the alarm.""" + return self._alarm_id + + @property + def unique_id(self) -> str: + """Return the unique ID of the switch.""" + return f"{SONOS_DOMAIN}-{self.alarm_id}" + + @property + def icon(self): + """Return icon of Sonos alarm switch.""" + return "mdi:alarm" + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return "Sonos Alarm {} {} {}".format( + self.speaker.zone_name, + self.alarm.recurrence.title(), + str(self.alarm.start_time)[0:5], + ) + + async def async_check_if_available(self): + """Check if alarm exists and remove alarm entity if not available.""" + if self.alarm_id in self.hass.data[DATA_SONOS].alarms: + return True + + _LOGGER.debug("The alarm is removed from hass because it has been deleted") + + entity_registry = er.async_get(self.hass) + if entity_registry.async_get(self.entity_id): + entity_registry.async_remove(self.entity_id) + + return False + + async def async_update(self, now: datetime.datetime | None = None) -> None: + """Poll the device for the current state.""" + if await self.async_check_if_available(): + await self.hass.async_add_executor_job(self.update_alarm) + + def update_alarm(self): + """Update the state of the alarm.""" + _LOGGER.debug("Updating the state of the alarm") + if self.speaker.soco.uid != self.alarm.zone.uid: + self.speaker = self.hass.data[DATA_SONOS].discovered.get( + self.alarm.zone.uid + ) + if self.speaker is None: + raise RuntimeError( + "No configured Sonos speaker has been found to match the alarm." + ) + + self._update_device() + + self.schedule_update_ha_state() + + def _update_device(self): + """Update the device, since this alarm moved to a different player.""" + device_registry = dr.async_get(self.hass) + entity_registry = er.async_get(self.hass) + entity = entity_registry.async_get(self.entity_id) + + if entity is None: + raise RuntimeError("Alarm has been deleted by accident.") + + entry_id = entity.config_entry_id + + new_device = device_registry.async_get_or_create( + config_entry_id=entry_id, + identifiers={(SONOS_DOMAIN, self.soco.uid)}, + connections={(dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address)}, + ) + if not entity_registry.async_get(self.entity_id).device_id == new_device.id: + _LOGGER.debug("The alarm is switching the sonos player") + # pylint: disable=protected-access + entity_registry._async_update_entity( + self.entity_id, device_id=new_device.id + ) + + @property + def _is_today(self): + recurrence = self.alarm.recurrence + timestr = int(datetime.datetime.today().strftime("%w")) + return ( + bool(recurrence[:2] == "ON" and str(timestr) in recurrence) + or bool(recurrence == "DAILY") + or bool(recurrence == "WEEKDAYS" and int(timestr) not in [0, 7]) + or bool(recurrence == "ONCE") + or bool(recurrence == "WEEKDAYS" and int(timestr) not in [0, 7]) + or bool(recurrence == "WEEKENDS" and int(timestr) not in range(1, 7)) + ) + + @property + def is_on(self): + """Return state of Sonos alarm switch.""" + return self.alarm.enabled + + @property + def extra_state_attributes(self): + """Return attributes of Sonos alarm switch.""" + return { + ATTR_ID: str(self.alarm_id), + ATTR_TIME: str(self.alarm.start_time), + ATTR_DURATION: str(self.alarm.duration), + ATTR_RECURRENCE: str(self.alarm.recurrence), + ATTR_VOLUME: self.alarm.volume / 100, + ATTR_PLAY_MODE: str(self.alarm.play_mode), + ATTR_SCHEDULED_TODAY: self._is_today, + ATTR_INCLUDE_LINKED_ZONES: self.alarm.include_linked_zones, + } + + async def async_turn_on(self, **kwargs) -> None: + """Turn alarm switch on.""" + await self.async_handle_switch_on_off(turn_on=True) + + async def async_turn_off(self, **kwargs) -> None: + """Turn alarm switch off.""" + await self.async_handle_switch_on_off(turn_on=False) + + async def async_handle_switch_on_off(self, turn_on: bool) -> None: + """Handle turn on/off of alarm switch.""" + try: + _LOGGER.debug("Switching the state of the alarm") + self.alarm.enabled = turn_on + await self.hass.async_add_executor_job(self.alarm.save) + except SoCoUPnPException as exc: + _LOGGER.warning( + "Home Assistant couldn't switch the alarm %s", exc, exc_info=True + ) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 79e44720591..e7e4c42d64c 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -17,7 +17,9 @@ def config_entry_fixture(): @pytest.fixture(name="soco") -def soco_fixture(music_library, speaker_info, battery_info, dummy_soco_service): +def soco_fixture( + music_library, speaker_info, battery_info, dummy_soco_service, alarmClock +): """Create a mock pysonos SoCo fixture.""" with patch("pysonos.SoCo", autospec=True) as mock, patch( "socket.gethostbyname", return_value="192.168.42.2" @@ -32,12 +34,13 @@ def soco_fixture(music_library, speaker_info, battery_info, dummy_soco_service): mock_soco.zoneGroupTopology = dummy_soco_service mock_soco.contentDirectory = dummy_soco_service mock_soco.deviceProperties = dummy_soco_service + mock_soco.alarmClock = alarmClock mock_soco.mute = False mock_soco.night_mode = True mock_soco.dialog_mode = True mock_soco.volume = 19 mock_soco.get_battery_info.return_value = battery_info - + mock_soco.all_zones = [mock_soco] yield mock_soco @@ -75,6 +78,26 @@ def music_library_fixture(): return music_library +@pytest.fixture(name="alarmClock") +def alarmClock_fixture(): + """Create alarmClock fixture.""" + alarmClock = Mock() + alarmClock.subscribe = AsyncMock() + alarmClock.ListAlarms.return_value = { + "CurrentAlarmList": "" + '' + '' + " " + } + return alarmClock + + @pytest.fixture(name="speaker_info") def speaker_info_fixture(): """Create speaker_info fixture.""" diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py new file mode 100644 index 00000000000..c33c472ee27 --- /dev/null +++ b/tests/components/sonos/test_switch.py @@ -0,0 +1,47 @@ +"""Tests for the Sonos Alarm switch platform.""" +from homeassistant.components.sonos import DOMAIN +from homeassistant.components.sonos.switch import ( + ATTR_DURATION, + ATTR_ID, + ATTR_INCLUDE_LINKED_ZONES, + ATTR_PLAY_MODE, + ATTR_RECURRENCE, + ATTR_VOLUME, +) +from homeassistant.const import ATTR_TIME, STATE_ON +from homeassistant.setup import async_setup_component + + +async def setup_platform(hass, config_entry, config): + """Set up the media player platform for testing.""" + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + +async def test_entity_registry(hass, config_entry, config, soco): + """Test sonos device with alarm registered in the device registry.""" + await setup_platform(hass, config_entry, config) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + assert "media_player.zone_a" in entity_registry.entities + assert "switch.sonos_alarm_14" in entity_registry.entities + + +async def test_alarm_attributes(hass, config_entry, config, soco): + """Test for correct sonos alarm state.""" + await setup_platform(hass, config_entry, config) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + alarm = entity_registry.entities["switch.sonos_alarm_14"] + alarm_state = hass.states.get(alarm.entity_id) + assert alarm_state.state == STATE_ON + assert alarm_state.attributes.get(ATTR_TIME) == "07:00:00" + assert alarm_state.attributes.get(ATTR_ID) == "14" + assert alarm_state.attributes.get(ATTR_DURATION) == "02:00:00" + assert alarm_state.attributes.get(ATTR_RECURRENCE) == "DAILY" + assert alarm_state.attributes.get(ATTR_VOLUME) == 0.25 + assert alarm_state.attributes.get(ATTR_PLAY_MODE) == "SHUFFLE_NOREPEAT" + assert not alarm_state.attributes.get(ATTR_INCLUDE_LINKED_ZONES) From aa18ad2abf3c04abe94508421b0373c534432dc9 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Tue, 25 May 2021 09:37:00 -0700 Subject: [PATCH 737/852] Add service to snooze SmartTub reminders (#51012) * Add service to snooze SmartTub reminders * minimum is 10 days * 0->10 in services.yaml as well * Update homeassistant/components/smarttub/services.yaml Co-authored-by: Franck Nijhof * Update homeassistant/components/smarttub/services.yaml Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .../components/smarttub/binary_sensor.py | 22 +++++++++++++++++++ .../components/smarttub/services.yaml | 19 ++++++++++++++++ .../components/smarttub/test_binary_sensor.py | 20 +++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index 31c6f6d0bc0..2ca2e10245c 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -2,12 +2,14 @@ import logging from smarttub import SpaReminder +import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_PROBLEM, BinarySensorEntity, ) +from homeassistant.helpers import entity_platform from .const import ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubEntity, SmartTubSensorBase @@ -17,6 +19,12 @@ _LOGGER = logging.getLogger(__name__) # whether the reminder has been snoozed (bool) ATTR_REMINDER_SNOOZED = "snoozed" +# how many days to snooze the reminder for +ATTR_SNOOZE_DAYS = "days" +SNOOZE_REMINDER_SCHEMA = { + vol.Required(ATTR_SNOOZE_DAYS): vol.All(vol.Coerce(int), vol.Range(min=10, max=120)) +} + async def async_setup_entry(hass, entry, async_add_entities): """Set up binary sensor entities for the binary sensors in the tub.""" @@ -33,6 +41,14 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + "snooze_reminder", + SNOOZE_REMINDER_SCHEMA, + "async_snooze", + ) + class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity): """A binary sensor indicating whether the spa is currently online (connected to the cloud).""" @@ -98,3 +114,9 @@ class SmartTubReminder(SmartTubEntity, BinarySensorEntity): def device_class(self) -> str: """Return the device class for this entity.""" return DEVICE_CLASS_PROBLEM + + async def async_snooze(self, **kwargs): + """Snooze this reminder for the specified number of days.""" + days = kwargs[ATTR_SNOOZE_DAYS] + await self.reminder.snooze(days) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/smarttub/services.yaml b/homeassistant/components/smarttub/services.yaml index 30bd225113e..bb5ee66f1d0 100644 --- a/homeassistant/components/smarttub/services.yaml +++ b/homeassistant/components/smarttub/services.yaml @@ -45,3 +45,22 @@ set_secondary_filtration: - "away" required: true example: "frequent" + +snooze_reminder: + name: Snooze a reminder + description: Delay a reminder, so that it won't trigger again for a period of time. + target: + entity: + integration: smarttub + domain: binary_sensor + fields: + days: + name: Days + description: The number of days to delay the reminder. + required: true + example: 7 + selector: + number: + min: 10 + max: 120 + unit_of_measurement: days diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py index b5a7c516a0e..b39986ef394 100644 --- a/tests/components/smarttub/test_binary_sensor.py +++ b/tests/components/smarttub/test_binary_sensor.py @@ -19,3 +19,23 @@ async def test_reminders(spa, setup_entry, hass): assert state is not None assert state.state == STATE_OFF assert state.attributes["snoozed"] is False + + +async def test_snooze(spa, setup_entry, hass): + """Test snoozing a reminder.""" + + entity_id = f"binary_sensor.{spa.brand}_{spa.model}_myfilter_reminder" + reminder = spa.get_reminders.return_value[0] + days = 30 + + await hass.services.async_call( + "smarttub", + "snooze_reminder", + { + "entity_id": entity_id, + "days": 30, + }, + blocking=True, + ) + + reminder.snooze.assert_called_with(days) From 1e86818f854ad9452ef919dee67101e5196f08d1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 25 May 2021 11:39:31 -0500 Subject: [PATCH 738/852] Add battery support for Sonos S1 speakers (#50864) --- .../components/sonos/binary_sensor.py | 5 +++ homeassistant/components/sonos/sensor.py | 5 +++ homeassistant/components/sonos/speaker.py | 45 ++++++++++++++----- tests/components/sonos/conftest.py | 22 +++++++++ tests/components/sonos/test_sensor.py | 33 +++++++++++++- 5 files changed, 96 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 9fd81a1f006..21e0c077136 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -65,3 +65,8 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): return { ATTR_BATTERY_POWER_SOURCE: self.speaker.power_source, } + + @property + def available(self) -> bool: + """Return whether this device is available.""" + return self.speaker.available and (self.speaker.charging is not None) diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index fcb856e1c06..d9ff19af581 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -58,3 +58,8 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): def state(self) -> int | None: """Return the state of the sensor.""" return self.speaker.battery_info.get("Level") + + @property + def available(self) -> bool: + """Return whether this device is available.""" + return self.speaker.available and self.speaker.power_source diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 708b29d5c55..7ce51176a88 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -187,15 +187,21 @@ class SonosSpeaker: self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen ) - if battery_info := fetch_battery_info_or_none(self.soco): - # Battery events can be infrequent, polling is still necessary - self.battery_info = battery_info - self._battery_poll_timer = self.hass.helpers.event.track_time_interval( - self.async_poll_battery, BATTERY_SCAN_INTERVAL - ) - dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) - else: + if (battery_info := fetch_battery_info_or_none(self.soco)) is None: self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) + else: + self.battery_info = battery_info + # Only create a polling task if successful, may fail on S1 firmware + if battery_info: + # Battery events can be infrequent, polling is still necessary + self._battery_poll_timer = self.hass.helpers.event.track_time_interval( + self.async_poll_battery, BATTERY_SCAN_INTERVAL + ) + else: + _LOGGER.warning( + "S1 firmware detected, battery sensor may update infrequently" + ) + dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) if new_alarms := self.update_alarms_for_speaker(): dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) @@ -421,6 +427,17 @@ class SonosSpeaker: self._last_battery_event = dt_util.utcnow() is_charging = EVENT_CHARGING[battery_dict["BattChg"]] + + if not self._battery_poll_timer: + # Battery info received for an S1 speaker + self.battery_info.update( + { + "Level": int(battery_dict["BattPct"]), + "PowerSource": "EXTERNAL" if is_charging else "BATTERY", + } + ) + return + if is_charging == self.charging: self.battery_info.update({"Level": int(battery_dict["BattPct"])}) else: @@ -435,17 +452,21 @@ class SonosSpeaker: return self.coordinator is None @property - def power_source(self) -> str: + def power_source(self) -> str | None: """Return the name of the current power source. Observed to be either BATTERY or SONOS_CHARGING_RING or USB_POWER. + + May be an empty dict if used with an S1 Move. """ - return self.battery_info["PowerSource"] + return self.battery_info.get("PowerSource") @property - def charging(self) -> bool: + def charging(self) -> bool | None: """Return the charging status of the speaker.""" - return self.power_source != "BATTERY" + if self.power_source: + return self.power_source != "BATTERY" + return None async def async_poll_battery(self, now: datetime.datetime | None = None) -> None: """Poll the device for the current battery state.""" diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index e7e4c42d64c..2feb2b54896 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -10,6 +10,18 @@ from homeassistant.const import CONF_HOSTS from tests.common import MockConfigEntry +class SonosMockEvent: + """Mock a sonos Event used in callbacks.""" + + def __init__(self, soco, variables): + """Initialize the instance.""" + self.sid = f"{soco.uid}_sub0000000001" + self.seq = "0" + self.timestamp = 1621000000.0 + self.service = dummy_soco_service_fixture + self.variables = variables + + @pytest.fixture(name="config_entry") def config_entry_fixture(): """Create a mock Sonos config entry.""" @@ -119,3 +131,13 @@ def battery_info_fixture(): "Temperature": "NORMAL", "PowerSource": "SONOS_CHARGING_RING", } + + +@pytest.fixture(name="battery_event") +def battery_event_fixture(soco): + """Create battery_event fixture.""" + variables = { + "zone_name": "Zone A", + "more_info": "BattChg:NOT_CHARGING,RawBattPct:100,BattPct:100,BattTmp:25", + } + return SonosMockEvent(soco, variables) diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 42bf6eedb9c..bd667b6cf3b 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -1,9 +1,9 @@ """Tests for the Sonos battery sensor platform.""" from pysonos.exceptions import NotSupportedException -from homeassistant.components.sonos import DOMAIN +from homeassistant.components.sonos import DATA_SONOS, DOMAIN from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component @@ -55,3 +55,32 @@ async def test_battery_attributes(hass, config_entry, config, soco): assert ( power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "SONOS_CHARGING_RING" ) + + +async def test_battery_on_S1(hass, config_entry, config, soco, battery_event): + """Test battery state updates on a Sonos S1 device.""" + soco.get_battery_info.return_value = {} + + await setup_platform(hass, config_entry, config) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + battery = entity_registry.entities["sensor.zone_a_battery"] + battery_state = hass.states.get(battery.entity_id) + assert battery_state.state == STATE_UNAVAILABLE + + power = entity_registry.entities["binary_sensor.zone_a_power"] + power_state = hass.states.get(power.entity_id) + assert power_state.state == STATE_UNAVAILABLE + + # Update the speaker with a callback event + speaker = hass.data[DATA_SONOS].discovered[soco.uid] + speaker.async_dispatch_properties(battery_event) + await hass.async_block_till_done() + + battery_state = hass.states.get(battery.entity_id) + assert battery_state.state == "100" + + power_state = hass.states.get(power.entity_id) + assert power_state.state == STATE_OFF + assert power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "BATTERY" From 98535c9e6ca8873f7d78ac57edc69ebdf22db021 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 May 2021 11:47:28 -0500 Subject: [PATCH 739/852] Set homekit controller available state at startup (#51013) --- .../homekit_controller/connection.py | 44 +++++++++++++------ .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index b8713972334..16866bedcfe 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -27,6 +27,7 @@ from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds +MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 _LOGGER = logging.getLogger(__name__) @@ -107,7 +108,7 @@ class HKDevice: # Useful when routing events to triggers self.devices = {} - self.available = True + self.available = False self.signal_state_updated = "_".join((DOMAIN, self.unique_id, "state_updated")) @@ -124,6 +125,7 @@ class HKDevice: # Never allow concurrent polling of the same accessory or bridge self._polling_lock = asyncio.Lock() self._polling_lock_warned = False + self._poll_failures = 0 self.watchable_characteristics = [] @@ -151,9 +153,14 @@ class HKDevice: ] @callback - def async_set_unavailable(self): - """Mark state of all entities on this connection as unavailable.""" - self.available = False + def async_set_available_state(self, available): + """Mark state of all entities on this connection when it becomes available or unavailable.""" + _LOGGER.debug( + "Called async_set_available_state with %s for %s", available, self.unique_id + ) + if self.available == available: + return + self.available = available self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated) async def async_setup(self): @@ -259,6 +266,8 @@ class HKDevice: if self.watchable_characteristics: await self.pairing.subscribe(self.watchable_characteristics) + if not self.pairing.connection.is_connected: + return await self.async_update() @@ -386,25 +395,30 @@ class HKDevice: async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" if not self.pollable_characteristics: - _LOGGER.debug("HomeKit connection not polling any characteristics") + self.async_set_available_state(self.pairing.connection.is_connected) + _LOGGER.debug( + "HomeKit connection not polling any characteristics: %s", self.unique_id + ) return if self._polling_lock.locked(): if not self._polling_lock_warned: _LOGGER.warning( - "HomeKit controller update skipped as previous poll still in flight" + "HomeKit controller update skipped as previous poll still in flight: %s", + self.unique_id, ) self._polling_lock_warned = True return if self._polling_lock_warned: _LOGGER.info( - "HomeKit controller no longer detecting back pressure - not skipping poll" + "HomeKit controller no longer detecting back pressure - not skipping poll: %s", + self.unique_id, ) self._polling_lock_warned = False async with self._polling_lock: - _LOGGER.debug("Starting HomeKit controller update") + _LOGGER.debug("Starting HomeKit controller update: %s", self.unique_id) try: new_values_dict = await self.get_characteristics( @@ -413,20 +427,24 @@ class HKDevice: except AccessoryNotFoundError: # Not only did the connection fail, but also the accessory is not # visible on the network. - self.async_set_unavailable() + self.async_set_available_state(False) return except (AccessoryDisconnectedError, EncryptionError): - # Temporary connection failure. Device is still available but our - # connection was dropped. + # Temporary connection failure. Device may still available but our + # connection was dropped or we are reconnecting + self._poll_failures += 1 + if self._poll_failures >= MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE: + self.async_set_available_state(False) return + self._poll_failures = 0 self.process_new_events(new_values_dict) - _LOGGER.debug("Finished HomeKit controller update") + _LOGGER.debug("Finished HomeKit controller update: %s", self.unique_id) def process_new_events(self, new_values_dict): """Process events from accessory into HA state.""" - self.available = True + self.async_set_available_state(True) # Process any stateless events (via device_triggers) async_fire_triggers(self, new_values_dict) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index ac12c5dc123..46fe126ebf0 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.2.64"], + "requirements": ["aiohomekit==0.2.65"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 715b80e5266..ffb7db21e2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.64 +aiohomekit==0.2.65 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b77a1c301d0..679fcdb5f25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.64 +aiohomekit==0.2.65 # homeassistant.components.emulated_hue # homeassistant.components.http From 9f22509a4b5cbff2384cc86c7e44853ff519e0f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 May 2021 11:47:45 -0500 Subject: [PATCH 740/852] Recover fast when homekit_controller sees a zeroconf announcment for a device that is offline (#51038) --- homeassistant/components/homekit_controller/config_flow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 5fbd3f3b4cb..6ae66d362c9 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -238,6 +238,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # (config_num) for changes. If it changes, we check for new entities if paired and hkid in self.hass.data.get(KNOWN_DEVICES, {}): conn = self.hass.data[KNOWN_DEVICES][hkid] + # When we rediscover the device, let aiohomekit know + # that the device is available and we should not wait + # to retry connecting any longer. reconnect_soon + # will do nothing if the device is already connected + await conn.pairing.connection.reconnect_soon() if conn.config_num != config_num: _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid From 3d41a6667385909af9e94f2bb9b9c268676be0f0 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 25 May 2021 19:28:12 +0200 Subject: [PATCH 741/852] Bump aioshelly to 0.6.4 (#51081) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index fed55b2096a..ab87c4cef38 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.6.3"], + "requirements": ["aioshelly==0.6.4"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index ffb7db21e2d..0076da48f34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -227,7 +227,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.4 # homeassistant.components.shelly -aioshelly==0.6.3 +aioshelly==0.6.4 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 679fcdb5f25..73831e1f90b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -149,7 +149,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.4 # homeassistant.components.shelly -aioshelly==0.6.3 +aioshelly==0.6.4 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From fe75a1bb9de0ce5f1d01d30d00127095ae0a2d69 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 25 May 2021 12:32:59 -0500 Subject: [PATCH 742/852] Set Fahrenheit reporting precision to tenths for Homekit Controller climate entities (#50415) --- .../components/homekit_controller/climate.py | 12 +++++++++++- tests/components/homekit_controller/test_climate.py | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 2c251d41fb3..a2c9c2540ac 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -36,7 +36,7 @@ from homeassistant.components.climate.const import ( SWING_OFF, SWING_VERTICAL, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -323,6 +323,11 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): """Return the unit of measurement.""" return TEMP_CELSIUS + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_TENTHS + class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): """Representation of a Homekit climate device.""" @@ -536,6 +541,11 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): """Return the unit of measurement.""" return TEMP_CELSIUS + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_TENTHS + ENTITY_TYPES = { ServicesTypes.HEATER_COOLER: HomeKitHeaterCoolerEntity, diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 52671703cca..bc9fdaa1013 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -1,4 +1,6 @@ """Basic checks for HomeKitclimate.""" +from unittest.mock import patch + from aiohomekit.model.characteristics import ( ActivationStateValues, CharacteristicsTypes, @@ -19,6 +21,7 @@ from homeassistant.components.climate.const import ( SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, ) +from homeassistant.const import TEMP_FAHRENHEIT from tests.components.homekit_controller.common import setup_test_component @@ -445,6 +448,11 @@ async def test_climate_read_thermostat_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == HVAC_MODE_HEAT_COOL + # Ensure converted Fahrenheit precision is reported in tenths + with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT): + state = await helper.poll_and_get_state() + assert state.attributes["current_temperature"] == 69.8 + async def test_hvac_mode_vs_hvac_action(hass, utcnow): """Check that we haven't conflated hvac_mode and hvac_action.""" From 023c094b0158301b5f96994fa90a4b612a5f49be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 May 2021 12:40:05 -0500 Subject: [PATCH 743/852] Add v3 smartthings hub to discovery (#51051) - I recently switched to a v3 hub, and it has a new OUI --- homeassistant/components/smartthings/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 0c05c5abb90..b67a05d5753 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -24,6 +24,10 @@ { "hostname": "hub*", "macaddress": "D052A8*" + }, + { + "hostname": "hub*", + "macaddress": "286D97*" } ] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 7ea9d1c1992..82b09e5f7ef 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -210,6 +210,11 @@ DHCP = [ "hostname": "hub*", "macaddress": "D052A8*" }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "286D97*" + }, { "domain": "solaredge", "hostname": "target", From abd6f739e8cf108176eaa2acafdfdb6a022c1ee0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 May 2021 19:53:18 +0200 Subject: [PATCH 744/852] Pylint fix (#51083) --- homeassistant/helpers/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index cbbbbc24643..ff037998f34 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -788,7 +788,7 @@ def verify_domain_control( class ReloadServiceHelper: """Helper for reload services to minimize unnecessary reloads.""" - def __init__(self, service_func: Callable[[ServiceCall], Awaitable]): + def __init__(self, service_func: Callable[[ServiceCall], Awaitable]) -> None: """Initialize ReloadServiceHelper.""" self._service_func = service_func self._service_running = False From 58e37435b3396ca2e3a084597bf5335772fb2b30 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 25 May 2021 13:58:01 -0400 Subject: [PATCH 745/852] Make more max lengths constants and add additional MaxLengthExceeded checks (#50337) * Add more MaxLengthExceeded checks * remove some validations to reduce performance impact * check length of generated entity ID * dont check entity ID twice and use single context id length constant * fix test * add missing test --- homeassistant/components/recorder/models.py | 25 +++++++----- homeassistant/const.py | 11 +++-- homeassistant/core.py | 11 +++-- homeassistant/helpers/entity_registry.py | 12 ++++++ tests/helpers/test_entity_registry.py | 45 +++++++++++++++++++++ 5 files changed, 88 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 4fefcaa19e3..ac3f6c9e401 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -20,7 +20,14 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from sqlalchemy.orm.session import Session -from homeassistant.const import MAX_LENGTH_EVENT_TYPE +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_DOMAIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util @@ -63,14 +70,14 @@ class Events(Base): # type: ignore } __tablename__ = TABLE_EVENTS event_id = Column(Integer, Identity(), primary_key=True) - event_type = Column(String(MAX_LENGTH_EVENT_TYPE)) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - origin = Column(String(32)) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) time_fired = Column(DATETIME_TYPE, index=True) created = Column(DATETIME_TYPE, default=dt_util.utcnow) - context_id = Column(String(36), index=True) - context_user_id = Column(String(36), index=True) - context_parent_id = Column(String(36), index=True) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) __table_args__ = ( # Used for fetching events at a specific time @@ -130,9 +137,9 @@ class States(Base): # type: ignore } __tablename__ = TABLE_STATES state_id = Column(Integer, Identity(), primary_key=True) - domain = Column(String(64)) - entity_id = Column(String(255)) - state = Column(String(255)) + domain = Column(String(MAX_LENGTH_STATE_DOMAIN)) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) event_id = Column( Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True diff --git a/homeassistant/const.py b/homeassistant/const.py index b0ebd6781de..00e06f1e8d0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -26,9 +26,14 @@ ENTITY_MATCH_ALL: Final = "all" # If no name is specified DEVICE_DEFAULT_NAME: Final = "Unnamed Device" -# Max characters for an event_type (changing this requires a recorder -# database migration) -MAX_LENGTH_EVENT_TYPE: Final = 64 +# Max characters for data stored in the recorder (changes to these limits would require +# a database migration) +MAX_LENGTH_EVENT_EVENT_TYPE: Final = 64 +MAX_LENGTH_EVENT_ORIGIN: Final = 32 +MAX_LENGTH_EVENT_CONTEXT_ID: Final = 36 +MAX_LENGTH_STATE_DOMAIN: Final = 64 +MAX_LENGTH_STATE_ENTITY_ID: Final = 255 +MAX_LENGTH_STATE_STATE: Final = 255 # Sun events SUN_EVENT_SUNSET: Final = "sunset" diff --git a/homeassistant/core.py b/homeassistant/core.py index 5447277e835..7b5c93b15bb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -47,7 +47,8 @@ from homeassistant.const import ( EVENT_TIMER_OUT_OF_SYNC, LENGTH_METERS, MATCH_ALL, - MAX_LENGTH_EVENT_TYPE, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_STATE_STATE, __version__, ) from homeassistant.exceptions import ( @@ -130,7 +131,7 @@ def valid_entity_id(entity_id: str) -> bool: def valid_state(state: str) -> bool: """Test if a state is valid.""" - return len(state) < 256 + return len(state) <= MAX_LENGTH_STATE_STATE def callback(func: CALLABLE_T) -> CALLABLE_T: @@ -700,8 +701,10 @@ class EventBus: This method must be run in the event loop. """ - if len(event_type) > MAX_LENGTH_EVENT_TYPE: - raise MaxLengthExceeded(event_type, "event_type", MAX_LENGTH_EVENT_TYPE) + if len(event_type) > MAX_LENGTH_EVENT_EVENT_TYPE: + raise MaxLengthExceeded( + event_type, "event_type", MAX_LENGTH_EVENT_EVENT_TYPE + ) listeners = self._listeners.get(event_type, []) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index af0c5f4f20f..dbb3fae0e53 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -24,6 +24,8 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START, + MAX_LENGTH_STATE_DOMAIN, + MAX_LENGTH_STATE_ENTITY_ID, STATE_UNAVAILABLE, ) from homeassistant.core import ( @@ -33,6 +35,7 @@ from homeassistant.core import ( split_entity_id, valid_entity_id, ) +from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass @@ -201,6 +204,10 @@ class EntityRegistry: Conflicts checked against registered and currently existing entities. """ preferred_string = f"{domain}.{slugify(suggested_object_id)}" + + if len(domain) > MAX_LENGTH_STATE_DOMAIN: + raise MaxLengthExceeded(domain, "domain", MAX_LENGTH_STATE_DOMAIN) + test_string = preferred_string if not known_object_ids: known_object_ids = {} @@ -214,6 +221,11 @@ class EntityRegistry: tries += 1 test_string = f"{preferred_string}_{tries}" + if len(test_string) > MAX_LENGTH_STATE_ENTITY_ID: + raise MaxLengthExceeded( + test_string, "generated_entity_id", MAX_LENGTH_STATE_ENTITY_ID + ) + return test_string @callback diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 9133c59d7c8..a124e1e6da1 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -6,6 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE from homeassistant.core import CoreState, callback, valid_entity_id +from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import ( @@ -904,3 +905,47 @@ async def test_disabled_entities_excluded_from_entity_list(hass, registry): registry, device_entry.id, include_disabled_entities=True ) assert entries == [entry1, entry2] + + +async def test_entity_max_length_exceeded(hass, registry): + """Test that an exception is raised when the max character length is exceeded.""" + + long_entity_id_name = ( + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890" + ) + + with pytest.raises(MaxLengthExceeded) as exc_info: + registry.async_generate_entity_id("sensor", long_entity_id_name) + + assert exc_info.value.property_name == "generated_entity_id" + assert exc_info.value.max_length == 255 + assert exc_info.value.value == f"sensor.{long_entity_id_name}" + + # Try again but against the domain + long_domain_name = long_entity_id_name + with pytest.raises(MaxLengthExceeded) as exc_info: + registry.async_generate_entity_id(long_domain_name, "sensor") + + assert exc_info.value.property_name == "domain" + assert exc_info.value.max_length == 64 + assert exc_info.value.value == long_domain_name + + # Try again but force a number to get added to the entity ID + long_entity_id_name = ( + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567" + ) + + with pytest.raises(MaxLengthExceeded) as exc_info: + registry.async_generate_entity_id( + "sensor", long_entity_id_name, [f"sensor.{long_entity_id_name}"] + ) + + assert exc_info.value.property_name == "generated_entity_id" + assert exc_info.value.max_length == 255 + assert exc_info.value.value == f"sensor.{long_entity_id_name}_2" From 9ec0b0a8da2e5bb49257f3acc606c2c69fbf8e57 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 25 May 2021 12:15:37 -0600 Subject: [PATCH 746/852] Fix for invalid value error when using UI editor for Litter-Robot's set_wait_time service (#50269) --- .../components/litterrobot/entity.py | 3 +- .../components/litterrobot/vacuum.py | 2 +- tests/components/litterrobot/test_vacuum.py | 30 +++++++++++-------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 015e781c38a..d75207fb80d 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -69,7 +69,8 @@ class LitterRobotControlEntity(LitterRobotEntity): try: await action(*args, **kwargs) - except InvalidCommandException as ex: + except InvalidCommandException as ex: # pragma: no cover + # this exception should only occur if the underlying API for commands changes _LOGGER.error(ex) return False diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index bd5fb9b92df..2cfe104c753 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -71,7 +71,7 @@ async def async_setup_entry( ) platform.async_register_entity_service( SERVICE_SET_WAIT_TIME, - {vol.Required("minutes"): vol.In(VALID_WAIT_TIMES)}, + {vol.Required("minutes"): vol.All(vol.Coerce(int), vol.In(VALID_WAIT_TIMES))}, "async_set_wait_time", ) diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 67c526e4a30..7db0ca5dde4 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -2,6 +2,7 @@ from datetime import timedelta import pytest +from voluptuous.error import MultipleInvalid from homeassistant.components.litterrobot import DOMAIN from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS @@ -92,6 +93,11 @@ async def test_vacuum_with_error(hass: HomeAssistant, mock_account_with_error): "set_wait_time", {"minutes": 3}, ), + ( + SERVICE_SET_WAIT_TIME, + "set_wait_time", + {"minutes": "15"}, + ), ], ) async def test_commands(hass: HomeAssistant, mock_account, service, command, extra): @@ -117,21 +123,19 @@ async def test_commands(hass: HomeAssistant, mock_account, service, command, ext getattr(mock_account.robots[0], command).assert_called_once() -async def test_invalid_commands( - hass: HomeAssistant, caplog, mock_account_with_side_effects -): - """Test sending invalid commands to the vacuum.""" - await setup_integration(hass, mock_account_with_side_effects, PLATFORM_DOMAIN) +async def test_invalid_wait_time(hass: HomeAssistant, mock_account): + """Test an attempt to send an invalid wait time to the vacuum.""" + await setup_integration(hass, mock_account, PLATFORM_DOMAIN) vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum assert vacuum.state == STATE_DOCKED - await hass.services.async_call( - DOMAIN, - SERVICE_SET_WAIT_TIME, - {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, "minutes": 15}, - blocking=True, - ) - mock_account_with_side_effects.robots[0].set_wait_time.assert_called_once() - assert "Invalid command: oops" in caplog.text + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_WAIT_TIME, + {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, "minutes": 10}, + blocking=True, + ) + assert not mock_account.robots[0].set_wait_time.called From 8b21a652ba53a8e0a70f7fc49dce52268977b26a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 25 May 2021 20:16:54 +0200 Subject: [PATCH 747/852] Create KNX sensor entity directly from config (#49642) * create sensor entities directly from config * move staticmethod to module level * remove factory call --- homeassistant/components/knx/__init__.py | 11 ++------ homeassistant/components/knx/factory.py | 35 ------------------------ homeassistant/components/knx/sensor.py | 31 +++++++++++++++++---- 3 files changed, 29 insertions(+), 48 deletions(-) delete mode 100644 homeassistant/components/knx/factory.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index a52163cfca3..e98e598af1d 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -36,7 +36,6 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, KNX_ADDRESS, SupportedPlatforms from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure -from .factory import create_knx_device from .schema import ( BinarySensorSchema, ClimateSchema, @@ -229,19 +228,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) for platform in SupportedPlatforms: - if platform.value in config[DOMAIN]: - for device_config in config[DOMAIN][platform.value]: - create_knx_device(platform, knx_module.xknx, device_config) - - # We need to wait until all entities are loaded into the device list since they could also be created from other platforms - for platform in SupportedPlatforms: + if platform.value not in config[DOMAIN]: + continue hass.async_create_task( discovery.async_load_platform( hass, platform.value, DOMAIN, { - "platform_config": config[DOMAIN].get(platform.value), + "platform_config": config[DOMAIN][platform.value], }, config, ) diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py deleted file mode 100644 index 99885c8387a..00000000000 --- a/homeassistant/components/knx/factory.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Factory function to initialize KNX devices from config.""" -from __future__ import annotations - -from xknx import XKNX -from xknx.devices import Device as XknxDevice, Sensor as XknxSensor - -from homeassistant.const import CONF_NAME, CONF_TYPE -from homeassistant.helpers.typing import ConfigType - -from .const import SupportedPlatforms -from .schema import SensorSchema - - -def create_knx_device( - platform: SupportedPlatforms, - knx_module: XKNX, - config: ConfigType, -) -> XknxDevice | None: - """Return the requested XKNX device.""" - if platform is SupportedPlatforms.SENSOR: - return _create_sensor(knx_module, config) - - return None - - -def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor: - """Return a KNX sensor to be used within XKNX.""" - return XknxSensor( - knx_module, - name=config[CONF_NAME], - group_address_state=config[SensorSchema.CONF_STATE_ADDRESS], - sync_state=config[SensorSchema.CONF_SYNC_STATE], - always_callback=config[SensorSchema.CONF_ALWAYS_CALLBACK], - value_type=config[CONF_TYPE], - ) diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index fa4de79cb03..21586faf58c 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -3,9 +3,11 @@ from __future__ import annotations from typing import Any +from xknx import XKNX from xknx.devices import Sensor as XknxSensor from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity +from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType @@ -13,6 +15,7 @@ from homeassistant.util import dt from .const import ATTR_LAST_KNX_UPDATE, ATTR_SOURCE, DOMAIN from .knx_entity import KnxEntity +from .schema import SensorSchema async def async_setup_platform( @@ -22,20 +25,38 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up sensor(s) for KNX platform.""" + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + entities = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxSensor): - entities.append(KNXSensor(device)) + for entity_config in platform_config: + entities.append(KNXSensor(xknx, entity_config)) + async_add_entities(entities) +def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: + """Return a KNX sensor to be used within XKNX.""" + return XknxSensor( + xknx, + name=config[CONF_NAME], + group_address_state=config[SensorSchema.CONF_STATE_ADDRESS], + sync_state=config[SensorSchema.CONF_SYNC_STATE], + always_callback=config[SensorSchema.CONF_ALWAYS_CALLBACK], + value_type=config[CONF_TYPE], + ) + + class KNXSensor(KnxEntity, SensorEntity): """Representation of a KNX sensor.""" - def __init__(self, device: XknxSensor) -> None: + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" self._device: XknxSensor - super().__init__(device) + super().__init__(_create_sensor(xknx, config)) self._unique_id = f"{self._device.sensor_value.group_address_state}" @property From d82f6abbe44c01049e8a341709ebf6bd47bc2012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ana=C3=AFs=20Betts?= Date: Tue, 25 May 2021 21:05:56 +0200 Subject: [PATCH 748/852] Consider Continuous Mode on Nuki Opener to be "unlocked" (#49557) Co-authored-by: Franck Nijhof --- homeassistant/components/nuki/lock.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index ca6e72bde8f..48e72b88530 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod import logging +from pynuki import MODE_OPENER_CONTINUOUS import voluptuous as vol from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity @@ -144,8 +145,11 @@ class NukiOpenerEntity(NukiDeviceEntity): @property def is_locked(self): - """Return true if ring-to-open is enabled.""" - return not self._nuki_device.is_rto_activated + """Return true if either ring-to-open or continuous mode is enabled.""" + return not ( + self._nuki_device.is_rto_activated + or self._nuki_device.mode == MODE_OPENER_CONTINUOUS + ) def lock(self, **kwargs): """Disable ring-to-open.""" From 3a6a1a4d6b2da4f3310e6d9d2727207ee44f6fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 25 May 2021 22:53:16 +0200 Subject: [PATCH 749/852] Tibber, state class (#50951) --- homeassistant/components/tibber/sensor.py | 123 ++++++++++++++++++---- 1 file changed, 105 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 58e27b6b653..70dfa54c70a 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -1,6 +1,6 @@ """Support for Tibber sensors.""" import asyncio -from datetime import timedelta +from datetime import datetime, timedelta import logging from random import randrange @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.const import ( @@ -42,52 +43,89 @@ PARALLEL_UPDATES = 0 SIGNAL_UPDATE_ENTITY = "tibber_rt_update_{}" RT_SENSOR_MAP = { - "averagePower": ["average power", DEVICE_CLASS_POWER, POWER_WATT], - "power": ["power", DEVICE_CLASS_POWER, POWER_WATT], - "minPower": ["min power", DEVICE_CLASS_POWER, POWER_WATT], - "maxPower": ["max power", DEVICE_CLASS_POWER, POWER_WATT], + "averagePower": ["average power", DEVICE_CLASS_POWER, POWER_WATT, None], + "power": ["power", DEVICE_CLASS_POWER, POWER_WATT, None], + "minPower": ["min power", DEVICE_CLASS_POWER, POWER_WATT, None], + "maxPower": ["max power", DEVICE_CLASS_POWER, POWER_WATT, None], "accumulatedConsumption": [ "accumulated consumption", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, + STATE_CLASS_MEASUREMENT, ], "accumulatedConsumptionLastHour": [ - "accumulated consumption last hour", + "accumulated consumption current hour", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, + STATE_CLASS_MEASUREMENT, ], "accumulatedProduction": [ "accumulated production", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, + STATE_CLASS_MEASUREMENT, ], "accumulatedProductionLastHour": [ - "accumulated production last hour", + "accumulated production current hour", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, + STATE_CLASS_MEASUREMENT, ], "lastMeterConsumption": [ "last meter consumption", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, + STATE_CLASS_MEASUREMENT, ], "lastMeterProduction": [ "last meter production", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, + STATE_CLASS_MEASUREMENT, + ], + "voltagePhase1": [ + "voltage phase1", + DEVICE_CLASS_VOLTAGE, + VOLT, + STATE_CLASS_MEASUREMENT, + ], + "voltagePhase2": [ + "voltage phase2", + DEVICE_CLASS_VOLTAGE, + VOLT, + STATE_CLASS_MEASUREMENT, + ], + "voltagePhase3": [ + "voltage phase3", + DEVICE_CLASS_VOLTAGE, + VOLT, + STATE_CLASS_MEASUREMENT, + ], + "currentL1": [ + "current L1", + DEVICE_CLASS_CURRENT, + ELECTRICAL_CURRENT_AMPERE, + STATE_CLASS_MEASUREMENT, + ], + "currentL2": [ + "current L2", + DEVICE_CLASS_CURRENT, + ELECTRICAL_CURRENT_AMPERE, + STATE_CLASS_MEASUREMENT, + ], + "currentL3": [ + "current L3", + DEVICE_CLASS_CURRENT, + ELECTRICAL_CURRENT_AMPERE, + STATE_CLASS_MEASUREMENT, ], - "voltagePhase1": ["voltage phase1", DEVICE_CLASS_VOLTAGE, VOLT], - "voltagePhase2": ["voltage phase2", DEVICE_CLASS_VOLTAGE, VOLT], - "voltagePhase3": ["voltage phase3", DEVICE_CLASS_VOLTAGE, VOLT], - "currentL1": ["current L1", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE], - "currentL2": ["current L2", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE], - "currentL3": ["current L3", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE], "signalStrength": [ "signal strength", DEVICE_CLASS_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS, + STATE_CLASS_MEASUREMENT, ], - "accumulatedCost": ["accumulated cost", None, None], + "accumulatedCost": ["accumulated cost", None, None, STATE_CLASS_MEASUREMENT], } @@ -250,7 +288,9 @@ class TibberSensorRT(TibberSensor): _attr_should_poll = False - def __init__(self, tibber_home, sensor_name, device_class, unit, initial_state): + def __init__( + self, tibber_home, sensor_name, device_class, unit, initial_state, state_class + ): """Initialize the sensor.""" super().__init__(tibber_home) self._sensor_name = sensor_name @@ -261,6 +301,29 @@ class TibberSensorRT(TibberSensor): self._attr_state = initial_state self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{self._sensor_name}" self._attr_unit_of_measurement = unit + self._attr_state_class = state_class + if sensor_name in [ + "last meter consumption", + "last meter production", + ]: + self._attr_last_reset = datetime.fromtimestamp(0) + elif self._sensor_name in [ + "accumulated consumption", + "accumulated production", + "accumulated cost", + ]: + self._attr_last_reset = dt_util.as_utc( + dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) + ) + elif self._sensor_name in [ + "accumulated consumption current hour", + "accumulated production current hour", + ]: + self._attr_last_reset = dt_util.as_utc( + dt_util.now().replace(minute=0, second=0, microsecond=0) + ) + else: + self._attr_last_reset = None async def async_added_to_hass(self): """Start listen for real time data.""" @@ -278,8 +341,23 @@ class TibberSensorRT(TibberSensor): return self._tibber_home.rt_subscription_running @callback - def _set_state(self, state): + def _set_state(self, state, timestamp): """Set sensor state.""" + if state < self._attr_state and self._sensor_name in [ + "accumulated consumption", + "accumulated production", + "accumulated cost", + ]: + self._attr_last_reset = dt_util.as_utc( + timestamp.replace(hour=0, minute=0, second=0, microsecond=0) + ) + if state < self._attr_state and self._sensor_name in [ + "accumulated consumption current hour", + "accumulated production current hour", + ]: + self._attr_last_reset = dt_util.as_utc( + timestamp.replace(minute=0, second=0, microsecond=0) + ) self._attr_state = state self.async_write_ha_state() @@ -307,6 +385,7 @@ class TibberRtDataHandler: if live_measurement is None: return + timestamp = datetime.fromisoformat(live_measurement.pop("timestamp")) new_entities = [] for sensor_type, state in live_measurement.items(): if state is None or sensor_type not in RT_SENSOR_MAP: @@ -316,13 +395,21 @@ class TibberRtDataHandler: self.hass, SIGNAL_UPDATE_ENTITY.format(RT_SENSOR_MAP[sensor_type][0]), state, + timestamp, ) else: - sensor_name, device_class, unit = RT_SENSOR_MAP[sensor_type] + sensor_name, device_class, unit, state_class = RT_SENSOR_MAP[ + sensor_type + ] if sensor_type == "accumulatedCost": unit = self._tibber_home.currency entity = TibberSensorRT( - self._tibber_home, sensor_name, device_class, unit, state + self._tibber_home, + sensor_name, + device_class, + unit, + state, + state_class, ) new_entities.append(entity) self._entities.add(sensor_type) From deb913570721dd434197eb7702baae514523e916 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 May 2021 16:06:17 -0500 Subject: [PATCH 750/852] Handle clamped fan maxValue in homekit_controller (#51088) --- .../components/homekit_controller/fan.py | 3 +- .../specific_devices/test_haa_fan.py | 48 ++++ .../fixtures/homekit_controller/haa_fan.json | 257 ++++++++++++++++++ 3 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 tests/components/homekit_controller/specific_devices/test_haa_fan.py create mode 100644 tests/fixtures/homekit_controller/haa_fan.json diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 591050f5fd9..89f24a66a94 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -84,7 +84,8 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): def speed_count(self): """Speed count for the fan.""" return round( - 100 / max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0) + min(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100, 100) + / max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0) ) async def async_set_direction(self, direction): diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py new file mode 100644 index 00000000000..9e04434d830 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -0,0 +1,48 @@ +"""Make sure that a H.A.A. fan can be setup.""" + +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_haa_fan_setup(hass): + """Test that a H.A.A. fan can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "haa_fan.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + # Check that the switch entity is handled correctly + + entry = entity_registry.async_get("switch.haa_c718b3") + assert entry.unique_id == "homekit-C718B3-2-8" + + helper = Helper(hass, "switch.haa_c718b3", pairing, accessories[0], config_entry) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "HAA-C718B3" + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "José A. Jiménez Campos" + assert device.name == "HAA-C718B3" + assert device.sw_version == "5.0.18" + assert device.via_device_id is not None + + # Assert the fan is detected + entry = entity_registry.async_get("fan.haa_c718b3") + assert entry.unique_id == "homekit-C718B3-1-8" + + helper = Helper( + hass, + "fan.haa_c718b3", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "HAA-C718B3" + assert round(state.attributes["percentage_step"], 2) == 33.33 diff --git a/tests/fixtures/homekit_controller/haa_fan.json b/tests/fixtures/homekit_controller/haa_fan.json new file mode 100644 index 00000000000..b06ccdf9644 --- /dev/null +++ b/tests/fixtures/homekit_controller/haa_fan.json @@ -0,0 +1,257 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "aid": 2, + "iid": 2, + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "HAA-C718B3" + }, + { + "aid": 2, + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "Jos\u00e9 A. Jim\u00e9nez Campos" + }, + { + "aid": 1, + "iid": 4, + "type": "00000030-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "C718B3-1" + }, + { + "aid": 2, + "iid": 5, + "type": "00000021-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "RavenSystem HAA" + }, + { + "aid": 2, + "iid": 6, + "type": "00000052-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "5.0.18" + }, + { + "aid": 2, + "iid": 7, + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": [ + "pw" + ], + "format": "bool" + } + ] + }, + { + "iid": 8, + "type": "00000040-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "aid": 1, + "iid": 9, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "format": "bool", + "value": false + }, + { + "aid": 1, + "iid": 10, + "type": "00000029-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "format": "float", + "unit": "percentage", + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "value": 3 + } + ] + }, + { + "iid": 1000, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": true, + "characteristics": [ + { + "aid": 1, + "iid": 1001, + "type": "00000037-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "1.1.0" + } + ] + }, + { + "iid": 1010, + "type": "F0000100-0218-2017-81BF-AF2B7C833922", + "primary": false, + "hidden": true, + "characteristics": [ + { + "aid": 1, + "iid": 1011, + "type": "F0000101-0218-2017-81BF-AF2B7C833922", + "perms": [ + "pr", + "pw", + "hd" + ], + "description": "Update", + "format": "string", + "value": "" + }, + { + "aid": 1, + "iid": 1012, + "type": "F0000102-0218-2017-81BF-AF2B7C833922", + "perms": [ + "pr", + "pw", + "hd" + ], + "description": "Setup", + "format": "string", + "value": "" + } + ] + } + ] + }, + { + "aid": 2, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "aid": 2, + "iid": 2, + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "HAA-C718B3" + }, + { + "aid": 2, + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "Jos\u00e9 A. Jim\u00e9nez Campos" + }, + { + "aid": 2, + "iid": 4, + "type": "00000030-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "C718B3-2" + }, + { + "aid": 2, + "iid": 5, + "type": "00000021-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "RavenSystem HAA" + }, + { + "aid": 2, + "iid": 6, + "type": "00000052-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "5.0.18" + }, + { + "aid": 2, + "iid": 7, + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": [ + "pw" + ], + "format": "bool" + } + ] + }, + { + "iid": 8, + "type": "00000049-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "aid": 2, + "iid": 9, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "format": "bool", + "value": false + } + ] + } + ] + } +] \ No newline at end of file From 997a847b5cb4882dfca9ca250e9fb1f7bdaafdd7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 00:21:18 +0200 Subject: [PATCH 751/852] Add support for Sensor state class to ESPHome (#51090) * Add support for Sensor state class to ESPHome * Bump aioesphome to 2.8.0 --- .../components/esphome/manifest.json | 2 +- homeassistant/components/esphome/sensor.py | 31 +++++++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index e103fa65992..592ca616d04 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==2.7.0"], + "requirements": ["aioesphomeapi==2.8.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter"], "after_dependencies": ["zeroconf", "tag"], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 12319be8c40..7b905aad148 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -3,12 +3,19 @@ from __future__ import annotations import math -from aioesphomeapi import SensorInfo, SensorState, TextSensorInfo, TextSensorState +from aioesphomeapi import ( + SensorInfo, + SensorState, + SensorStateClass, + TextSensorInfo, + TextSensorState, +) import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASS_TIMESTAMP, DEVICE_CLASSES, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -16,7 +23,12 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.util import dt -from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from . import ( + EsphomeEntity, + esphome_map_enum, + esphome_state_property, + platform_async_setup_entry, +) ICON_SCHEMA = vol.Schema(cv.icon) @@ -49,6 +61,14 @@ async def async_setup_entry( # pylint: disable=invalid-overridden-method +@esphome_map_enum +def _state_classes(): + return { + SensorStateClass.NONE: None, + SensorStateClass.MEASUREMENT: STATE_CLASS_MEASUREMENT, + } + + class EsphomeSensor(EsphomeEntity, SensorEntity): """A sensor implementation for esphome.""" @@ -97,6 +117,13 @@ class EsphomeSensor(EsphomeEntity, SensorEntity): return None return self._static_info.device_class + @property + def state_class(self) -> str | None: + """Return the state class of this entity.""" + if not self._static_info.state_class: + return None + return _state_classes.from_esphome(self._static_info.state_class) + class EsphomeTextSensor(EsphomeEntity, SensorEntity): """A text sensor implementation for ESPHome.""" diff --git a/requirements_all.txt b/requirements_all.txt index 0076da48f34..a3de0bb383e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.7.0 +aioesphomeapi==2.8.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73831e1f90b..015e565af0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.7.0 +aioesphomeapi==2.8.0 # homeassistant.components.flo aioflo==0.4.1 From affc8e0f0be765f5adc31113ad535852e01cc75a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 00:21:53 +0200 Subject: [PATCH 752/852] Fix unique ID Verisure alarm control panel (#51087) --- homeassistant/components/verisure/alarm_control_panel.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 3c77541cea8..4def470ac5e 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -36,8 +36,6 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): coordinator: VerisureDataUpdateCoordinator _attr_name = "Verisure Alarm" - _attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY - _changed_by: str | None = None @property @@ -55,6 +53,11 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): """Return the list of supported features.""" return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return self.coordinator.entry.data[CONF_GIID] + @property def code_format(self) -> str: """Return one or more digits/characters.""" From c302b5d4eb3f0af827e0cff75100ba8db4aea054 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 26 May 2021 00:16:09 +0000 Subject: [PATCH 753/852] [ci skip] Translation update --- .../emulated_roku/translations/ru.json | 2 +- .../components/fritz/translations/nl.json | 9 ++++ .../components/fritz/translations/no.json | 9 ++++ .../fritz/translations/zh-Hant.json | 9 ++++ .../homekit_controller/translations/no.json | 2 + .../translations/nl.json | 1 + .../keenetic_ndms2/translations/en.json | 4 +- .../keenetic_ndms2/translations/et.json | 5 +- .../keenetic_ndms2/translations/ru.json | 6 ++- .../meteoclimatic/translations/en.json | 6 +-- .../meteoclimatic/translations/et.json | 20 ++++++++ .../meteoclimatic/translations/ru.json | 20 ++++++++ .../components/samsungtv/translations/nl.json | 8 ++- .../components/samsungtv/translations/no.json | 17 +++++-- .../components/sia/translations/nl.json | 44 ++++++++++++++++ .../components/sia/translations/no.json | 50 +++++++++++++++++++ .../components/wallbox/translations/nl.json | 22 ++++++++ .../components/wallbox/translations/no.json | 22 ++++++++ .../wallbox/translations/zh-Hant.json | 22 ++++++++ 19 files changed, 264 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/meteoclimatic/translations/et.json create mode 100644 homeassistant/components/meteoclimatic/translations/ru.json create mode 100644 homeassistant/components/sia/translations/nl.json create mode 100644 homeassistant/components/sia/translations/no.json create mode 100644 homeassistant/components/wallbox/translations/nl.json create mode 100644 homeassistant/components/wallbox/translations/no.json create mode 100644 homeassistant/components/wallbox/translations/zh-Hant.json diff --git a/homeassistant/components/emulated_roku/translations/ru.json b/homeassistant/components/emulated_roku/translations/ru.json index f0094930f83..47925bb4aec 100644 --- a/homeassistant/components/emulated_roku/translations/ru.json +++ b/homeassistant/components/emulated_roku/translations/ru.json @@ -17,5 +17,5 @@ } } }, - "title": "\u042d\u043c\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 Roku" + "title": "Emulated Roku" } \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json index 904dc6629b9..06c29cd9d38 100644 --- a/homeassistant/components/fritz/translations/nl.json +++ b/homeassistant/components/fritz/translations/nl.json @@ -51,5 +51,14 @@ "title": "Setup FRITZ!Box Tools" } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Seconden om een apparaat als \"thuis\" te beschouwen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json index 47c51349aca..0838efbf649 100644 --- a/homeassistant/components/fritz/translations/no.json +++ b/homeassistant/components/fritz/translations/no.json @@ -51,5 +51,14 @@ "title": "Sett opp FRITZ!Box verkt\u00f8y" } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Sekunder \u00e5 vurdere en enhet hjemme" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json index 900cbd7df59..861ec6d62ce 100644 --- a/homeassistant/components/fritz/translations/zh-Hant.json +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -51,5 +51,14 @@ "title": "\u8a2d\u5b9a FRITZ!Box Tools" } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u8996\u70ba\u5728\u5bb6\u7684\u7b49\u5019\u79d2\u6578" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/no.json b/homeassistant/components/homekit_controller/translations/no.json index 81e94e6a117..c383dddb763 100644 --- a/homeassistant/components/homekit_controller/translations/no.json +++ b/homeassistant/components/homekit_controller/translations/no.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Ugyldig HomeKit kode. Vennligst sjekk den og pr\u00f8v igjen.", + "insecure_setup_code": "Den forespurte installasjonskoden er usikker p\u00e5 grunn av triviell natur. Dette tilbeh\u00f8ret oppfyller ikke grunnleggende sikkerhetskrav.", "max_peers_error": "Enheten nekter \u00e5 sammenkoble da den ikke har ledig sammenkoblingslagring.", "pairing_failed": "En uh\u00e5ndtert feil oppstod under fors\u00f8k p\u00e5 \u00e5 koble til denne enheten. Dette kan v\u00e6re en midlertidig feil, eller at enheten din kan ikke st\u00f8ttes for \u00f8yeblikket.", "unable_to_pair": "Kunne ikke koble til, vennligst pr\u00f8v igjen.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Tillat sammenkobling med usikre oppsettkoder.", "pairing_code": "Sammenkoblingskode" }, "description": "HomeKit Controller kommuniserer med {name} over lokalnettverket ved hjelp av en sikker kryptert tilkobling uten en separat HomeKit-kontroller eller iCloud. Skriv inn HomeKit-paringskoden (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret. Denne koden finnes vanligvis p\u00e5 selve enheten eller i emballasjen.", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/nl.json b/homeassistant/components/hunterdouglas_powerview/translations/nl.json index e988b44a2de..588fa21c813 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/nl.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/nl.json @@ -7,6 +7,7 @@ "cannot_connect": "Verbinding mislukt, probeer het opnieuw", "unknown": "Onverwachte fout" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Wil je {name} ({host}) instellen?", diff --git a/homeassistant/components/keenetic_ndms2/translations/en.json b/homeassistant/components/keenetic_ndms2/translations/en.json index aafcf284e86..397a00ee1a1 100644 --- a/homeassistant/components/keenetic_ndms2/translations/en.json +++ b/homeassistant/components/keenetic_ndms2/translations/en.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Account is already configured", "no_udn": "SSDP discovery info has no UDN", - "not_keenetic_ndms2": "Discovered item is no a Keenetic router" + "not_keenetic_ndms2": "Discovered item is not a Keenetic router" }, "error": { "cannot_connect": "Failed to connect" @@ -35,4 +35,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/et.json b/homeassistant/components/keenetic_ndms2/translations/et.json index ccb23aff796..0375ccfccfe 100644 --- a/homeassistant/components/keenetic_ndms2/translations/et.json +++ b/homeassistant/components/keenetic_ndms2/translations/et.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Kasutaja on juba seadistatud" + "already_configured": "Kasutaja on juba seadistatud", + "no_udn": "SSDP tuvastusteabel pole UDN-i", + "not_keenetic_ndms2": "Avastatud \u00fcksus pole Keeneticu ruuter" }, "error": { "cannot_connect": "\u00dchendamine nurjus" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/ru.json b/homeassistant/components/keenetic_ndms2/translations/ru.json index fefcf6a4093..3c7eed4be01 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ru.json +++ b/homeassistant/components/keenetic_ndms2/translations/ru.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "no_udn": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0431 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 SSDP \u043d\u0435 \u0438\u043c\u0435\u0435\u0442 UDN.", + "not_keenetic_ndms2": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 Keenetic." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." @@ -33,4 +35,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/en.json b/homeassistant/components/meteoclimatic/translations/en.json index 4868d4e4656..a066971be25 100644 --- a/homeassistant/components/meteoclimatic/translations/en.json +++ b/homeassistant/components/meteoclimatic/translations/en.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Station already configured", - "unknown": "Unknown error: please try again later" + "already_configured": "Device is already configured", + "unknown": "Unexpected error" }, "error": { - "not_found": "The station code did not return any data. Check that the code belongs to a station and it has the right format (e.g., ESCAT4300000043206B)" + "not_found": "No devices found on the network" }, "step": { "user": { diff --git a/homeassistant/components/meteoclimatic/translations/et.json b/homeassistant/components/meteoclimatic/translations/et.json new file mode 100644 index 00000000000..e019a4a0e12 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "unknown": "Tundmatu t\u00f5rge" + }, + "error": { + "not_found": "Seadmeid ei leitud" + }, + "step": { + "user": { + "data": { + "code": "Jaama kood" + }, + "description": "Sisesta Meteoclimatic jaama kood (nt ESCAT4300000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/ru.json b/homeassistant/components/meteoclimatic/translations/ru.json new file mode 100644 index 00000000000..14df114c903 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "not_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "step": { + "user": { + "data": { + "code": "\u041a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 Meteoclimatic (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, ESCAT4300000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index 3f9e61c8a8c..c64b8beca78 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -5,7 +5,13 @@ "already_in_progress": "De configuratiestroom is al aan de gang", "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV.", "cannot_connect": "Kan geen verbinding maken", - "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund." + "id_missing": "Dit Samsung-apparaat heeft geen serienummer.", + "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund.", + "reauth_successful": "Herauthenticatie was succesvol", + "unknown": "Onverwachte fout" + }, + "error": { + "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV." }, "flow_title": "{model}", "step": { diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index 90c64e02a3e..6da9787d3f6 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -3,16 +3,25 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "auth_missing": "Home Assistant er ikke godkjent til \u00e5 koble til denne Samsung-TV. Vennligst kontroller innstillingene for TV-en for \u00e5 godkjenne Home Assistent.", + "auth_missing": "Home Assistant er ikke autorisert til \u00e5 koble til denne Samsung TV-en. Sjekk TV-ens innstillinger for ekstern enhetsbehandling for \u00e5 autorisere Home Assistant.", "cannot_connect": "Tilkobling mislyktes", - "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." + "id_missing": "Denne Samsung-enheten har ikke serienummer.", + "not_supported": "Denne Samsung-enheten st\u00f8ttes forel\u00f8pig ikke.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "unknown": "Uventet feil" }, - "flow_title": "{model}", + "error": { + "auth_missing": "Home Assistant er ikke autorisert til \u00e5 koble til denne Samsung TV-en. Sjekk TV-ens innstillinger for ekstern enhetsbehandling for \u00e5 autorisere Home Assistant." + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", + "description": "Vil du konfigurere {device} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 TV-en din som ber om autorisasjon.", "title": "" }, + "reauth_confirm": { + "description": "Etter innsending, godta popup-vinduet p\u00e5 {device} ber om autorisasjon innen 30 sekunder." + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/sia/translations/nl.json b/homeassistant/components/sia/translations/nl.json new file mode 100644 index 00000000000..789106ead23 --- /dev/null +++ b/homeassistant/components/sia/translations/nl.json @@ -0,0 +1,44 @@ +{ + "config": { + "error": { + "unknown": "Onverwachte fout" + }, + "step": { + "additional_account": { + "data": { + "account": "Account ID", + "additional_account": "Extra accounts", + "encryption_key": "Encryptiesleutel", + "ping_interval": "Ping Interval (min)", + "zones": "Aantal zones voor het account" + }, + "title": "Voeg nog een account toe aan de huidige poort." + }, + "user": { + "data": { + "account": "Account ID", + "additional_account": "Extra accounts", + "encryption_key": "Encryptiesleutel", + "ping_interval": "Ping Interval (min)", + "port": "Port", + "protocol": "Protocol", + "zones": "Aantal zones voor het account" + }, + "title": "Maak een verbinding voor SIA gebaseerde alarmsystemen." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Negeer de tijdstempelcontrole van de SIA-gebeurtenissen", + "zones": "Aantal zones voor het account" + }, + "description": "Stel de opties voor account in: {account}", + "title": "Opties voor de SIA Setup." + } + } + }, + "title": "SIA Alarm Systems" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/no.json b/homeassistant/components/sia/translations/no.json new file mode 100644 index 00000000000..cd09ae7cdff --- /dev/null +++ b/homeassistant/components/sia/translations/no.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "Kontoen er ikke en hex-verdi. Bruk bare 0-9 og AF.", + "invalid_account_length": "Kontoen har ikke riktig lengde, den m\u00e5 v\u00e6re mellom 3 og 16 tegn.", + "invalid_key_format": "N\u00f8kkelen er ikke en hex-verdi, bruk bare 0-9 og AF.", + "invalid_key_length": "N\u00f8kkelen har ikke riktig lengde, den m\u00e5 v\u00e6re p\u00e5 16, 24 eller 32 tegn med hex-tegn.", + "invalid_ping": "Ping-intervallet m\u00e5 v\u00e6re mellom 1 og 1440 minutter.", + "invalid_zones": "Det m\u00e5 v\u00e6re minst 1 sone.", + "unknown": "Uventet feil" + }, + "step": { + "additional_account": { + "data": { + "account": "Konto-ID", + "additional_account": "Flere kontoer", + "encryption_key": "Krypteringsn\u00f8kkel", + "ping_interval": "Ping-intervall (min)", + "zones": "Antall soner for kontoen" + }, + "title": "Legg til en annen konto i gjeldende port." + }, + "user": { + "data": { + "account": "Konto-ID", + "additional_account": "Flere kontoer", + "encryption_key": "Krypteringsn\u00f8kkel", + "ping_interval": "Ping-intervall (min)", + "port": "Port", + "protocol": "Protokoll", + "zones": "Antall soner for kontoen" + }, + "title": "Opprett en forbindelse for SIA-baserte alarmsystemer." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignorer tidsstempelkontrollen for SIA-hendelsene", + "zones": "Antall soner for kontoen" + }, + "description": "Angi alternativene for kontoen: {account}", + "title": "Alternativer for SIA-oppsett." + } + } + }, + "title": "SIA Alarm Systems" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/nl.json b/homeassistant/components/wallbox/translations/nl.json new file mode 100644 index 00000000000..6ba03e7ee99 --- /dev/null +++ b/homeassistant/components/wallbox/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "station": "Station Serienummer", + "username": "Gebruikersnaam" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/no.json b/homeassistant/components/wallbox/translations/no.json new file mode 100644 index 00000000000..42368703121 --- /dev/null +++ b/homeassistant/components/wallbox/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "station": "Serienummer for stasjon", + "username": "Brukernavn" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/zh-Hant.json b/homeassistant/components/wallbox/translations/zh-Hant.json new file mode 100644 index 00000000000..78a752f9a0d --- /dev/null +++ b/homeassistant/components/wallbox/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "station": "\u5de5\u4f5c\u7ad9\u5e8f\u865f", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file From 9b13350b01d18c8a5f61236e3cbcc53ae9830ace Mon Sep 17 00:00:00 2001 From: Johan Josua Storm Date: Wed, 26 May 2021 08:01:35 +0200 Subject: [PATCH 754/852] Replace wrong domain returned from xbox api revisited (#51074) * Added replacement http to https Somehow the fix of replacing the domain doesn't work on android, so explicit replacement of http to https protocol is needed. * Update homeassistant/components/xbox/base_sensor.py Co-authored-by: Jason Hunter Co-authored-by: Jason Hunter --- homeassistant/components/xbox/base_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index 894213fd94c..c463b31d3c5 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -55,7 +55,7 @@ class XboxBaseSensorEntity(CoordinatorEntity): # We need to also remove the 'mode=Padding' query because with it, it results in an error 400. url = URL(self.data.display_pic) if url.host == "images-eds.xboxlive.com": - url = url.with_host("images-eds-ssl.xboxlive.com") + url = url.with_host("images-eds-ssl.xboxlive.com").with_scheme("https") query = dict(url.query) query.pop("mode", None) return str(url.with_query(query)) From 154c849eac16de767aa91123b79e1a37bd727784 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 May 2021 08:02:59 +0200 Subject: [PATCH 755/852] Filter unsupported parameters from light service calls (#51084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Filter unsupported parameters from light service calls * Silence pylint * Fix deconz tests * Fix mqtt tests * Fix scene tests * Fix trådfri emulated CT * Fix mqtt tests --- homeassistant/components/light/__init__.py | 47 +++++- homeassistant/components/tradfri/light.py | 2 +- tests/components/deconz/test_light.py | 1 - tests/components/light/test_init.py | 180 ++++++++++++++++++++- tests/components/mqtt/test_light_json.py | 4 + tests/components/scene/test_init.py | 24 ++- 6 files changed, 243 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index bd1f21f8ecb..27f3bbfc0c6 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -247,11 +247,52 @@ def preprocess_turn_on_alternatives(hass, params): params[ATTR_BRIGHTNESS] = round(255 * brightness_pct / 100) -def filter_turn_off_params(params): +def filter_turn_off_params(light, params): """Filter out params not used in turn off.""" + supported_features = light.supported_features + + if not supported_features & SUPPORT_FLASH: + params.pop(ATTR_FLASH, None) + if not supported_features & SUPPORT_TRANSITION: + params.pop(ATTR_TRANSITION, None) + return {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)} +def filter_turn_on_params(light, params): + """Filter out params not used in turn off.""" + supported_features = light.supported_features + + if not supported_features & SUPPORT_EFFECT: + params.pop(ATTR_EFFECT, None) + if not supported_features & SUPPORT_FLASH: + params.pop(ATTR_FLASH, None) + if not supported_features & SUPPORT_TRANSITION: + params.pop(ATTR_TRANSITION, None) + if not supported_features & SUPPORT_WHITE_VALUE: + params.pop(ATTR_WHITE_VALUE, None) + + supported_color_modes = ( + light._light_internal_supported_color_modes # pylint:disable=protected-access + ) + if not brightness_supported(supported_color_modes): + params.pop(ATTR_BRIGHTNESS, None) + if COLOR_MODE_COLOR_TEMP not in supported_color_modes: + params.pop(ATTR_COLOR_TEMP, None) + if COLOR_MODE_HS not in supported_color_modes: + params.pop(ATTR_HS_COLOR, None) + if COLOR_MODE_RGB not in supported_color_modes: + params.pop(ATTR_RGB_COLOR, None) + if COLOR_MODE_RGBW not in supported_color_modes: + params.pop(ATTR_RGBW_COLOR, None) + if COLOR_MODE_RGBWW not in supported_color_modes: + params.pop(ATTR_RGBWW_COLOR, None) + if COLOR_MODE_XY not in supported_color_modes: + params.pop(ATTR_XY_COLOR, None) + + return params + + async def async_setup(hass, config): # noqa: C901 """Expose light control via state machine and services.""" component = hass.data[DOMAIN] = EntityComponent( @@ -373,7 +414,7 @@ async def async_setup(hass, config): # noqa: C901 if params.get(ATTR_BRIGHTNESS) == 0: await async_handle_light_off_service(light, call) else: - await light.async_turn_on(**params) + await light.async_turn_on(**filter_turn_on_params(light, params)) async def async_handle_light_off_service(light, call): """Handle turning off a light.""" @@ -382,7 +423,7 @@ async def async_setup(hass, config): # noqa: C901 if ATTR_TRANSITION not in params: profiles.apply_default(light.entity_id, True, params) - await light.async_turn_off(**filter_turn_off_params(params)) + await light.async_turn_off(**filter_turn_off_params(light, params)) async def async_handle_toggle_service(light, call): """Handle toggling a light.""" diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 5da2c2b9b1f..a4c2ee67865 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -116,7 +116,7 @@ class TradfriLight(TradfriBaseDevice, LightEntity): if device.light_control.can_set_dimmer: _features |= SUPPORT_BRIGHTNESS if device.light_control.can_set_color: - _features |= SUPPORT_COLOR + _features |= SUPPORT_COLOR | SUPPORT_COLOR_TEMP if device.light_control.can_set_temp: _features |= SUPPORT_COLOR_TEMP self._features = _features diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 84c7a9b1078..a5e27709ebf 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -179,7 +179,6 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): blocking=True, ) assert aioclient_mock.mock_calls[1][2] == { - "ct": 2500, "bri": 200, "transitiontime": 50, "alert": "select", diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 71764eec186..842fb305c6c 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -117,6 +117,16 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES + ent1.supported_color_modes = [light.COLOR_MODE_HS] + ent3.supported_color_modes = [light.COLOR_MODE_HS] + ent1.supported_features = light.SUPPORT_TRANSITION + ent2.supported_features = ( + light.SUPPORT_COLOR + | light.SUPPORT_EFFECT + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + ent3.supported_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION # Test init assert light.is_on(hass, ent1.entity_id) @@ -205,6 +215,7 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ent2.entity_id, + light.ATTR_EFFECT: "fun_effect", light.ATTR_RGB_COLOR: (255, 255, 255), light.ATTR_WHITE_VALUE: 255, }, @@ -215,6 +226,7 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ent3.entity_id, + light.ATTR_FLASH: "short", light.ATTR_XY_COLOR: (0.4, 0.6), }, blocking=True, @@ -228,10 +240,14 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): } _, data = ent2.last_call("turn_on") - assert data == {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} + assert data == { + light.ATTR_EFFECT: "fun_effect", + light.ATTR_HS_COLOR: (0, 0), + light.ATTR_WHITE_VALUE: 255, + } _, data = ent3.last_call("turn_on") - assert data == {light.ATTR_HS_COLOR: (71.059, 100)} + assert data == {light.ATTR_FLASH: "short", light.ATTR_HS_COLOR: (71.059, 100)} # Ensure attributes are filtered when light is turned off await hass.services.async_call( @@ -521,6 +537,8 @@ async def test_light_profiles( await hass.async_block_till_done() ent1, _, _ = platform.ENTITIES + ent1.supported_color_modes = [light.COLOR_MODE_HS] + ent1.supported_features = light.SUPPORT_TRANSITION await hass.services.async_call( light.DOMAIN, @@ -556,6 +574,8 @@ async def test_default_profiles_group( mock_light_profiles[profile.name] = profile ent, _, _ = platform.ENTITIES + ent.supported_color_modes = [light.COLOR_MODE_HS] + ent.supported_features = light.SUPPORT_TRANSITION await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ent.entity_id}, blocking=True ) @@ -661,6 +681,8 @@ async def test_default_profiles_light( mock_light_profiles[profile.name] = profile dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES)) + dev.supported_color_modes = [light.COLOR_MODE_HS] + dev.supported_features = light.SUPPORT_TRANSITION await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, @@ -1625,3 +1647,157 @@ async def test_light_state_color_conversion(hass, enable_custom_integrations): assert state.attributes["hs_color"] == (240, 100) assert state.attributes["rgb_color"] == (0, 0, 255) assert state.attributes["xy_color"] == (0.136, 0.04) + + +async def test_services_filter_parameters( + hass, mock_light_profiles, enable_custom_integrations +): + """Test turn_on and turn_off filters unsupported parameters.""" + platform = getattr(hass.components, "test.light") + + platform.init() + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + ent1, _, _ = platform.ENTITIES + + # turn off the light by setting brightness to 0, this should work even if the light + # doesn't support brightness + await hass.services.async_call( + light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True + ) + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL, light.ATTR_BRIGHTNESS: 0}, + blocking=True, + ) + + assert not light.is_on(hass, ent1.entity_id) + + # Ensure all unsupported attributes are filtered when light is turned on + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_BRIGHTNESS: 0, + light.ATTR_EFFECT: "fun_effect", + light.ATTR_FLASH: "short", + light.ATTR_TRANSITION: 10, + light.ATTR_WHITE_VALUE: 0, + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_COLOR_TEMP: 153, + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_HS_COLOR: (0, 0), + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_RGB_COLOR: (0, 0, 0), + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_RGBW_COLOR: (0, 0, 0, 0), + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_RGBWW_COLOR: (0, 0, 0, 0, 0), + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_XY_COLOR: (0, 0), + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + # Ensure all unsupported attributes are filtered when light is turned off + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_BRIGHTNESS: 0, + light.ATTR_EFFECT: "fun_effect", + light.ATTR_FLASH: "short", + light.ATTR_TRANSITION: 10, + light.ATTR_WHITE_VALUE: 0, + }, + blocking=True, + ) + + assert not light.is_on(hass, ent1.entity_id) + + _, data = ent1.last_call("turn_off") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_FLASH: "short", + light.ATTR_TRANSITION: 10, + }, + blocking=True, + ) + + assert not light.is_on(hass, ent1.entity_id) + + _, data = ent1.last_call("turn_off") + assert data == {} diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index f892b6a3bbd..f4bf11df026 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -945,6 +945,7 @@ async def test_sending_hs_color(hass, mqtt_mock): "command_topic": "test_light_rgb/set", "brightness": True, "hs": True, + "white_value": True, } }, ) @@ -1139,6 +1140,7 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): "command_topic": "test_light_rgb/set", "brightness": True, "rgb": True, + "white_value": True, } }, ) @@ -1209,6 +1211,7 @@ async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): "brightness": True, "brightness_scale": 100, "rgb": True, + "white_value": True, } }, ) @@ -1278,6 +1281,7 @@ async def test_sending_xy_color(hass, mqtt_mock): "command_topic": "test_light_rgb/set", "brightness": True, "xy": True, + "white_value": True, } }, ) diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 8263b9c3006..4c5b832ac14 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -117,6 +117,8 @@ async def test_activate_scene(hass, entities, enable_custom_integrations): assert light.is_on(hass, light_2.entity_id) assert light_2.last_call("turn_on")[1].get("brightness") == 100 + await turn_off_lights(hass, [light_2.entity_id]) + calls = async_mock_service(hass, "light", "turn_on") await hass.services.async_call( @@ -156,16 +158,22 @@ async def setup_lights(hass, entities): await hass.async_block_till_done() light_1, light_2 = entities + light_1.supported_color_modes = ["brightness"] + light_2.supported_color_modes = ["brightness"] - await hass.services.async_call( - "light", - "turn_off", - {"entity_id": [light_1.entity_id, light_2.entity_id]}, - blocking=True, - ) - await hass.async_block_till_done() - + await turn_off_lights(hass, [light_1.entity_id, light_2.entity_id]) assert not light.is_on(hass, light_1.entity_id) assert not light.is_on(hass, light_2.entity_id) return light_1, light_2 + + +async def turn_off_lights(hass, entity_ids): + """Turn lights off.""" + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": entity_ids}, + blocking=True, + ) + await hass.async_block_till_done() From c1d5dd7141ca8ca7c2cbd4b07a97560334d65cae Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Tue, 25 May 2021 23:13:26 -0700 Subject: [PATCH 756/852] Remove unneeded **kwargs from SmartTub reminders snooze service (#51093) --- homeassistant/components/smarttub/binary_sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index 2ca2e10245c..d1019f7f432 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -115,8 +115,7 @@ class SmartTubReminder(SmartTubEntity, BinarySensorEntity): """Return the device class for this entity.""" return DEVICE_CLASS_PROBLEM - async def async_snooze(self, **kwargs): + async def async_snooze(self, days): """Snooze this reminder for the specified number of days.""" - days = kwargs[ATTR_SNOOZE_DAYS] await self.reminder.snooze(days) await self.coordinator.async_request_refresh() From 5f7964b54b9e98e95d7a5efb0439f57487f581fb Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 26 May 2021 02:57:00 -0400 Subject: [PATCH 757/852] Add firmware updates support for zwave_js (#50390) * Add WS API support for zwave_js firmware updates * move file to fixture * review comments * fix logic and test based on upstream changes * handle failure scenario * handle failure scenario * fix tests and adjust message * Update homeassistant/components/zwave_js/api.py Co-authored-by: Paulus Schoutsen * remove return from firmware upload view because client will raise an exception if not successful * raise if user is not an admin * raise bad request exception if firmware command fails * incorporate #50923 * Add test for failed command * add event name to messages * change error to not found Co-authored-by: Paulus Schoutsen --- homeassistant/components/zwave_js/api.py | 148 ++++++++++++- tests/components/zwave_js/conftest.py | 7 + tests/components/zwave_js/test_api.py | 262 ++++++++++++++++++++++- 3 files changed, 412 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 8a08da13bfc..2d0fee54a18 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -6,12 +6,22 @@ from functools import wraps import json from typing import Callable -from aiohttp import hdrs, web, web_exceptions +from aiohttp import hdrs, web, web_exceptions, web_request import voluptuous as vol from zwave_js_server import dump from zwave_js_server.client import Client from zwave_js_server.const import CommandClass, LogLevel -from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed +from zwave_js_server.exceptions import ( + BaseZwaveJSServerError, + InvalidNewValue, + NotFoundError, + SetValueFailed, +) +from zwave_js_server.firmware import begin_firmware_update +from zwave_js_server.model.firmware import ( + FirmwareUpdateFinished, + FirmwareUpdateProgress, +) from zwave_js_server.model.log_config import LogConfig from zwave_js_server.model.log_message import LogMessage from zwave_js_server.model.node import Node @@ -28,6 +38,7 @@ from homeassistant.components.websocket_api.const import ( from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import Unauthorized from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntry @@ -147,7 +158,12 @@ def async_register_api(hass: HomeAssistant) -> None: hass, websocket_update_data_collection_preference ) websocket_api.async_register_command(hass, websocket_data_collection_status) + websocket_api.async_register_command(hass, websocket_abort_firmware_update) + websocket_api.async_register_command( + hass, websocket_subscribe_firmware_update_status + ) hass.http.register_view(DumpView()) + hass.http.register_view(FirmwareUploadView()) @websocket_api.require_admin @@ -1024,3 +1040,131 @@ class DumpView(HomeAssistantView): hdrs.CONTENT_DISPOSITION: 'attachment; filename="zwave_js_dump.json"', }, ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/abort_firmware_update", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_abort_firmware_update( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Abort a firmware update.""" + await node.async_abort_firmware_update() + connection.send_result(msg[ID]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/subscribe_firmware_update_status", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_subscribe_firmware_update_status( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Subsribe to the status of a firmware update.""" + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_progress(event: dict) -> None: + progress: FirmwareUpdateProgress = event["firmware_update_progress"] + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": event["event"], + "sent_fragments": progress.sent_fragments, + "total_fragments": progress.total_fragments, + }, + ) + ) + + @callback + def forward_finished(event: dict) -> None: + finished: FirmwareUpdateFinished = event["firmware_update_finished"] + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": event["event"], + "status": finished.status, + "wait_time": finished.wait_time, + }, + ) + ) + + unsubs = [ + node.on("firmware update progress", forward_progress), + node.on("firmware update finished", forward_finished), + ] + connection.subscriptions[msg["id"]] = async_cleanup + + connection.send_result(msg[ID]) + + +class FirmwareUploadView(HomeAssistantView): + """View to upload firmware.""" + + url = r"/api/zwave_js/firmware/upload/{config_entry_id}/{node_id:\d+}" + name = "api:zwave_js:firmware:upload" + + async def post( + self, request: web.Request, config_entry_id: str, node_id: str + ) -> web.Response: + """Handle upload.""" + if not request["hass_user"].is_admin: + raise Unauthorized() + hass = request.app["hass"] + if config_entry_id not in hass.data[DOMAIN]: + raise web_exceptions.HTTPBadRequest + + entry = hass.config_entries.async_get_entry(config_entry_id) + client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT] + node = client.driver.controller.nodes.get(int(node_id)) + if not node: + raise web_exceptions.HTTPNotFound + + # Increase max payload + request._client_max_size = 1024 * 1024 * 10 # pylint: disable=protected-access + + data = await request.post() + + if "file" not in data or not isinstance(data["file"], web_request.FileField): + raise web_exceptions.HTTPBadRequest + + uploaded_file: web_request.FileField = data["file"] + + try: + await begin_firmware_update( + entry.data[CONF_URL], + node, + uploaded_file.filename, + await hass.async_add_executor_job(uploaded_file.file.read), + async_get_clientsession(hass), + ) + except BaseZwaveJSServerError as err: + raise web_exceptions.HTTPBadRequest from err + + return self.json(None) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 2b6abacbf91..a2a712b59f1 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -1,6 +1,7 @@ """Provide common Z-Wave JS fixtures.""" import asyncio import copy +import io import json from unittest.mock import AsyncMock, patch @@ -717,3 +718,9 @@ def wallmote_central_scene_fixture(client, wallmote_central_scene_state): node = Node(client, copy.deepcopy(wallmote_central_scene_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="firmware_file") +def firmware_file_fixture(): + """Return mock firmware file stream.""" + return io.BytesIO(bytes(10)) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 596ed9c0ed9..fd6161b6f00 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -2,9 +2,15 @@ import json from unittest.mock import patch +import pytest from zwave_js_server.const import LogLevel from zwave_js_server.event import Event -from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed +from zwave_js_server.exceptions import ( + FailedCommand, + InvalidNewValue, + NotFoundError, + SetValueFailed, +) from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.zwave_js.api import ( @@ -1123,13 +1129,74 @@ async def test_dump_view(integration, hass_client): assert json.loads(await resp.text()) == [{"hello": "world"}, {"second": "msg"}] -async def test_dump_view_invalid_entry_id(integration, hass_client): +async def test_firmware_upload_view( + hass, multisensor_6, integration, hass_client, firmware_file +): + """Test the HTTP firmware upload view.""" + client = await hass_client() + with patch( + "homeassistant.components.zwave_js.api.begin_firmware_update", + ) as mock_cmd: + resp = await client.post( + f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", + data={"file": firmware_file}, + ) + assert mock_cmd.call_args[0][1:4] == (multisensor_6, "file", bytes(10)) + assert json.loads(await resp.text()) is None + + +async def test_firmware_upload_view_failed_command( + hass, multisensor_6, integration, hass_client, firmware_file +): + """Test failed command for the HTTP firmware upload view.""" + client = await hass_client() + with patch( + "homeassistant.components.zwave_js.api.begin_firmware_update", + side_effect=FailedCommand("test", "test"), + ): + resp = await client.post( + f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", + data={"file": firmware_file}, + ) + assert resp.status == 400 + + +async def test_firmware_upload_view_invalid_payload( + hass, multisensor_6, integration, hass_client +): + """Test an invalid payload for the HTTP firmware upload view.""" + client = await hass_client() + resp = await client.post( + f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", + data={"wrong_key": bytes(10)}, + ) + assert resp.status == 400 + + +@pytest.mark.parametrize( + "method, url", + [ + ("get", "/api/zwave_js/dump/INVALID"), + ("post", "/api/zwave_js/firmware/upload/INVALID/1"), + ], +) +async def test_view_invalid_entry_id(integration, hass_client, method, url): """Test an invalid config entry id parameter.""" client = await hass_client() - resp = await client.get("/api/zwave_js/dump/INVALID") + resp = await client.request(method, url) assert resp.status == 400 +@pytest.mark.parametrize( + "method, url", [("post", "/api/zwave_js/firmware/upload/{}/111")] +) +async def test_view_invalid_node_id(integration, hass_client, method, url): + """Test an invalid config entry id parameter.""" + client = await hass_client() + resp = await client.request(method, url.format(integration.entry_id)) + assert resp.status == 404 + + async def test_subscribe_logs(hass, integration, client, hass_ws_client): """Test the subscribe_logs websocket command.""" entry = integration @@ -1468,3 +1535,192 @@ async def test_data_collection(hass, client, integration, hass_ws_client): assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_abort_firmware_update( + hass, client, multisensor_6, integration, hass_ws_client +): + """Test that the abort_firmware_update WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command_no_wait.return_value = {} + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/abort_firmware_update", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.abort_firmware_update" + assert args["nodeId"] == multisensor_6.node_id + + +async def test_abort_firmware_update_failures( + hass, integration, multisensor_6, client, hass_ws_client +): + """Test failures for the abort_firmware_update websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + # Test sending command with improper entry ID fails + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/abort_firmware_update", + ENTRY_ID: "fake_entry_id", + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with improper node ID fails + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/abort_firmware_update", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id + 100, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/abort_firmware_update", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_subscribe_firmware_update_status( + hass, integration, multisensor_6, client, hass_ws_client +): + """Test the subscribe_firmware_update_status websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command_no_wait.return_value = {} + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/subscribe_firmware_update_status", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + event = Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": multisensor_6.node_id, + "sentFragments": 1, + "totalFragments": 10, + }, + ) + multisensor_6.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"] == { + "event": "firmware update progress", + "sent_fragments": 1, + "total_fragments": 10, + } + + event = Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": multisensor_6.node_id, + "status": 255, + "waitTime": 10, + }, + ) + multisensor_6.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"] == { + "event": "firmware update finished", + "status": 255, + "wait_time": 10, + } + + +async def test_subscribe_firmware_update_status_failures( + hass, integration, multisensor_6, client, hass_ws_client +): + """Test failures for the subscribe_firmware_update_status websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + # Test sending command with improper entry ID fails + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/subscribe_firmware_update_status", + ENTRY_ID: "fake_entry_id", + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with improper node ID fails + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/subscribe_firmware_update_status", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id + 100, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/subscribe_firmware_update_status", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED From d5a9419fb74152d773ddf3fc376378b95417fe5f Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 26 May 2021 09:19:30 +0200 Subject: [PATCH 758/852] Fix AsusWRT sensor test (#50956) * Fix AsusWRT sensor test * Revert use of utcnow * Add MockDevice class * Proper initialize static member * Added mock_device fixture --- tests/components/asuswrt/test_sensor.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index e69af0cd322..60ce6b1aa68 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -36,23 +36,28 @@ CONFIG_DATA = { CONF_MODE: "router", } -MOCK_DEVICES = { - "a1:b1:c1:d1:e1:f1": Device("a1:b1:c1:d1:e1:f1", "192.168.1.2", "Test"), - "a2:b2:c2:d2:e2:f2": Device("a2:b2:c2:d2:e2:f2", "192.168.1.3", "TestTwo"), -} MOCK_BYTES_TOTAL = [60000000000, 50000000000] MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] +@pytest.fixture(name="mock_devices") +def mock_devices_fixture(): + """Mock a list of devices.""" + return { + "a1:b1:c1:d1:e1:f1": Device("a1:b1:c1:d1:e1:f1", "192.168.1.2", "Test"), + "a2:b2:c2:d2:e2:f2": Device("a2:b2:c2:d2:e2:f2", "192.168.1.3", "TestTwo"), + } + + @pytest.fixture(name="connect") -def mock_controller_connect(): +def mock_controller_connect(mock_devices): """Mock a successful connection.""" with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock: service_mock.return_value.connection.async_connect = AsyncMock() service_mock.return_value.is_connected = True service_mock.return_value.connection.disconnect = Mock() service_mock.return_value.async_get_connected_devices = AsyncMock( - return_value=MOCK_DEVICES + return_value=mock_devices ) service_mock.return_value.async_get_bytes_total = AsyncMock( return_value=MOCK_BYTES_TOTAL @@ -63,7 +68,7 @@ def mock_controller_connect(): yield service_mock -async def test_sensors(hass, connect): +async def test_sensors(hass, connect, mock_devices): """Test creating an AsusWRT sensor.""" entity_reg = er.async_get(hass) @@ -134,10 +139,11 @@ async def test_sensors(hass, connect): assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2" # add one device and remove another - MOCK_DEVICES.pop("a1:b1:c1:d1:e1:f1") - MOCK_DEVICES["a3:b3:c3:d3:e3:f3"] = Device( + mock_devices.pop("a1:b1:c1:d1:e1:f1") + mock_devices["a3:b3:c3:d3:e3:f3"] = Device( "a3:b3:c3:d3:e3:f3", "192.168.1.4", "TestThree" ) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() From 41a940f644cb4518a2f70a0dd98e4845f64ebb96 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 26 May 2021 09:36:37 +0200 Subject: [PATCH 759/852] Add state class to Nettigo Air Monitor sensors (#50959) --- homeassistant/components/nam/const.py | 16 ++++++++++++++++ homeassistant/components/nam/model.py | 1 + homeassistant/components/nam/sensor.py | 5 +++-- tests/components/nam/test_sensor.py | 19 ++++++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 8171914b832..1c191019c04 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta from typing import Final +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -59,6 +60,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_BME280_PRESSURE: { ATTR_LABEL: f"{DEFAULT_NAME} BME280 Pressure", @@ -66,6 +68,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_BME280_TEMPERATURE: { ATTR_LABEL: f"{DEFAULT_NAME} BME280 Temperature", @@ -73,6 +76,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_BMP280_PRESSURE: { ATTR_LABEL: f"{DEFAULT_NAME} BMP280 Pressure", @@ -80,6 +84,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_BMP280_TEMPERATURE: { ATTR_LABEL: f"{DEFAULT_NAME} BMP280 Temperature", @@ -87,6 +92,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_HECA_HUMIDITY: { ATTR_LABEL: f"{DEFAULT_NAME} HECA Humidity", @@ -94,6 +100,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_HECA_TEMPERATURE: { ATTR_LABEL: f"{DEFAULT_NAME} HECA Temperature", @@ -101,6 +108,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_SHT3X_HUMIDITY: { ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Humidity", @@ -108,6 +116,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_SHT3X_TEMPERATURE: { ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Temperature", @@ -115,6 +124,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_SPS30_P0: { ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", @@ -122,6 +132,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:blur", ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_SPS30_P4: { ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", @@ -129,6 +140,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:blur", ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_DHT22_HUMIDITY: { ATTR_LABEL: f"{DEFAULT_NAME} DHT22 Humidity", @@ -136,6 +148,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_DHT22_TEMPERATURE: { ATTR_LABEL: f"{DEFAULT_NAME} DHT22 Temperature", @@ -143,6 +156,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_ICON: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_SIGNAL_STRENGTH: { ATTR_LABEL: f"{DEFAULT_NAME} Signal Strength", @@ -150,6 +164,7 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH, ATTR_ICON: None, ATTR_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_UPTIME: { ATTR_LABEL: f"{DEFAULT_NAME} Uptime", @@ -157,5 +172,6 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, ATTR_ICON: None, ATTR_ENABLED: False, + ATTR_STATE_CLASS: None, }, } diff --git a/homeassistant/components/nam/model.py b/homeassistant/components/nam/model.py index 8d1bfe29a4a..0cadaad647e 100644 --- a/homeassistant/components/nam/model.py +++ b/homeassistant/components/nam/model.py @@ -12,3 +12,4 @@ class SensorDescription(TypedDict): device_class: str | None icon: str | None enabled: bool + state_class: str | None diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 2774d87f2d3..30982d8571d 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from typing import Any -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON from homeassistant.core import HomeAssistant @@ -45,7 +45,8 @@ class NAMSensor(CoordinatorEntity, SensorEntity): """Initialize.""" super().__init__(coordinator) self.sensor_type = sensor_type - self._description = SENSORS[self.sensor_type] + self._description = SENSORS[sensor_type] + self._attr_state_class = SENSORS[sensor_type][ATTR_STATE_CLASS] @property def name(self) -> str: diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 2dfdc8987bc..b4c92c92e67 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -5,7 +5,11 @@ from unittest.mock import patch from nettigo_air_monitor import ApiError from homeassistant.components.nam.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -57,6 +61,7 @@ async def test_sensor(hass): assert state assert state.state == "45.7" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.nettigo_air_monitor_bme280_humidity") @@ -67,6 +72,7 @@ async def test_sensor(hass): assert state assert state.state == "7.6" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bme280_temperature") @@ -77,6 +83,7 @@ async def test_sensor(hass): assert state assert state.state == "1011" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA entry = registry.async_get("sensor.nettigo_air_monitor_bme280_pressure") @@ -87,6 +94,7 @@ async def test_sensor(hass): assert state assert state.state == "5.6" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_temperature") @@ -97,6 +105,7 @@ async def test_sensor(hass): assert state assert state.state == "1022" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_pressure") @@ -107,6 +116,7 @@ async def test_sensor(hass): assert state assert state.state == "34.7" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_humidity") @@ -117,6 +127,7 @@ async def test_sensor(hass): assert state assert state.state == "6.3" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_temperature") @@ -127,6 +138,7 @@ async def test_sensor(hass): assert state assert state.state == "46.2" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity") @@ -137,6 +149,7 @@ async def test_sensor(hass): assert state assert state.state == "6.3" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature") @@ -147,6 +160,7 @@ async def test_sensor(hass): assert state assert state.state == "50.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.nettigo_air_monitor_heca_humidity") @@ -157,6 +171,7 @@ async def test_sensor(hass): assert state assert state.state == "8.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_heca_temperature") @@ -167,6 +182,7 @@ async def test_sensor(hass): assert state assert state.state == "-72" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT @@ -183,6 +199,7 @@ async def test_sensor(hass): == (utcnow() - timedelta(seconds=456987)).replace(microsecond=0).isoformat() ) assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.nettigo_air_monitor_uptime") assert entry From 58586d5e1f60224ca77385c4f9ae1d4da5361d07 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 10:18:26 +0200 Subject: [PATCH 760/852] Use entity class vars in Elgato (#50973) --- homeassistant/components/elgato/light.py | 44 +++++++----------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index abd1fae410e..46060fe23fb 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -67,32 +67,27 @@ class ElgatoLight(LightEntity): self._state: State | None = None self.elgato = elgato - self._min_mired = 143 - self._max_mired = 344 - self._supported_color_modes = {COLOR_MODE_COLOR_TEMP} + min_mired = 143 + max_mired = 344 + supported_color_modes = {COLOR_MODE_COLOR_TEMP} # Elgato Light supporting color, have a different temperature range if settings.power_on_hue is not None: - self._supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} - self._min_mired = 153 - self._max_mired = 285 + supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + min_mired = 153 + max_mired = 285 - @property - def name(self) -> str: - """Return the name of the entity.""" - # Return the product name, if display name is not set - return self._info.display_name or self._info.product_name + self._attr_max_mired = max_mired + self._attr_min_mired = min_mired + self._attr_name = info.display_name or info.product_name + self._attr_supported_color_modes = supported_color_modes + self._attr_unique_id = info.serial_number @property def available(self) -> bool: """Return True if entity is available.""" return self._state is not None - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return self._info.serial_number - @property def brightness(self) -> int | None: """Return the brightness of this light between 1..255.""" @@ -105,22 +100,6 @@ class ElgatoLight(LightEntity): assert self._state is not None return self._state.temperature - @property - def min_mireds(self) -> int: - """Return the coldest color_temp that this light supports.""" - return self._min_mired - - @property - def max_mireds(self) -> int: - """Return the warmest color_temp that this light supports.""" - # Elgato lights with color capabilities have a different highest value - return self._max_mired - - @property - def supported_color_modes(self) -> set[str]: - """Flag supported color modes.""" - return self._supported_color_modes - @property def color_mode(self) -> str | None: """Return the color mode of the light.""" @@ -175,6 +154,7 @@ class ElgatoLight(LightEntity): brightness and ATTR_HS_COLOR not in kwargs and ATTR_COLOR_TEMP not in kwargs + and self.supported_color_modes and COLOR_MODE_HS in self.supported_color_modes and self.color_mode == COLOR_MODE_COLOR_TEMP ): From c6f108f7c3c79673616db85b8d01b1206246ed62 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 26 May 2021 16:19:09 +0800 Subject: [PATCH 761/852] Refactor stream to use bytes (#51066) * Refactor stream to use bytes --- homeassistant/components/stream/core.py | 8 ++- homeassistant/components/stream/fmp4utils.py | 58 +++++++++----------- homeassistant/components/stream/hls.py | 12 ++-- homeassistant/components/stream/recorder.py | 7 ++- homeassistant/components/stream/worker.py | 15 +++-- tests/components/stream/test_hls.py | 38 ++++++------- tests/components/stream/test_recorder.py | 28 ++++++++-- 7 files changed, 91 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 0d29474858f..695f1d05ac3 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections import deque -import io from typing import Callable from aiohttp import web @@ -19,12 +18,15 @@ from .const import ATTR_STREAMS, DOMAIN PROVIDERS = Registry() -@attr.s +@attr.s(slots=True) class Segment: """Represent a segment.""" sequence: int = attr.ib() - segment: io.BytesIO = attr.ib() + # the init of the mp4 + init: bytes = attr.ib() + # the video data (moof + mddat)s of the mp4 + moof_data: bytes = attr.ib() duration: float = attr.ib() # For detecting discontinuities across stream restarts stream_id: int = attr.ib(default=0) diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index ad5b100ce77..511bbc0939a 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -2,67 +2,59 @@ from __future__ import annotations from collections.abc import Generator -import io def find_box( - segment: io.BytesIO, target_type: bytes, box_start: int = 0 + mp4_bytes: bytes | memoryview, target_type: bytes, box_start: int = 0 ) -> Generator[int, None, None]: """Find location of first box (or sub_box if box_start provided) of given type.""" if box_start == 0: - box_end = segment.seek(0, io.SEEK_END) - segment.seek(0) index = 0 + box_end = len(mp4_bytes) else: - segment.seek(box_start) - box_end = box_start + int.from_bytes(segment.read(4), byteorder="big") + box_end = box_start + int.from_bytes( + mp4_bytes[box_start : box_start + 4], byteorder="big" + ) index = box_start + 8 while 1: if index > box_end - 8: # End of box, not found break - segment.seek(index) - box_header = segment.read(8) + box_header = mp4_bytes[index : index + 8] if box_header[4:8] == target_type: yield index - segment.seek(index) index += int.from_bytes(box_header[0:4], byteorder="big") -def get_init(segment: io.BytesIO) -> bytes: - """Get init section from fragmented mp4.""" - moof_location = next(find_box(segment, b"moof")) - segment.seek(0) - return segment.read(moof_location) +def get_init_and_moof_data(segment: memoryview) -> tuple[bytes, bytes]: + """Get the init and moof data from a segment.""" + moof_location = next(find_box(segment, b"moof"), 0) + mfra_location = next(find_box(segment, b"mfra"), len(segment)) + return ( + segment[:moof_location].tobytes(), + segment[moof_location:mfra_location].tobytes(), + ) -def get_m4s(segment: io.BytesIO, sequence: int) -> bytes: - """Get m4s section from fragmented mp4.""" - moof_location = next(find_box(segment, b"moof")) - mfra_location = next(find_box(segment, b"mfra")) - segment.seek(moof_location) - return segment.read(mfra_location - moof_location) - - -def get_codec_string(segment: io.BytesIO) -> str: +def get_codec_string(mp4_bytes: bytes) -> str: """Get RFC 6381 codec string.""" codecs = [] # Find moov - moov_location = next(find_box(segment, b"moov")) + moov_location = next(find_box(mp4_bytes, b"moov")) # Find tracks - for trak_location in find_box(segment, b"trak", moov_location): + for trak_location in find_box(mp4_bytes, b"trak", moov_location): # Drill down to media info - mdia_location = next(find_box(segment, b"mdia", trak_location)) - minf_location = next(find_box(segment, b"minf", mdia_location)) - stbl_location = next(find_box(segment, b"stbl", minf_location)) - stsd_location = next(find_box(segment, b"stsd", stbl_location)) + mdia_location = next(find_box(mp4_bytes, b"mdia", trak_location)) + minf_location = next(find_box(mp4_bytes, b"minf", mdia_location)) + stbl_location = next(find_box(mp4_bytes, b"stbl", minf_location)) + stsd_location = next(find_box(mp4_bytes, b"stsd", stbl_location)) # Get stsd box - segment.seek(stsd_location) - stsd_length = int.from_bytes(segment.read(4), byteorder="big") - segment.seek(stsd_location) - stsd_box = segment.read(stsd_length) + stsd_length = int.from_bytes( + mp4_bytes[stsd_location : stsd_location + 4], byteorder="big" + ) + stsd_box = mp4_bytes[stsd_location : stsd_location + stsd_length] # Base Codec codec = stsd_box[20:24].decode("utf-8") diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 42f7f2dbfa3..941f4407423 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -1,13 +1,11 @@ """Provide functionality to stream HLS.""" -import io - from aiohttp import web from homeassistant.core import callback from .const import FORMAT_CONTENT_TYPE, MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS from .core import PROVIDERS, HomeAssistant, IdleTimer, StreamOutput, StreamView -from .fmp4utils import get_codec_string, get_init, get_m4s +from .fmp4utils import get_codec_string @callback @@ -35,9 +33,9 @@ class HlsMasterPlaylistView(StreamView): # hls spec already allows for 25% variation segment = track.get_segment(track.segments[-1]) bandwidth = round( - segment.segment.seek(0, io.SEEK_END) * 8 / segment.duration * 1.2 + (len(segment.init) + len(segment.moof_data)) * 8 / segment.duration * 1.2 ) - codecs = get_codec_string(segment.segment) + codecs = get_codec_string(segment.init) lines = [ "#EXTM3U", f'#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},CODECS="{codecs}"', @@ -129,7 +127,7 @@ class HlsInitView(StreamView): if not segments: return web.HTTPNotFound() headers = {"Content-Type": "video/mp4"} - return web.Response(body=get_init(segments[0].segment), headers=headers) + return web.Response(body=segments[0].init, headers=headers) class HlsSegmentView(StreamView): @@ -147,7 +145,7 @@ class HlsSegmentView(StreamView): return web.HTTPNotFound() headers = {"Content-Type": "video/iso.segment"} return web.Response( - body=get_m4s(segment.segment, int(sequence)), + body=segment.moof_data, headers=headers, ) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 085a6448597..7d849375ece 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections import deque +from io import BytesIO import logging import os import threading @@ -51,7 +52,11 @@ def recorder_save_worker(file_out: str, segments: deque[Segment]): last_sequence = segment.sequence # Open segment - source = av.open(segment.segment, "r", format=SEGMENT_CONTAINER_FORMAT) + source = av.open( + BytesIO(segment.init + segment.moof_data), + "r", + format=SEGMENT_CONTAINER_FORMAT, + ) source_v = source.streams.video[0] source_a = source.streams.audio[0] if len(source.streams.audio) > 0 else None diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index d6562cf93db..cb6d6a6a017 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -19,6 +19,7 @@ from .const import ( STREAM_TIMEOUT, ) from .core import Segment, StreamOutput +from .fmp4utils import get_init_and_moof_data _LOGGER = logging.getLogger(__name__) @@ -29,8 +30,6 @@ class SegmentBuffer: def __init__(self, outputs_callback) -> None: """Initialize SegmentBuffer.""" self._stream_id = 0 - self._video_stream = None - self._audio_stream = None self._outputs_callback = outputs_callback self._outputs: list[StreamOutput] = [] self._sequence = 0 @@ -41,10 +40,11 @@ class SegmentBuffer: self._input_audio_stream = None # av.audio.AudioStream | None self._output_video_stream: av.video.VideoStream = None self._output_audio_stream = None # av.audio.AudioStream | None + self._segment: Segment = cast(Segment, None) @staticmethod def make_new_av( - memory_file, sequence: int, input_vstream: av.video.VideoStream + memory_file: BytesIO, sequence: int, input_vstream: av.video.VideoStream ) -> av.container.OutputContainer: """Make a new av OutputContainer.""" return av.open( @@ -120,7 +120,13 @@ class SegmentBuffer: def flush(self, duration): """Create a segment from the buffered packets and write to output.""" self._av_output.close() - segment = Segment(self._sequence, self._memory_file, duration, self._stream_id) + segment = Segment( + self._sequence, + *get_init_and_moof_data(self._memory_file.getbuffer()), + duration, + self._stream_id, + ) + self._memory_file.close() for stream_output in self._outputs: stream_output.put(segment) @@ -134,6 +140,7 @@ class SegmentBuffer: def close(self): """Close stream buffer.""" self._av_output.close() + self._memory_file.close() def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901 diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index ab0c21efdfb..f9b96a662d9 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -1,6 +1,5 @@ """The tests for hls streams.""" from datetime import timedelta -import io from unittest.mock import patch from urllib.parse import urlparse @@ -18,7 +17,8 @@ from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video STREAM_SOURCE = "some-stream-source" -SEQUENCE_BYTES = io.BytesIO(b"some-bytes") +INIT_BYTES = b"init" +MOOF_BYTES = b"some-bytes" DURATION = 10 TEST_TIMEOUT = 5.0 # Lower than 9s home assistant timeout MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever @@ -248,7 +248,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): stream_worker_sync.pause() hls = stream.add_provider("hls") - hls.put(Segment(1, SEQUENCE_BYTES, DURATION)) + hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION)) await hass.async_block_till_done() hls_client = await hls_stream(stream) @@ -257,7 +257,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): assert resp.status == 200 assert await resp.text() == make_playlist(sequence=1, segments=[make_segment(1)]) - hls.put(Segment(2, SEQUENCE_BYTES, DURATION)) + hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION)) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") assert resp.status == 200 @@ -281,7 +281,7 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): # Produce enough segments to overfill the output buffer by one for sequence in range(1, MAX_SEGMENTS + 2): - hls.put(Segment(sequence, SEQUENCE_BYTES, DURATION)) + hls.put(Segment(sequence, INIT_BYTES, MOOF_BYTES, DURATION)) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") @@ -297,18 +297,14 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): segments=segments, ) - # Fetch the actual segments with a fake byte payload - with patch( - "homeassistant.components.stream.hls.get_m4s", return_value=b"fake-payload" - ): - # The segment that fell off the buffer is not accessible - segment_response = await hls_client.get("/segment/1.m4s") - assert segment_response.status == 404 + # The segment that fell off the buffer is not accessible + segment_response = await hls_client.get("/segment/1.m4s") + assert segment_response.status == 404 - # However all segments in the buffer are accessible, even those that were not in the playlist. - for sequence in range(2, MAX_SEGMENTS + 2): - segment_response = await hls_client.get(f"/segment/{sequence}.m4s") - assert segment_response.status == 200 + # However all segments in the buffer are accessible, even those that were not in the playlist. + for sequence in range(2, MAX_SEGMENTS + 2): + segment_response = await hls_client.get(f"/segment/{sequence}.m4s") + assert segment_response.status == 200 stream_worker_sync.resume() stream.stop() @@ -322,9 +318,9 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s stream_worker_sync.pause() hls = stream.add_provider("hls") - hls.put(Segment(1, SEQUENCE_BYTES, DURATION, stream_id=0)) - hls.put(Segment(2, SEQUENCE_BYTES, DURATION, stream_id=0)) - hls.put(Segment(3, SEQUENCE_BYTES, DURATION, stream_id=1)) + hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) + hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) + hls.put(Segment(3, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=1)) await hass.async_block_till_done() hls_client = await hls_stream(stream) @@ -354,11 +350,11 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy hls_client = await hls_stream(stream) - hls.put(Segment(1, SEQUENCE_BYTES, DURATION, stream_id=0)) + hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) # Produce enough segments to overfill the output buffer by one for sequence in range(1, MAX_SEGMENTS + 2): - hls.put(Segment(sequence, SEQUENCE_BYTES, DURATION, stream_id=1)) + hls.put(Segment(sequence, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=1)) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 5ee055754b9..9097d03a7a9 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -1,10 +1,13 @@ """The tests for hls streams.""" +from __future__ import annotations + import asyncio +from collections import deque from datetime import timedelta +from io import BytesIO import logging import os import threading -from typing import Deque from unittest.mock import patch import async_timeout @@ -13,6 +16,7 @@ import pytest from homeassistant.components.stream import create_stream from homeassistant.components.stream.core import Segment +from homeassistant.components.stream.fmp4utils import get_init_and_moof_data from homeassistant.components.stream.recorder import recorder_save_worker from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -37,8 +41,9 @@ class SaveRecordWorkerSync: """Initialize SaveRecordWorkerSync.""" self.reset() self._segments = None + self._save_thread = None - def recorder_save_worker(self, file_out: str, segments: Deque[Segment]): + def recorder_save_worker(self, file_out: str, segments: deque[Segment]): """Mock method for patch.""" logging.debug("recorder_save_worker thread started") assert self._save_thread is None @@ -180,7 +185,9 @@ async def test_recorder_save(tmpdir): filename = f"{tmpdir}/test.mp4" # Run - recorder_save_worker(filename, [Segment(1, source, 4)]) + recorder_save_worker( + filename, [Segment(1, *get_init_and_moof_data(source.getbuffer()), 4)] + ) # Assert assert os.path.exists(filename) @@ -193,13 +200,20 @@ async def test_recorder_discontinuity(tmpdir): filename = f"{tmpdir}/test.mp4" # Run - recorder_save_worker(filename, [Segment(1, source, 4, 0), Segment(2, source, 4, 1)]) + init, moof_data = get_init_and_moof_data(source.getbuffer()) + recorder_save_worker( + filename, + [ + Segment(1, init, moof_data, 4, 0), + Segment(2, init, moof_data, 4, 1), + ], + ) # Assert assert os.path.exists(filename) -async def test_recorder_no_segements(tmpdir): +async def test_recorder_no_segments(tmpdir): """Test recorder behavior with a stream failure which causes no segments.""" # Setup filename = f"{tmpdir}/test.mp4" @@ -247,7 +261,9 @@ async def test_record_stream_audio( last_segment = segment stream_worker_sync.resume() - result = av.open(last_segment.segment, "r", format="mp4") + result = av.open( + BytesIO(last_segment.init + last_segment.moof_data), "r", format="mp4" + ) assert len(result.streams.audio) == expected_audio_streams result.close() From 5ee0df29d4cb91726acb44e26a17cc1fca3cb768 Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Wed, 26 May 2021 15:26:23 +0700 Subject: [PATCH 762/852] Remove old Keenetic NDMS2 entities when some interfaces are unselected (#47311) Co-authored-by: Martin Hjelmare --- .../components/keenetic_ndms2/__init__.py | 41 ++++++++++++++++++- .../components/keenetic_ndms2/router.py | 8 +++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index 787e6a5f5f1..473acac57cd 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -1,9 +1,14 @@ """The keenetic_ndms2 component.""" +from __future__ import annotations -from homeassistant.components import binary_sensor, device_tracker +import logging + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry, entity_registry from .const import ( CONF_CONSIDER_HOME, @@ -20,7 +25,8 @@ from .const import ( ) from .router import KeeneticRouter -PLATFORMS = [device_tracker.DOMAIN, binary_sensor.DOMAIN] +PLATFORMS = [BINARY_SENSOR_DOMAIN, DEVICE_TRACKER_DOMAIN] +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: @@ -57,6 +63,37 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> hass.data[DOMAIN].pop(config_entry.entry_id) + new_tracked_interfaces: set[str] = set(config_entry.options[CONF_INTERFACES]) + + if router.tracked_interfaces - new_tracked_interfaces: + _LOGGER.debug( + "Cleaning device_tracker entities since some interfaces are now untracked:" + ) + ent_reg = entity_registry.async_get(hass) + dev_reg = device_registry.async_get(hass) + # We keep devices currently connected to new_tracked_interfaces + keep_devices: set[str] = { + mac + for mac, device in router.last_devices.items() + if device.interface in new_tracked_interfaces + } + for entity_entry in list(ent_reg.entities.values()): + if ( + entity_entry.config_entry_id == config_entry.entry_id + and entity_entry.domain == DEVICE_TRACKER_DOMAIN + ): + mac = entity_entry.unique_id.partition("_")[0] + if mac not in keep_devices: + _LOGGER.debug("Removing entity %s", entity_entry.entity_id) + + ent_reg.async_remove(entity_entry.entity_id) + dev_reg.async_update_device( + entity_entry.device_id, + remove_config_entry_id=config_entry.entry_id, + ) + + _LOGGER.debug("Finished cleaning device_tracker entities") + return unload_ok diff --git a/homeassistant/components/keenetic_ndms2/router.py b/homeassistant/components/keenetic_ndms2/router.py index 87841d8291c..d79f2591525 100644 --- a/homeassistant/components/keenetic_ndms2/router.py +++ b/homeassistant/components/keenetic_ndms2/router.py @@ -48,6 +48,7 @@ class KeeneticRouter: self._cancel_periodic_update: Callable | None = None self._available = False self._progress = None + self._tracked_interfaces = set(config_entry.options[CONF_INTERFACES]) @property def client(self): @@ -105,6 +106,11 @@ class KeeneticRouter: """Config entry option defining number of seconds from last seen to away.""" return timedelta(seconds=self.config_entry.options[CONF_CONSIDER_HOME]) + @property + def tracked_interfaces(self): + """Tracked interfaces.""" + return self._tracked_interfaces + @property def signal_update(self): """Event specific per router entry to signal updates.""" @@ -178,7 +184,7 @@ class KeeneticRouter: self._last_devices = { dev.mac: dev for dev in _response - if dev.interface in self.config_entry.options[CONF_INTERFACES] + if dev.interface in self._tracked_interfaces } _LOGGER.debug("Successfully fetched data from router: %s", str(_response)) self._router_info = self._client.get_router_info() From b3607343fc6bd5a832b79c128d1322a197b77a77 Mon Sep 17 00:00:00 2001 From: Raj Laud <50647620+rajlaud@users.noreply.github.com> Date: Wed, 26 May 2021 03:30:15 -0500 Subject: [PATCH 763/852] Fix error in Squeezebox DHCP discovery flow (#50771) * Map data from dhcp to squeezebox discovery flow * Add tests for squeezebox dhcp discovery --- .../components/squeezebox/config_flow.py | 16 +++++++-- .../components/squeezebox/test_config_flow.py | 36 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index f81ff505583..1f1c23942db 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -6,6 +6,7 @@ from pysqueezebox import Server, async_discover import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -172,10 +173,21 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(discovery_info.pop("uuid")) self._abort_if_unique_id_configured() else: - # attempt to connect to server and determine uuid. will fail if password required + # attempt to connect to server and determine uuid. will fail if + # password required + + if CONF_HOST not in discovery_info and IP_ADDRESS in discovery_info: + discovery_info[CONF_HOST] = discovery_info[IP_ADDRESS] + + if CONF_PORT not in discovery_info: + discovery_info[CONF_PORT] = DEFAULT_PORT + error = await self._validate_input(discovery_info) if error: - await self._async_handle_discovery_without_unique_id() + if MAC_ADDRESS in discovery_info: + await self.async_set_unique_id(discovery_info[MAC_ADDRESS]) + else: + await self._async_handle_discovery_without_unique_id() # update schema with suggested values from discovery self.data_schema = _base_schema(discovery_info) diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index cd734e1627c..e740ea671cd 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from pysqueezebox import Server from homeassistant import config_entries +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.squeezebox.const import DOMAIN from homeassistant.const import ( CONF_HOST, @@ -196,6 +197,41 @@ async def test_discovery_no_uuid(hass): assert result["step_id"] == "edit" +async def test_dhcp_discovery(hass): + """Test we can process discovery from dhcp.""" + with patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "edit" + + +async def test_dhcp_discovery_no_connection(hass): + """Test we can process discovery from dhcp without connecting to squeezebox server.""" + with patch("pysqueezebox.Server.async_query", new=patch_async_query_unauthorized): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "edit" + + async def test_import(hass): """Test handling of configuration imported.""" with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch( From 19c9675d4a0b69799f0b606e6782fde65cfaaf88 Mon Sep 17 00:00:00 2001 From: chpego <38792705+chpego@users.noreply.github.com> Date: Wed, 26 May 2021 08:32:43 +0000 Subject: [PATCH 764/852] Bump youtube-dl to 2021.04.26 (#50037) --- 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 1d59c02d9ac..d7dd665b9d9 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2021.04.17"], + "requirements": ["youtube_dl==2021.04.26"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal", diff --git a/requirements_all.txt b/requirements_all.txt index a3de0bb383e..867bb347fae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2400,7 +2400,7 @@ yeelight==0.6.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2021.04.17 +youtube_dl==2021.04.26 # homeassistant.components.onvif zeep[async]==4.0.0 From 4f9b7254d2b66b6438ec8a30675d390ac8258450 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 26 May 2021 11:21:11 +0200 Subject: [PATCH 765/852] Initialize KNX expose value (#49623) * simplify value extraction * allow 0/1 and "True" / "False" for binary exposes * initialize ExposeSensor value * handle binary states * use default for initialization --- homeassistant/components/knx/expose.py | 61 ++++++++++++++------------ 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 5616a6deb23..5c371445cc4 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -13,7 +13,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, StateType @@ -74,6 +74,7 @@ class KNXExposeSensor: self.address = address self._remove_listener: Callable[[], None] | None = None self.device: ExposeSensor = self.async_register() + self._init_expose_state() @callback def async_register(self) -> ExposeSensor: @@ -93,6 +94,15 @@ class KNXExposeSensor: ) return device + @callback + def _init_expose_state(self) -> None: + """Initialize state of the exposure.""" + init_state = self.hass.states.get(self.entity_id) + init_value = self._get_expose_value(init_state) + self.device.sensor_value.value = ( + init_value if init_value is not None else self.expose_default + ) + @callback def shutdown(self) -> None: """Prepare for deletion.""" @@ -101,45 +111,40 @@ class KNXExposeSensor: self._remove_listener = None self.device.shutdown() + def _get_expose_value(self, state: State | None) -> StateType: + """Extract value from state.""" + if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + return None + value = ( + state.state + if self.expose_attribute is None + else state.attributes.get(self.expose_attribute) + ) + if self.type == "binary": + if value in (1, STATE_ON, "True"): + value = True + elif value in (0, STATE_OFF, "False"): + value = False + return value + async def _async_entity_changed(self, event: Event) -> None: """Handle entity change.""" new_state = event.data.get("new_state") - if new_state is None: + new_value = self._get_expose_value(new_state) + if new_value is None: return - if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): - return - old_state = event.data.get("old_state") - - if self.expose_attribute is None: - if old_state is None or old_state.state != new_state.state: - # don't send same value sequentially - await self._async_set_knx_value(new_state.state) - return - - new_attribute = new_state.attributes.get(self.expose_attribute) - - if old_state is not None: - old_attribute = old_state.attributes.get(self.expose_attribute) - if old_attribute == new_attribute: - # don't send same value sequentially - return - await self._async_set_knx_value(new_attribute) + old_value = self._get_expose_value(old_state) + # don't send same value sequentially + if new_value != old_value: + await self._async_set_knx_value(new_value) async def _async_set_knx_value(self, value: StateType) -> None: """Set new value on xknx ExposeSensor.""" - assert self.device is not None if value is None: if self.expose_default is None: return value = self.expose_default - - if self.type == "binary": - if value == STATE_ON: - value = True - elif value == STATE_OFF: - value = False - await self.device.set(value) From 789a14fc445e020c037b972ca93db548e2aa51e5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 May 2021 11:50:29 +0200 Subject: [PATCH 766/852] Add support for last_reset to MQTT sensor (#51036) * Add support for last_reset to MQTT sensor * Update abbreviations * Improve test coverage --- .../components/mqtt/abbreviations.py | 2 + homeassistant/components/mqtt/sensor.py | 64 ++++++++++-- tests/components/mqtt/test_sensor.py | 98 +++++++++++++++++++ 3 files changed, 155 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index a16d721ba7c..6c572c093a3 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -77,6 +77,8 @@ ABBREVIATIONS = { "json_attr": "json_attributes", "json_attr_t": "json_attributes_topic", "json_attr_tpl": "json_attributes_template", + "lrst_t": "last_reset_topic", + "lrst_val_tpl": "last_reset_value_template", "max": "max", "min": "min", "max_mirs": "max_mireds", diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 145af55daa8..51caeb5f6da 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import functools +import logging import voluptuous as vol @@ -36,7 +37,11 @@ from .mixins import ( async_setup_entry_helper, ) +_LOGGER = logging.getLogger(__name__) + CONF_EXPIRE_AFTER = "expire_after" +CONF_LAST_RESET_TOPIC = "last_reset_topic" +CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template" CONF_STATE_CLASS = "state_class" DEFAULT_NAME = "MQTT Sensor" @@ -46,6 +51,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( 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_LAST_RESET_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -79,6 +86,8 @@ async def _async_setup_entity( class MqttSensor(MqttEntity, SensorEntity): """Representation of a sensor that can be updated using MQTT.""" + _attr_last_reset = None + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the sensor.""" self._state = None @@ -102,9 +111,13 @@ class MqttSensor(MqttEntity, SensorEntity): template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: template.hass = self.hass + last_reset_template = self._config.get(CONF_LAST_RESET_VALUE_TEMPLATE) + if last_reset_template is not None: + last_reset_template.hass = self.hass async def _subscribe_topics(self): """(Re)Subscribe to topics.""" + topics = {} @callback @log_messages(self.hass, self.entity_id) @@ -140,16 +153,49 @@ class MqttSensor(MqttEntity, SensorEntity): self._state = payload self.async_write_ha_state() + topics["state_topic"] = { + "topic": self._config[CONF_STATE_TOPIC], + "msg_callback": message_received, + "qos": self._config[CONF_QOS], + } + + @callback + @log_messages(self.hass, self.entity_id) + def last_reset_message_received(msg): + """Handle new last_reset messages.""" + payload = msg.payload + + template = self._config.get(CONF_LAST_RESET_VALUE_TEMPLATE) + if template is not None: + variables = {"entity_id": self.entity_id} + payload = template.async_render_with_possible_json_value( + payload, + self._state, + variables=variables, + ) + if not payload: + _LOGGER.debug("Ignoring empty last_reset message from '%s'", msg.topic) + return + try: + last_reset = dt_util.parse_datetime(payload) + if last_reset is None: + raise ValueError + self._attr_last_reset = last_reset + except ValueError: + _LOGGER.warning( + "Invalid last_reset message '%s' from '%s'", msg.payload, msg.topic + ) + self.async_write_ha_state() + + if CONF_LAST_RESET_TOPIC in self._config: + topics["state_topic"] = { + "topic": self._config[CONF_LAST_RESET_TOPIC], + "msg_callback": last_reset_message_received, + "qos": self._config[CONF_QOS], + } + self._sub_state = await subscription.async_subscribe_topics( - self.hass, - self._sub_state, - { - "state_topic": { - "topic": self._config[CONF_STATE_TOPIC], - "msg_callback": message_received, - "qos": self._config[CONF_QOS], - } - }, + self.hass, self._sub_state, topics ) @callback diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index fe97bdfbfde..7d732849906 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -206,6 +206,104 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): assert state.state == "100" +async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock): + """Test the setting of the last_reset property via MQTT.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "last-reset-topic", "2020-01-02 08:11:00") + state = hass.states.get("sensor.test") + assert state.attributes.get("last_reset") == "2020-01-02T08:11:00" + + +@pytest.mark.parametrize("datestring", ["2020-21-02 08:11:00", "Hello there!"]) +async def test_setting_sensor_bad_last_reset_via_mqtt_message( + hass, caplog, datestring, mqtt_mock +): + """Test the setting of the last_reset property via MQTT.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "last-reset-topic", datestring) + state = hass.states.get("sensor.test") + assert state.attributes.get("last_reset") is None + assert "Invalid last_reset message" in caplog.text + + +async def test_setting_sensor_empty_last_reset_via_mqtt_message( + hass, caplog, mqtt_mock +): + """Test the setting of the last_reset property via MQTT.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "last-reset-topic", "") + state = hass.states.get("sensor.test") + assert state.attributes.get("last_reset") is None + assert "Ignoring empty last_reset message" in caplog.text + + +async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of the value via MQTT with JSON payload.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + "last_reset_value_template": "{{ value_json.last_reset }}", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message( + hass, "last-reset-topic", '{ "last_reset": "2020-01-02 08:11:00" }' + ) + state = hass.states.get("sensor.test") + assert state.attributes.get("last_reset") == "2020-01-02T08:11:00" + + async def test_force_update_disabled(hass, mqtt_mock): """Test force update option.""" assert await async_setup_component( From 1de0d20a76cbf753cf55407c51f25bb416254aa6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 26 May 2021 06:37:24 -0400 Subject: [PATCH 767/852] Bump zwave-js-server-python to 0.25.1 (#51097) * Bump zwave-js-server-python to 0.25.1 * update fixtures --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/conftest.py | 16 ++++++++++++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 9c632f72f47..c68206373ba 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.25.0"], + "requirements": ["zwave-js-server-python==0.25.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 867bb347fae..5559f89a102 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2442,4 +2442,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.25.0 +zwave-js-server-python==0.25.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 015e565af0a..ac9610f5285 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1324,4 +1324,4 @@ zigpy-znp==0.5.1 zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.25.0 +zwave-js-server-python==0.25.1 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index a2a712b59f1..12db8bafb77 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -153,6 +153,18 @@ def version_state_fixture(): } +@pytest.fixture(name="log_config_state") +def log_config_state_fixture(): + """Return log config state fixture data.""" + return { + "enabled": True, + "level": "info", + "logToFile": False, + "filename": "", + "forceConsole": False, + } + + @pytest.fixture(name="multisensor_6_state", scope="session") def multisensor_6_state_fixture(): """Load the multisensor 6 node state fixture data.""" @@ -371,7 +383,7 @@ def wallmote_central_scene_state_fixture(): @pytest.fixture(name="client") -def mock_client_fixture(controller_state, version_state): +def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" with patch( @@ -394,7 +406,7 @@ def mock_client_fixture(controller_state, version_state): client.connect = AsyncMock(side_effect=connect) client.listen = AsyncMock(side_effect=listen) client.disconnect = AsyncMock(side_effect=disconnect) - client.driver = Driver(client, controller_state) + client.driver = Driver(client, controller_state, log_config_state) client.version = VersionInfo.from_message(version_state) client.ws_server_url = "ws://test:3000/zjs" From 5a5a14577808fcd1cee2b82dca3b4e9a6147d455 Mon Sep 17 00:00:00 2001 From: Massimiliano Cannarozzo Date: Wed, 26 May 2021 12:50:44 +0200 Subject: [PATCH 768/852] Make all MQTT cover payloads optional (#50579) * Remove unused constant * Make payload_close optional * Make payload_open optional * Compute supported features based on config --- homeassistant/components/mqtt/cover.py | 15 ++++++--- tests/components/mqtt/test_cover.py | 44 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index a8de06ff1ca..0a78a102a13 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -94,7 +94,6 @@ DEFAULT_TILT_MIN = 0 DEFAULT_TILT_OPEN_POSITION = 100 DEFAULT_TILT_OPTIMISTIC = False -OPEN_CLOSE_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE TILT_FEATURES = ( SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT @@ -151,8 +150,12 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string, - vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string, + vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): vol.Any( + cv.string, None + ), + vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): vol.Any( + cv.string, None + ), vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): vol.Any( cv.string, None ), @@ -474,8 +477,10 @@ class MqttCover(MqttEntity, CoverEntity): """Flag supported features.""" supported_features = 0 if self._config.get(CONF_COMMAND_TOPIC) is not None: - supported_features = OPEN_CLOSE_FEATURES - + if self._config.get(CONF_PAYLOAD_OPEN) is not None: + supported_features |= SUPPORT_OPEN + if self._config.get(CONF_PAYLOAD_CLOSE) is not None: + supported_features |= SUPPORT_CLOSE if self._config.get(CONF_PAYLOAD_STOP) is not None: supported_features |= SUPPORT_STOP diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index c9dd30038ab..c5a8552dc20 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1014,6 +1014,50 @@ async def test_no_command_topic(hass, mqtt_mock): assert hass.states.get("cover.test").attributes["supported_features"] == 240 +async def test_no_payload_close(hass, mqtt_mock): + """Test with no close payload.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": None, + "payload_stop": "STOP", + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("cover.test").attributes["supported_features"] == 9 + + +async def test_no_payload_open(hass, mqtt_mock): + """Test with no open payload.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": None, + "payload_close": "CLOSE", + "payload_stop": "STOP", + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("cover.test").attributes["supported_features"] == 10 + + async def test_no_payload_stop(hass, mqtt_mock): """Test with no stop payload.""" assert await async_setup_component( From bf13af34b4546e798d2f18f3b7d044bb9d5e500f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 14:08:09 +0200 Subject: [PATCH 769/852] Use entity class vars in WLED (#50975) --- homeassistant/components/wled/__init__.py | 36 ---- homeassistant/components/wled/light.py | 57 +++--- homeassistant/components/wled/sensor.py | 201 ++++++++-------------- homeassistant/components/wled/switch.py | 80 +++------ tests/components/wled/test_sensor.py | 6 +- 5 files changed, 117 insertions(+), 263 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 717bb87e057..b44df82f889 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -131,42 +131,6 @@ class WLEDEntity(CoordinatorEntity): coordinator: WLEDDataUpdateCoordinator - def __init__( - self, - *, - entry_id: str, - coordinator: WLEDDataUpdateCoordinator, - name: str, - icon: str, - enabled_default: bool = True, - ) -> None: - """Initialize the WLED entity.""" - super().__init__(coordinator) - self._enabled_default = enabled_default - self._entry_id = entry_id - self._icon = icon - self._name = name - self._unsub_dispatcher = None - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def icon(self) -> str: - """Return the mdi icon of the entity.""" - return self._icon - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default - - -class WLEDDeviceEntity(WLEDEntity): - """Defines a WLED device entity.""" - @property def device_info(self) -> DeviceInfo: """Return device information about this WLED device.""" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 6c3b11a65e2..20960ad0bb7 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -30,7 +30,7 @@ from homeassistant.helpers.entity_registry import ( ) import homeassistant.util.color as color_util -from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity, wled_exception_handler +from . import WLEDDataUpdateCoordinator, WLEDEntity, wled_exception_handler from .const import ( ATTR_COLOR_PRIMARY, ATTR_INTENSITY, @@ -93,27 +93,17 @@ async def async_setup_entry( update_segments() -class WLEDMasterLight(LightEntity, WLEDDeviceEntity): +class WLEDMasterLight(WLEDEntity, LightEntity): """Defines a WLED master light.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + _attr_icon = "mdi:led-strip-variant" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED master light.""" - super().__init__( - entry_id=entry_id, - coordinator=coordinator, - name=f"{coordinator.data.info.name} Master", - icon="mdi:led-strip-variant", - ) - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return f"{self.coordinator.data.info.mac_address}" - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Master" + self._attr_unique_id = coordinator.data.info.mac_address @property def brightness(self) -> int | None: @@ -172,33 +162,26 @@ class WLEDMasterLight(LightEntity, WLEDDeviceEntity): await self.coordinator.wled.preset(**data) -class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): +class WLEDSegmentLight(WLEDEntity, LightEntity): """Defines a WLED light based on a segment.""" - def __init__( - self, entry_id: str, coordinator: WLEDDataUpdateCoordinator, segment: int - ) -> None: + _attr_icon = "mdi:led-strip-variant" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: """Initialize WLED segment light.""" + super().__init__(coordinator=coordinator) self._rgbw = coordinator.data.info.leds.rgbw self._segment = segment # If this is the one and only segment, use a simpler name - name = f"{coordinator.data.info.name} Segment {self._segment}" + self._attr_name = f"{coordinator.data.info.name} Segment {segment}" if len(coordinator.data.state.segments) == 1: - name = coordinator.data.info.name + self._attr_name = coordinator.data.info.name - super().__init__( - entry_id=entry_id, - coordinator=coordinator, - name=name, - icon="mdi:led-strip-variant", + self._attr_unique_id = ( + f"{self.coordinator.data.info.mac_address}_{self._segment}" ) - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return f"{self.coordinator.data.info.mac_address}_{self._segment}" - @property def available(self) -> bool: """Return True if entity is available.""" @@ -436,12 +419,12 @@ def async_update_segments( # Process new segments, add them to Home Assistant new_entities = [] for segment_id in segment_ids - current_ids: - current[segment_id] = WLEDSegmentLight(entry.entry_id, coordinator, segment_id) + current[segment_id] = WLEDSegmentLight(coordinator, segment_id) new_entities.append(current[segment_id]) # More than 1 segment now? Add master controls if len(current_ids) < 2 and len(segment_ids) > 1: - current[-1] = WLEDMasterLight(entry.entry_id, coordinator) + current[-1] = WLEDMasterLight(coordinator) new_entities.append(current[-1]) if new_entities: diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 4c104e1c936..73c012f25c7 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity +from . import WLEDDataUpdateCoordinator, WLEDEntity from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN @@ -30,68 +30,30 @@ async def async_setup_entry( coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors = [ - WLEDEstimatedCurrentSensor(entry.entry_id, coordinator), - WLEDUptimeSensor(entry.entry_id, coordinator), - WLEDFreeHeapSensor(entry.entry_id, coordinator), - WLEDWifiBSSIDSensor(entry.entry_id, coordinator), - WLEDWifiChannelSensor(entry.entry_id, coordinator), - WLEDWifiRSSISensor(entry.entry_id, coordinator), - WLEDWifiSignalSensor(entry.entry_id, coordinator), + WLEDEstimatedCurrentSensor(coordinator), + WLEDUptimeSensor(coordinator), + WLEDFreeHeapSensor(coordinator), + WLEDWifiBSSIDSensor(coordinator), + WLEDWifiChannelSensor(coordinator), + WLEDWifiRSSISensor(coordinator), + WLEDWifiSignalSensor(coordinator), ] async_add_entities(sensors, True) -class WLEDSensor(WLEDDeviceEntity, SensorEntity): - """Defines a WLED sensor.""" - - def __init__( - self, - *, - coordinator: WLEDDataUpdateCoordinator, - enabled_default: bool = True, - entry_id: str, - icon: str, - key: str, - name: str, - unit_of_measurement: str | None = None, - ) -> None: - """Initialize WLED sensor.""" - self._unit_of_measurement = unit_of_measurement - self._key = key - - super().__init__( - entry_id=entry_id, - coordinator=coordinator, - name=name, - icon=icon, - enabled_default=enabled_default, - ) - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return f"{self.coordinator.data.info.mac_address}_{self._key}" - - @property - def unit_of_measurement(self) -> str | None: - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - - -class WLEDEstimatedCurrentSensor(WLEDSensor): +class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity): """Defines a WLED estimated current sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:power" + _attr_unit_of_measurement = CURRENT_MA + _attr_device_class = DEVICE_CLASS_CURRENT + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED estimated current sensor.""" - super().__init__( - coordinator=coordinator, - entry_id=entry_id, - icon="mdi:power", - key="estimated_current", - name=f"{coordinator.data.info.name} Estimated Current", - unit_of_measurement=CURRENT_MA, - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Estimated Current" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_estimated_current" @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -106,25 +68,18 @@ class WLEDEstimatedCurrentSensor(WLEDSensor): """Return the state of the sensor.""" return self.coordinator.data.info.leds.power - @property - def device_class(self) -> str | None: - """Return the class of this sensor.""" - return DEVICE_CLASS_CURRENT - -class WLEDUptimeSensor(WLEDSensor): +class WLEDUptimeSensor(WLEDEntity, SensorEntity): """Defines a WLED uptime sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED uptime sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:clock-outline", - key="uptime", - name=f"{coordinator.data.info.name} Uptime", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Uptime" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_uptime" @property def state(self) -> str: @@ -132,26 +87,19 @@ class WLEDUptimeSensor(WLEDSensor): uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) return uptime.replace(microsecond=0).isoformat() - @property - def device_class(self) -> str | None: - """Return the class of this sensor.""" - return DEVICE_CLASS_TIMESTAMP - -class WLEDFreeHeapSensor(WLEDSensor): +class WLEDFreeHeapSensor(WLEDEntity, SensorEntity): """Defines a WLED free heap sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:memory" + _attr_entity_registry_enabled_default = False + _attr_unit_of_measurement = DATA_BYTES + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED free heap sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:memory", - key="free_heap", - name=f"{coordinator.data.info.name} Free Memory", - unit_of_measurement=DATA_BYTES, - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Free Memory" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_free_heap" @property def state(self) -> int: @@ -159,20 +107,18 @@ class WLEDFreeHeapSensor(WLEDSensor): return self.coordinator.data.info.free_heap -class WLEDWifiSignalSensor(WLEDSensor): +class WLEDWifiSignalSensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi signal sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:wifi" + _attr_unit_of_measurement = PERCENTAGE + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi signal sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:wifi", - key="wifi_signal", - name=f"{coordinator.data.info.name} Wi-Fi Signal", - unit_of_measurement=PERCENTAGE, - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Wi-Fi Signal" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_signal" @property def state(self) -> int: @@ -180,45 +126,36 @@ class WLEDWifiSignalSensor(WLEDSensor): return self.coordinator.data.info.wifi.signal -class WLEDWifiRSSISensor(WLEDSensor): +class WLEDWifiRSSISensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi RSSI sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH + _attr_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi RSSI sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:wifi", - key="wifi_rssi", - name=f"{coordinator.data.info.name} Wi-Fi RSSI", - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Wi-Fi RSSI" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_rssi" @property def state(self) -> int: """Return the state of the sensor.""" return self.coordinator.data.info.wifi.rssi - @property - def device_class(self) -> str | None: - """Return the class of this sensor.""" - return DEVICE_CLASS_SIGNAL_STRENGTH - -class WLEDWifiChannelSensor(WLEDSensor): +class WLEDWifiChannelSensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi Channel sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:wifi" + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi Channel sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:wifi", - key="wifi_channel", - name=f"{coordinator.data.info.name} Wi-Fi Channel", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Wi-Fi Channel" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_channel" @property def state(self) -> int: @@ -226,19 +163,17 @@ class WLEDWifiChannelSensor(WLEDSensor): return self.coordinator.data.info.wifi.channel -class WLEDWifiBSSIDSensor(WLEDSensor): +class WLEDWifiBSSIDSensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi BSSID sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:wifi" + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi BSSID sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:wifi", - key="wifi_bssid", - name=f"{coordinator.data.info.name} Wi-Fi BSSID", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Wi-Fi BSSID" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_bssid" @property def state(self) -> str: diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index ebd3f7826e6..2d1801a0c5e 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity, wled_exception_handler +from . import WLEDDataUpdateCoordinator, WLEDEntity, wled_exception_handler from .const import ( ATTR_DURATION, ATTR_FADE, @@ -29,49 +29,23 @@ async def async_setup_entry( coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] switches = [ - WLEDNightlightSwitch(entry.entry_id, coordinator), - WLEDSyncSendSwitch(entry.entry_id, coordinator), - WLEDSyncReceiveSwitch(entry.entry_id, coordinator), + WLEDNightlightSwitch(coordinator), + WLEDSyncSendSwitch(coordinator), + WLEDSyncReceiveSwitch(coordinator), ] async_add_entities(switches, True) -class WLEDSwitch(WLEDDeviceEntity, SwitchEntity): - """Defines a WLED switch.""" - - def __init__( - self, - *, - entry_id: str, - coordinator: WLEDDataUpdateCoordinator, - name: str, - icon: str, - key: str, - ) -> None: - """Initialize WLED switch.""" - self._key = key - super().__init__( - entry_id=entry_id, coordinator=coordinator, name=name, icon=icon - ) - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return f"{self.coordinator.data.info.mac_address}_{self._key}" - - -class WLEDNightlightSwitch(WLEDSwitch): +class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): """Defines a WLED nightlight switch.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:weather-night" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED nightlight switch.""" - super().__init__( - coordinator=coordinator, - entry_id=entry_id, - icon="mdi:weather-night", - key="nightlight", - name=f"{coordinator.data.info.name} Nightlight", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Nightlight" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_nightlight" @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -98,18 +72,16 @@ class WLEDNightlightSwitch(WLEDSwitch): await self.coordinator.wled.nightlight(on=True) -class WLEDSyncSendSwitch(WLEDSwitch): +class WLEDSyncSendSwitch(WLEDEntity, SwitchEntity): """Defines a WLED sync send switch.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:upload-network-outline" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync send switch.""" - super().__init__( - coordinator=coordinator, - entry_id=entry_id, - icon="mdi:upload-network-outline", - key="sync_send", - name=f"{coordinator.data.info.name} Sync Send", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Sync Send" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_sync_send" @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -132,18 +104,16 @@ class WLEDSyncSendSwitch(WLEDSwitch): await self.coordinator.wled.sync(send=True) -class WLEDSyncReceiveSwitch(WLEDSwitch): +class WLEDSyncReceiveSwitch(WLEDEntity, SwitchEntity): """Defines a WLED sync receive switch.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:download-network-outline" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync receive switch.""" - super().__init__( - coordinator=coordinator, - entry_id=entry_id, - icon="mdi:download-network-outline", - key="sync_receive", - name=f"{coordinator.data.info.name} Sync Receive", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Sync Receive" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_sync_receive" @property def extra_state_attributes(self) -> dict[str, Any] | None: diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index f20e2f0419a..fcd36dd70a9 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -6,6 +6,8 @@ import pytest from homeassistant.components.sensor import ( DEVICE_CLASS_CURRENT, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TIMESTAMP, DOMAIN as SENSOR_DOMAIN, ) from homeassistant.components.wled.const import ( @@ -108,7 +110,7 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_uptime") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.state == "2019-11-11T09:10:00+00:00" @@ -138,7 +140,7 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_wifi_rssi") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:wifi" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT From a8a13da793bd5e3c39c8500ebb76f9fcbb224ec5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 16:29:52 +0200 Subject: [PATCH 770/852] Fix discovery without uid aborts on completing user flow (#51105) * Fix discovery without uid aborts on completing user flow * Fix comment --- homeassistant/config_entries.py | 23 ++++++++++------ tests/test_config_entries.py | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7719d8edcc9..b3589d03b92 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -615,16 +615,21 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): # Check if config entry exists with unique ID. Unload it. existing_entry = None - if flow.unique_id is not None: - # Abort all flows in progress with same unique ID. - for progress_flow in self.async_progress(): - if ( - progress_flow["handler"] == flow.handler - and progress_flow["flow_id"] != flow.flow_id - and progress_flow["context"].get("unique_id") == flow.unique_id - ): - self.async_abort(progress_flow["flow_id"]) + # Abort all flows in progress with same unique ID + # or the default discovery ID + for progress_flow in self.async_progress(): + progress_unique_id = progress_flow["context"].get("unique_id") + if ( + progress_flow["handler"] == flow.handler + and progress_flow["flow_id"] != flow.flow_id + and ( + (flow.unique_id and progress_unique_id == flow.unique_id) + or progress_unique_id == DEFAULT_DISCOVERY_UNIQUE_ID + ) + ): + self.async_abort(progress_flow["flow_id"]) + if flow.unique_id is not None: # Reset unique ID when the default discovery ID has been used if flow.unique_id == DEFAULT_DISCOVERY_UNIQUE_ID: await flow.async_set_unique_id(None) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 042bec418ac..e9e864c4491 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2495,6 +2495,55 @@ async def test_default_discovery_abort_on_new_unique_flow(hass, manager): assert flows[0]["context"]["unique_id"] == "mock-unique-id" +async def test_default_discovery_abort_on_user_flow_complete(hass, manager): + """Test that a flow using default discovery is aborted when a second flow completes.""" + mock_integration(hass, MockModule("comp")) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + if user_input is None: + return self.async_show_form(step_id="user") + return self.async_create_entry(title="title", data={"token": "supersecret"}) + + async def async_step_discovery(self, discovery_info=None): + """Test discovery step.""" + await self._async_handle_discovery_without_unique_id() + return self.async_show_form(step_id="mock") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + # First discovery with default, no unique ID + flow1 = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} + ) + assert flow1["type"] == data_entry_flow.RESULT_TYPE_FORM + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + # User sets up a manual flow + flow2 = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + assert flow2["type"] == data_entry_flow.RESULT_TYPE_FORM + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 2 + + # Complete the manual flow + result = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # Ensure the first flow is gone now + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 + + async def test_updating_entry_with_and_without_changes(manager): """Test that we can update an entry data.""" entry = MockConfigEntry( From a36935dceeeb6df2c8f06984d64fe4f42a95a84e Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Wed, 26 May 2021 07:36:36 -0700 Subject: [PATCH 771/852] Add services for Mazda integration (#51016) --- homeassistant/components/mazda/__init__.py | 106 ++++++++++++++- homeassistant/components/mazda/const.py | 11 ++ homeassistant/components/mazda/manifest.json | 2 +- homeassistant/components/mazda/services.yaml | 106 +++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/mazda/__init__.py | 7 + tests/components/mazda/test_init.py | 132 ++++++++++++++++++- 8 files changed, 361 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/mazda/services.yaml diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index c34dfa10682..2c480aaf606 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -11,12 +11,18 @@ from pymazda import ( MazdaException, MazdaTokenExpiredException, ) +import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) +from homeassistant.helpers import aiohttp_client, device_registry +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -24,7 +30,7 @@ from homeassistant.helpers.update_coordinator import ( ) from homeassistant.util.async_ import gather_with_concurrency -from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN +from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN, SERVICES _LOGGER = logging.getLogger(__name__) @@ -59,6 +65,77 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error("Error occurred during Mazda login request: %s", ex) raise ConfigEntryNotReady from ex + async def async_handle_service_call(service_call=None): + """Handle a service call.""" + # Get device entry from device registry + dev_reg = device_registry.async_get(hass) + device_id = service_call.data.get("device_id") + device_entry = dev_reg.async_get(device_id) + + # Get vehicle VIN from device identifiers + mazda_identifiers = [ + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN + ] + vin_identifier = next(iter(mazda_identifiers)) + vin = vin_identifier[1] + + # Get vehicle ID and API client from hass.data + vehicle_id = 0 + api_client = None + for entry_data in hass.data[DOMAIN].values(): + for vehicle in entry_data[DATA_VEHICLES]: + if vehicle["vin"] == vin: + vehicle_id = vehicle["id"] + api_client = entry_data[DATA_CLIENT] + + if vehicle_id == 0 or api_client is None: + raise HomeAssistantError("Vehicle ID not found") + + api_method = getattr(api_client, service_call.service) + try: + if service_call.service == "send_poi": + latitude = service_call.data.get("latitude") + longitude = service_call.data.get("longitude") + poi_name = service_call.data.get("poi_name") + await api_method(vehicle_id, latitude, longitude, poi_name) + else: + await api_method(vehicle_id) + except Exception as ex: + _LOGGER.exception("Error occurred during Mazda service call: %s", ex) + raise HomeAssistantError(ex) from ex + + def validate_mazda_device_id(device_id): + """Check that a device ID exists in the registry and has at least one 'mazda' identifier.""" + dev_reg = device_registry.async_get(hass) + device_entry = dev_reg.async_get(device_id) + + if device_entry is None: + raise vol.Invalid("Invalid device ID") + + mazda_identifiers = [ + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN + ] + if len(mazda_identifiers) < 1: + raise vol.Invalid("Device ID is not a Mazda vehicle") + + return device_id + + service_schema = vol.Schema( + {vol.Required("device_id"): vol.All(cv.string, validate_mazda_device_id)} + ) + + service_schema_send_poi = service_schema.extend( + { + vol.Required("latitude"): cv.latitude, + vol.Required("longitude"): cv.longitude, + vol.Required("poi_name"): cv.string, + } + ) + async def async_update_data(): """Fetch data from Mazda API.""" try: @@ -73,6 +150,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): for vehicle, status in zip(vehicles, statuses): vehicle["status"] = status + hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles + return vehicles except MazdaAuthenticationException as ex: raise ConfigEntryAuthFailed("Not authenticated with Mazda API") from ex @@ -94,6 +173,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = { DATA_CLIENT: mazda_client, DATA_COORDINATOR: coordinator, + DATA_VEHICLES: [], } # Fetch initial data so we have data when entities subscribe @@ -102,12 +182,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Setup components hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # Register services + for service in SERVICES: + if service == "send_poi": + hass.services.async_register( + DOMAIN, + service, + async_handle_service_call, + schema=service_schema_send_poi, + ) + else: + hass.services.async_register( + DOMAIN, service, async_handle_service_call, schema=service_schema + ) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + # Only remove services if it is the last config entry + if len(hass.data[DOMAIN]) == 1: + for service in SERVICES: + hass.services.async_remove(DOMAIN, service) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/mazda/const.py b/homeassistant/components/mazda/const.py index c75f6bf3b77..5baeef3102d 100644 --- a/homeassistant/components/mazda/const.py +++ b/homeassistant/components/mazda/const.py @@ -4,5 +4,16 @@ DOMAIN = "mazda" DATA_CLIENT = "mazda_client" DATA_COORDINATOR = "coordinator" +DATA_VEHICLES = "vehicles" MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"} + +SERVICES = [ + "send_poi", + "start_charging", + "start_engine", + "stop_charging", + "stop_engine", + "turn_off_hazard_lights", + "turn_on_hazard_lights", +] diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 4ca4384e952..dd169159bc8 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.1.5"], + "requirements": ["pymazda==0.1.6"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/mazda/services.yaml b/homeassistant/components/mazda/services.yaml new file mode 100644 index 00000000000..80d8c2f64f6 --- /dev/null +++ b/homeassistant/components/mazda/services.yaml @@ -0,0 +1,106 @@ +start_engine: + name: Start engine + description: Start the vehicle engine. + fields: + device_id: + name: Vehicle + description: The vehicle to start + required: true + selector: + device: + integration: mazda +stop_engine: + name: Stop engine + description: Stop the vehicle engine. + fields: + device_id: + name: Vehicle + description: The vehicle to stop + required: true + selector: + device: + integration: mazda +turn_on_hazard_lights: + name: Turn on hazard lights + description: Turn on the vehicle hazard lights. The lights will flash briefly and then turn off. + fields: + device_id: + name: Vehicle + description: The vehicle to turn hazard lights on + required: true + selector: + device: + integration: mazda +turn_off_hazard_lights: + name: Turn off hazard lights + description: Turn off the vehicle hazard lights if they have been manually turned on from inside the vehicle. + fields: + device_id: + name: Vehicle + description: The vehicle to turn hazard lights off + required: true + selector: + device: + integration: mazda +send_poi: + name: Send POI + description: Send a GPS location to the vehicle's navigation system as a POI (Point of Interest). Requires a navigation SD card installed in the vehicle. + fields: + device_id: + name: Vehicle + description: The vehicle to send the GPS location to + required: true + selector: + device: + integration: mazda + latitude: + name: Latitude + description: The latitude of the location to send + example: 12.34567 + required: true + selector: + number: + min: -90 + max: 90 + unit_of_measurement: ° + mode: box + longitude: + name: Longitude + description: The longitude of the location to send + example: -34.56789 + required: true + selector: + number: + min: -180 + max: 180 + unit_of_measurement: ° + mode: box + poi_name: + name: POI name + description: A friendly name for the location + example: Work + required: true + selector: + text: +start_charging: + name: Start charging + description: Start charging the vehicle. For electric vehicles only. + fields: + device_id: + name: Vehicle + description: The vehicle to start charging + required: true + selector: + device: + integration: mazda +stop_charging: + name: Stop charging + description: Stop charging the vehicle. For electric vehicles only. + fields: + device_id: + name: Vehicle + description: The vehicle to stop charging + required: true + selector: + device: + integration: mazda diff --git a/requirements_all.txt b/requirements_all.txt index 5559f89a102..b02a6191c9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1548,7 +1548,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.1.5 +pymazda==0.1.6 # homeassistant.components.mediaroom pymediaroom==0.6.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac9610f5285..0fa3d2ced9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -862,7 +862,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.1.5 +pymazda==0.1.6 # homeassistant.components.melcloud pymelcloud==2.5.2 diff --git a/tests/components/mazda/__init__.py b/tests/components/mazda/__init__.py index 9676b2b5765..7a81a9224d7 100644 --- a/tests/components/mazda/__init__.py +++ b/tests/components/mazda/__init__.py @@ -44,6 +44,13 @@ async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfig client_mock.get_vehicle_status = AsyncMock(return_value=get_vehicle_status_fixture) client_mock.lock_doors = AsyncMock() client_mock.unlock_doors = AsyncMock() + client_mock.send_poi = AsyncMock() + client_mock.start_charging = AsyncMock() + client_mock.start_engine = AsyncMock() + client_mock.stop_charging = AsyncMock() + client_mock.stop_engine = AsyncMock() + client_mock.turn_off_hazard_lights = AsyncMock() + client_mock.turn_on_hazard_lights = AsyncMock() with patch( "homeassistant.components.mazda.config_flow.MazdaAPI", diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index c9370459f1c..0280e8f34fa 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -4,8 +4,10 @@ import json from unittest.mock import patch from pymazda import MazdaAuthenticationException, MazdaException +import pytest +import voluptuous as vol -from homeassistant.components.mazda.const import DOMAIN +from homeassistant.components.mazda.const import DOMAIN, SERVICES from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_EMAIL, @@ -14,6 +16,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.util import dt as dt_util @@ -181,3 +184,130 @@ async def test_device_no_nickname(hass): assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD" assert reg_device.manufacturer == "Mazda" assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD" + + +async def test_services(hass): + """Test service calls.""" + client_mock = await init_integration(hass) + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "JM000000000000000")}, + ) + device_id = reg_device.id + + for service in SERVICES: + service_data = {"device_id": device_id} + if service == "send_poi": + service_data["latitude"] = 1.2345 + service_data["longitude"] = 2.3456 + service_data["poi_name"] = "Work" + + await hass.services.async_call(DOMAIN, service, service_data, blocking=True) + await hass.async_block_till_done() + + api_method = getattr(client_mock, service) + if service == "send_poi": + api_method.assert_called_once_with(12345, 1.2345, 2.3456, "Work") + else: + api_method.assert_called_once_with(12345) + + +async def test_service_invalid_device_id(hass): + """Test service call when the specified device ID is invalid.""" + await init_integration(hass) + + with pytest.raises(vol.error.MultipleInvalid) as err: + await hass.services.async_call( + DOMAIN, "start_engine", {"device_id": "invalid"}, blocking=True + ) + await hass.async_block_till_done() + + assert "Invalid device ID" in str(err.value) + + +async def test_service_device_id_not_mazda_vehicle(hass): + """Test service call when the specified device ID is not the device ID of a Mazda vehicle.""" + await init_integration(hass) + + device_registry = dr.async_get(hass) + # Create another device and pass its device ID. + # Service should fail because device is from wrong domain. + other_device = device_registry.async_get_or_create( + config_entry_id="test_config_entry_id", + identifiers={("OTHER_INTEGRATION", "ID_FROM_OTHER_INTEGRATION")}, + ) + + with pytest.raises(vol.error.MultipleInvalid) as err: + await hass.services.async_call( + DOMAIN, "start_engine", {"device_id": other_device.id}, blocking=True + ) + await hass.async_block_till_done() + + assert "Device ID is not a Mazda vehicle" in str(err.value) + + +async def test_service_vehicle_id_not_found(hass): + """Test service call when the vehicle ID is not found.""" + await init_integration(hass) + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "JM000000000000000")}, + ) + device_id = reg_device.id + + entries = hass.config_entries.async_entries(DOMAIN) + entry_id = entries[0].entry_id + + # Remove vehicle info from hass.data so that vehicle ID will not be found + hass.data[DOMAIN][entry_id]["vehicles"] = [] + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + DOMAIN, "start_engine", {"device_id": device_id}, blocking=True + ) + await hass.async_block_till_done() + + assert str(err.value) == "Vehicle ID not found" + + +async def test_service_mazda_api_error(hass): + """Test the Mazda API raising an error when a service is called.""" + get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json")) + get_vehicle_status_fixture = json.loads( + load_fixture("mazda/get_vehicle_status.json") + ) + + with patch( + "homeassistant.components.mazda.MazdaAPI.validate_credentials", + return_value=True, + ), patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicles", + return_value=get_vehicles_fixture, + ), patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicle_status", + return_value=get_vehicle_status_fixture, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "JM000000000000000")}, + ) + device_id = reg_device.id + + with patch( + "homeassistant.components.mazda.MazdaAPI.start_engine", + side_effect=MazdaException("Test error"), + ), pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + DOMAIN, "start_engine", {"device_id": device_id}, blocking=True + ) + await hass.async_block_till_done() + + assert str(err.value) == "Test error" From 8edf7f040740636419a7c92dab634a46865d7796 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 26 May 2021 11:32:26 -0400 Subject: [PATCH 772/852] Don't enforce uniqueness requirements for Waze and Google Travel Time (#50254) Co-authored-by: Paulus Schoutsen --- .../components/google_travel_time/__init__.py | 19 +- .../google_travel_time/config_flow.py | 63 +++- .../components/google_travel_time/sensor.py | 2 +- .../components/waze_travel_time/__init__.py | 15 + .../waze_travel_time/config_flow.py | 69 +++- .../components/waze_travel_time/sensor.py | 2 +- .../components/google_travel_time/conftest.py | 10 + .../google_travel_time/test_config_flow.py | 323 +++++++++++++++++- .../google_travel_time/test_init.py | 21 ++ tests/components/waze_travel_time/conftest.py | 10 + .../waze_travel_time/test_config_flow.py | 124 ++++++- .../components/waze_travel_time/test_init.py | 21 ++ 12 files changed, 651 insertions(+), 28 deletions(-) create mode 100644 tests/components/google_travel_time/test_init.py create mode 100644 tests/components/waze_travel_time/test_init.py diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 5d4b3d1b74a..bad6edd119e 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -1,14 +1,29 @@ """The google_travel_time component.""" +import logging from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry, + async_get, +) PLATFORMS = ["sensor"] +_LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up Google Maps Travel Time from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + if config_entry.unique_id is not None: + hass.config_entries.async_update_entry(config_entry, unique_id=None) + + ent_reg = async_get(hass) + for entity in async_entries_for_config_entry(ent_reg, config_entry.entry_id): + ent_reg.async_update_entity( + entity.entity_id, new_unique_id=config_entry.entry_id + ) + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py index 603a0ec12f0..931e1f355aa 100644 --- a/homeassistant/components/google_travel_time/config_flow.py +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -1,13 +1,15 @@ """Config flow for Google Maps Travel Time integration.""" +from __future__ import annotations + import logging +from typing import Any import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify from .const import ( ALL_LANGUAGES, @@ -18,12 +20,14 @@ from .const import ( CONF_DEPARTURE_TIME, CONF_DESTINATION, CONF_LANGUAGE, + CONF_OPTIONS, CONF_ORIGIN, CONF_TIME, CONF_TIME_TYPE, CONF_TRAFFIC_MODEL, CONF_TRANSIT_MODE, CONF_TRANSIT_ROUTING_PREFERENCE, + CONF_TRAVEL_MODE, CONF_UNITS, DEFAULT_NAME, DEPARTURE_TIME, @@ -40,6 +44,47 @@ from .helpers import is_valid_config_entry _LOGGER = logging.getLogger(__name__) +def is_dupe_import( + hass: HomeAssistant, entry: config_entries.ConfigEntry, user_input: dict[str, Any] +) -> bool: + """Return whether imported config already exists.""" + # Check the main data keys + if any( + entry.data[key] != user_input[key] + for key in (CONF_API_KEY, CONF_DESTINATION, CONF_ORIGIN) + ): + return False + + options = user_input.get(CONF_OPTIONS, {}) + + # We have to check for units differently because there is a default + units = options.get(CONF_UNITS) or hass.config.units.name + if entry.options[CONF_UNITS] != units: + return False + + # We have to check for travel mode differently because of the default and because + # it can be provided in two different ways. We have to give mode preference over + # travel mode because that's the way that entry setup works. + mode = options.get(CONF_MODE) or user_input.get(CONF_TRAVEL_MODE) or "driving" + if entry.options[CONF_MODE] != mode: + return False + + # We have to check for options that don't have defaults + for key in ( + CONF_LANGUAGE, + CONF_AVOID, + CONF_ARRIVAL_TIME, + CONF_DEPARTURE_TIME, + CONF_TRAFFIC_MODEL, + CONF_TRANSIT_MODE, + CONF_TRANSIT_ROUTING_PREFERENCE, + ): + if options.get(key) != entry.options.get(key): + return False + + return True + + class GoogleOptionsFlow(config_entries.OptionsFlow): """Handle an options flow for Google Travel Time.""" @@ -126,12 +171,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} user_input = user_input or {} if user_input: - await self.async_set_unique_id( - slugify( - f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}" - ) - ) - self._abort_if_unique_id_configured() + # We need to prevent duplicate imports + if self.source == config_entries.SOURCE_IMPORT and any( + is_dupe_import(self.hass, entry, user_input) + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.source == config_entries.SOURCE_IMPORT + ): + return self.async_abort(reason="already_configured") + if ( self.source == config_entries.SOURCE_IMPORT or await self.hass.async_add_executor_job( diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index b6019ba2991..6dbe6aa698b 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -172,7 +172,7 @@ class GoogleTravelTimeSensor(SensorEntity): self._unit_of_measurement = TIME_MINUTES self._matrix = None self._api_key = api_key - self._unique_id = config_entry.unique_id + self._unique_id = config_entry.entry_id self._client = client # Check if location is a trackable entity diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 5800cfe94ab..57382689f61 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1,13 +1,28 @@ """The waze_travel_time component.""" +import logging from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry, + async_get, +) PLATFORMS = ["sensor"] +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Load the saved entities.""" + if config_entry.unique_id is not None: + hass.config_entries.async_update_entry(config_entry, unique_id=None) + + ent_reg = async_get(hass) + for entity in async_entries_for_config_entry(ent_reg, config_entry.entry_id): + ent_reg.async_update_entity( + entity.entity_id, new_unique_id=config_entry.entry_id + ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index cdd83a35aa1..54097ad37bd 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -1,13 +1,15 @@ """Config flow for Waze Travel Time integration.""" +from __future__ import annotations + import logging +from typing import Any import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_NAME, CONF_REGION -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify from .const import ( CONF_AVOID_FERRIES, @@ -20,7 +22,12 @@ from .const import ( CONF_REALTIME, CONF_UNITS, CONF_VEHICLE_TYPE, + DEFAULT_AVOID_FERRIES, + DEFAULT_AVOID_SUBSCRIPTION_ROADS, + DEFAULT_AVOID_TOLL_ROADS, DEFAULT_NAME, + DEFAULT_REALTIME, + DEFAULT_VEHICLE_TYPE, DOMAIN, REGIONS, UNITS, @@ -31,6 +38,50 @@ from .helpers import is_valid_config_entry _LOGGER = logging.getLogger(__name__) +def is_dupe_import( + hass: HomeAssistant, entry: config_entries.ConfigEntry, user_input: dict[str, Any] +) -> bool: + """Return whether imported config already exists.""" + entry_data = {**entry.data, **entry.options} + defaults = { + CONF_REALTIME: DEFAULT_REALTIME, + CONF_VEHICLE_TYPE: DEFAULT_VEHICLE_TYPE, + CONF_UNITS: hass.config.units.name, + CONF_AVOID_FERRIES: DEFAULT_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS, + } + + for key in ( + CONF_ORIGIN, + CONF_DESTINATION, + CONF_REGION, + CONF_INCL_FILTER, + CONF_EXCL_FILTER, + CONF_REALTIME, + CONF_VEHICLE_TYPE, + CONF_UNITS, + CONF_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS, + ): + # If the key is present the check is simple + if key in user_input and user_input[key] != entry_data[key]: + return False + + # If the key is not present, then we have to check if the key has a default and + # if the default is in the options. If it doesn't have a default, we have to check + # if the key is in the options + if key not in user_input: + if key in defaults and defaults[key] != entry_data[key]: + return False + + if key not in defaults and key in entry_data: + return False + + return True + + class WazeOptionsFlow(config_entries.OptionsFlow): """Handle an options flow for Waze Travel Time.""" @@ -108,12 +159,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input = user_input or {} if user_input: - await self.async_set_unique_id( - slugify( - f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}" - ) - ) - self._abort_if_unique_id_configured() + # We need to prevent duplicate imports + if self.source == config_entries.SOURCE_IMPORT and any( + is_dupe_import(self.hass, entry, user_input) + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.source == config_entries.SOURCE_IMPORT + ): + return self.async_abort(reason="already_configured") + if ( self.source == config_entries.SOURCE_IMPORT or await self.hass.async_add_executor_job( diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index bd8e41dc31c..4fb56700f59 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -158,7 +158,7 @@ async def async_setup_entry( config_entry, ) - sensor = WazeTravelTime(config_entry.unique_id, name, origin, destination, data) + sensor = WazeTravelTime(config_entry.entry_id, name, origin, destination, data) async_add_entities([sensor], False) diff --git a/tests/components/google_travel_time/conftest.py b/tests/components/google_travel_time/conftest.py index 18e16a79e27..87d922ac22c 100644 --- a/tests/components/google_travel_time/conftest.py +++ b/tests/components/google_travel_time/conftest.py @@ -37,6 +37,16 @@ def bypass_setup_fixture(): yield +@pytest.fixture(name="bypass_platform_setup") +def bypass_platform_setup_fixture(): + """Bypass platform setup.""" + with patch( + "homeassistant.components.google_travel_time.sensor.async_setup_entry", + return_value=True, + ): + yield + + @pytest.fixture(name="bypass_update") def bypass_update_fixture(): """Bypass sensor update.""" diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index a0767246087..6118aac7cfa 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.components.google_travel_time.const import ( CONF_TRAFFIC_MODEL, CONF_TRANSIT_MODE, CONF_TRANSIT_ROUTING_PREFERENCE, + CONF_TRAVEL_MODE, CONF_UNITS, DEFAULT_NAME, DEPARTURE_TIME, @@ -24,6 +25,7 @@ from homeassistant.const import ( CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, ) from tests.common import MockConfigEntry @@ -197,8 +199,8 @@ async def test_options_flow_departure_time(hass, validate_config_entry, bypass_u } -async def test_dupe_id(hass, validate_config_entry, bypass_setup): - """Test setting up the same entry twice fails.""" +async def test_dupe(hass, validate_config_entry, bypass_setup): + """Test setting up the same entry data twice is OK.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -233,8 +235,7 @@ async def test_dupe_id(hass, validate_config_entry, bypass_setup): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result2["reason"] == "already_configured" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY async def test_import_flow(hass, validate_config_entry, bypass_update): @@ -297,3 +298,317 @@ async def test_import_flow(hass, validate_config_entry, bypass_update): CONF_TRANSIT_MODE: "train", CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", } + + +async def test_dupe_import_no_options(hass, bypass_update): + """Test duplicate import with no options.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dupe_import_default_options(hass, bypass_update): + """Test duplicate import with default options.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def _setup_dupe_import(hass, bypass_update): + """Set up dupe import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + +async def test_dupe_import(hass, bypass_update): + """Test duplicate import.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dupe_import_false_check_data_keys(hass, bypass_update): + """Test false duplicate import check when data keys differ.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key2", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_no_units(hass, bypass_update): + """Test false duplicate import check when units aren't provided.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_units(hass, bypass_update): + """Test false duplicate import check when units are provided but different.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_METRIC, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_travel_mode(hass, bypass_update): + """Test false duplicate import check when travel mode differs.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_TRAVEL_MODE: "driving", + CONF_OPTIONS: { + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_mode(hass, bypass_update): + """Test false duplicate import check when mode diiffers.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_no_mode(hass, bypass_update): + """Test false duplicate import check when no mode is provided.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_options(hass, bypass_update): + """Test false duplicate import check when options differ.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/google_travel_time/test_init.py b/tests/components/google_travel_time/test_init.py new file mode 100644 index 00000000000..583cd4dc7ce --- /dev/null +++ b/tests/components/google_travel_time/test_init.py @@ -0,0 +1,21 @@ +"""Test Google Maps Travel Time initialization.""" +from homeassistant.components.google_travel_time.const import DOMAIN +from homeassistant.helpers.entity_registry import async_get + +from tests.common import MockConfigEntry + + +async def test_migration(hass, bypass_platform_setup): + """Test migration logic for unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, version=1, entry_id="test", unique_id="test" + ) + ent_reg = async_get(hass) + ent_entry = ent_reg.async_get_or_create( + "sensor", DOMAIN, unique_id="replaceable_unique_id", config_entry=config_entry + ) + entity_id = ent_entry.entity_id + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.unique_id is None + assert ent_reg.async_get(entity_id).unique_id == config_entry.entry_id diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py index dd5b343cc16..237b476aa25 100644 --- a/tests/components/waze_travel_time/conftest.py +++ b/tests/components/waze_travel_time/conftest.py @@ -35,6 +35,16 @@ def bypass_setup_fixture(): yield +@pytest.fixture(name="bypass_platform_setup") +def bypass_platform_setup_fixture(): + """Bypass platform setup.""" + with patch( + "homeassistant.components.waze_travel_time.sensor.async_setup_entry", + return_value=True, + ): + yield + + @pytest.fixture(name="mock_update") def mock_update_fixture(): """Mock an update to the sensor.""" diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index b6690be4d86..f0f8a0f3bde 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -145,8 +145,125 @@ async def test_import(hass, validate_config_entry, mock_update): } -async def test_dupe_id(hass, validate_config_entry, bypass_setup): - """Test setting up the same entry twice fails.""" +async def _setup_dupe_import(hass, mock_update): + """Set up dupe import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + +async def test_dupe_import(hass, mock_update): + """Test duplicate import.""" + await _setup_dupe_import(hass, mock_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dupe_import_false_check_different_options_value(hass, mock_update): + """Test false duplicate import check when options value differs.""" + await _setup_dupe_import(hass, mock_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "car", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_default_option(hass, mock_update): + """Test false duplicate import check when option with a default is missing.""" + await _setup_dupe_import(hass, mock_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_no_default_option(hass, mock_update): + """Test false duplicate import check option when option with no default is miissing.""" + await _setup_dupe_import(hass, mock_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_REALTIME: False, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe(hass, validate_config_entry, bypass_setup): + """Test setting up the same entry data twice is OK.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -182,8 +299,7 @@ async def test_dupe_id(hass, validate_config_entry, bypass_setup): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result2["reason"] == "already_configured" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY async def test_invalid_config_entry(hass, invalidate_config_entry): diff --git a/tests/components/waze_travel_time/test_init.py b/tests/components/waze_travel_time/test_init.py new file mode 100644 index 00000000000..bf8f6a95844 --- /dev/null +++ b/tests/components/waze_travel_time/test_init.py @@ -0,0 +1,21 @@ +"""Test Waze Travel Time initialization.""" +from homeassistant.components.waze_travel_time.const import DOMAIN +from homeassistant.helpers.entity_registry import async_get + +from tests.common import MockConfigEntry + + +async def test_migration(hass, bypass_platform_setup): + """Test migration logic for unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, version=1, entry_id="test", unique_id="test" + ) + ent_reg = async_get(hass) + ent_entry = ent_reg.async_get_or_create( + "sensor", DOMAIN, unique_id="replaceable_unique_id", config_entry=config_entry + ) + entity_id = ent_entry.entity_id + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.unique_id is None + assert ent_reg.async_get(entity_id).unique_id == config_entry.entry_id From 6d9b67ddb213e5ce3fd5d8ef530ec38447048820 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 26 May 2021 11:38:02 -0400 Subject: [PATCH 773/852] Add zwave_js heal node and network WS API commands (#51047) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/api.py | 154 ++++++++++++++- tests/components/zwave_js/test_api.py | 175 ++++++++++++++++++ tests/fixtures/zwave_js/controller_state.json | 3 +- 3 files changed, 326 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 2d0fee54a18..3e13de5ada9 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2,7 +2,7 @@ from __future__ import annotations import dataclasses -from functools import wraps +from functools import partial, wraps import json from typing import Callable @@ -142,18 +142,24 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_ping_node) websocket_api.async_register_command(hass, websocket_add_node) websocket_api.async_register_command(hass, websocket_stop_inclusion) + websocket_api.async_register_command(hass, websocket_stop_exclusion) websocket_api.async_register_command(hass, websocket_remove_node) websocket_api.async_register_command(hass, websocket_remove_failed_node) websocket_api.async_register_command(hass, websocket_replace_failed_node) - websocket_api.async_register_command(hass, websocket_stop_exclusion) + websocket_api.async_register_command(hass, websocket_begin_healing_network) + websocket_api.async_register_command( + hass, websocket_subscribe_heal_network_progress + ) + websocket_api.async_register_command(hass, websocket_stop_healing_network) websocket_api.async_register_command(hass, websocket_refresh_node_info) websocket_api.async_register_command(hass, websocket_refresh_node_values) websocket_api.async_register_command(hass, websocket_refresh_node_cc_values) + websocket_api.async_register_command(hass, websocket_heal_node) + websocket_api.async_register_command(hass, websocket_set_config_parameter) + websocket_api.async_register_command(hass, websocket_get_config_parameters) websocket_api.async_register_command(hass, websocket_subscribe_logs) websocket_api.async_register_command(hass, websocket_update_log_config) websocket_api.async_register_command(hass, websocket_get_log_config) - websocket_api.async_register_command(hass, websocket_get_config_parameters) - websocket_api.async_register_command(hass, websocket_set_config_parameter) websocket_api.async_register_command( hass, websocket_update_data_collection_preference ) @@ -180,6 +186,7 @@ async def websocket_network_status( client: Client, ) -> None: """Get the status of the Z-Wave JS network.""" + controller = client.driver.controller data = { "client": { "ws_server_url": client.ws_server_url, @@ -188,7 +195,24 @@ async def websocket_network_status( "server_version": client.version.server_version, }, "controller": { - "home_id": client.driver.controller.data["homeId"], + "home_id": controller.home_id, + "library_version": controller.library_version, + "type": controller.controller_type, + "own_node_id": controller.own_node_id, + "is_secondary": controller.is_secondary, + "is_using_home_id_from_other_network": controller.is_using_home_id_from_other_network, + "is_sis_present": controller.is_SIS_present, + "was_real_primary": controller.was_real_primary, + "is_static_update_controller": controller.is_static_update_controller, + "is_slave": controller.is_slave, + "serial_api_version": controller.serial_api_version, + "manufacturer_id": controller.manufacturer_id, + "product_id": controller.product_id, + "product_type": controller.product_type, + "supported_function_types": controller.supported_function_types, + "suc_node_id": controller.suc_node_id, + "supports_timers": controller.supports_timers, + "is_heal_network_active": controller.is_heal_network_active, "nodes": list(client.driver.controller.nodes), }, } @@ -643,6 +667,126 @@ async def websocket_remove_failed_node( ) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/begin_healing_network", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_begin_healing_network( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Begin healing the Z-Wave network.""" + controller = client.driver.controller + + result = await controller.async_begin_healing_network() + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/subscribe_heal_network_progress", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_subscribe_heal_network_progress( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Subscribe to heal Z-Wave network status updates.""" + controller = client.driver.controller + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_event(key: str, event: dict) -> None: + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": event["event"], "heal_node_status": event[key]} + ) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + unsubs = [ + controller.on("heal network progress", partial(forward_event, "progress")), + controller.on("heal network done", partial(forward_event, "result")), + ] + + connection.send_result(msg[ID]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/stop_healing_network", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_stop_healing_network( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Stop healing the Z-Wave network.""" + controller = client.driver.controller + result = await controller.async_stop_healing_network() + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/heal_node", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_heal_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Heal a node on the Z-Wave network.""" + controller = client.driver.controller + node_id = msg[NODE_ID] + result = await controller.async_heal_node(node_id) + connection.send_result( + msg[ID], + result, + ) + + @websocket_api.require_admin @websocket_api.websocket_command( { diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index fd6161b6f00..a1a06f140db 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -496,6 +496,7 @@ async def test_replace_failed_node( msg = await ws_client.receive_json() assert msg["success"] + assert msg["result"] event = Event( type="inclusion started", @@ -674,6 +675,180 @@ async def test_remove_failed_node( assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_begin_healing_network( + hass, + integration, + client, + hass_ws_client, +): + """Test the begin_healing_network websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/begin_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/begin_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_subscribe_heal_network_progress( + hass, integration, client, hass_ws_client +): + """Test the subscribe_heal_network_progress command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/subscribe_heal_network_progress", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + # Fire heal network progress + event = Event( + "heal network progress", + { + "source": "controller", + "event": "heal network progress", + "progress": {67: "pending"}, + }, + ) + client.driver.controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "heal network progress" + assert msg["event"]["heal_node_status"] == {"67": "pending"} + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/subscribe_heal_network_progress", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_stop_healing_network( + hass, + integration, + client, + hass_ws_client, +): + """Test the stop_healing_network websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/stop_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/stop_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_heal_node( + hass, + integration, + client, + hass_ws_client, +): + """Test the heal_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/heal_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/heal_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + async def test_refresh_node_info( hass, client, multisensor_6, integration, hass_ws_client ): diff --git a/tests/fixtures/zwave_js/controller_state.json b/tests/fixtures/zwave_js/controller_state.json index df026e8fd2c..d4bf58a53ce 100644 --- a/tests/fixtures/zwave_js/controller_state.json +++ b/tests/fixtures/zwave_js/controller_state.json @@ -91,7 +91,8 @@ 239 ], "sucNodeId": 1, - "supportsTimers": false + "supportsTimers": false, + "isHealNetworkActive": false }, "nodes": [ ] From 18e6ae8750d050f8eb98de342104468411e23b76 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 26 May 2021 11:39:08 -0400 Subject: [PATCH 774/852] Add WS API commands to check for and install zwave_js config updates (#51106) --- homeassistant/components/zwave_js/api.py | 50 +++++++++++ tests/components/zwave_js/test_api.py | 101 +++++++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 3e13de5ada9..ffd00919941 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -168,6 +168,8 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command( hass, websocket_subscribe_firmware_update_status ) + websocket_api.async_register_command(hass, websocket_check_for_config_updates) + websocket_api.async_register_command(hass, websocket_install_config_update) hass.http.register_view(DumpView()) hass.http.register_view(FirmwareUploadView()) @@ -1312,3 +1314,51 @@ class FirmwareUploadView(HomeAssistantView): raise web_exceptions.HTTPBadRequest from err return self.json(None) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/check_for_config_updates", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_check_for_config_updates( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Check for config updates.""" + config_update = await client.driver.async_check_for_config_updates() + connection.send_result( + msg[ID], + { + "update_available": config_update.update_available, + "new_version": config_update.new_version, + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/install_config_update", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_install_config_update( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Check for config updates.""" + success = await client.driver.async_install_config_update() + connection.send_result(msg[ID], success) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index a1a06f140db..c498c4201ae 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1899,3 +1899,104 @@ async def test_subscribe_firmware_update_status_failures( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_check_for_config_updates(hass, client, integration, hass_ws_client): + """Test that the check_for_config_updates WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + + # Test we can get log configuration + client.async_send_command.return_value = { + "updateAvailable": True, + "newVersion": "test", + } + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/check_for_config_updates", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + assert msg["result"] + assert msg["success"] + + config_update = msg["result"] + assert config_update["update_available"] + assert config_update["new_version"] == "test" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/check_for_config_updates", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/check_for_config_updates", + ENTRY_ID: "INVALID", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + +async def test_install_config_update(hass, client, integration, hass_ws_client): + """Test that the install_config_update WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + + # Test we can get log configuration + client.async_send_command.return_value = {"success": True} + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/install_config_update", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + assert msg["result"] + assert msg["success"] + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/install_config_update", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/install_config_update", + ENTRY_ID: "INVALID", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND From 19c505c0f06239fb07c1cf9e0c671f496334647a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 17:40:07 +0200 Subject: [PATCH 775/852] Add Supervisor discovery to motionEye (#50901) Co-authored-by: Martin Hjelmare --- .../components/motioneye/config_flow.py | 41 ++++- .../components/motioneye/strings.json | 4 + .../components/motioneye/translations/en.json | 4 + .../components/motioneye/test_config_flow.py | 144 ++++++++++++++++++ 4 files changed, 189 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index a8562189d1f..463c804028a 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -32,6 +32,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for motionEye.""" VERSION = 1 + _hassio_discovery: dict[str, Any] | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -42,13 +43,18 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): user_input: dict[str, Any], errors: dict[str, str] | None = None ) -> FlowResult: """Show the form to the user.""" + url_schema: dict[vol.Required, type[str]] = {} + if not self._hassio_discovery: + # Only ask for URL when not discovered + url_schema[ + vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")) + ] = str + return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required( - CONF_URL, default=user_input.get(CONF_URL, "") - ): str, + **url_schema, vol.Optional( CONF_ADMIN_USERNAME, default=user_input.get(CONF_ADMIN_USERNAME), @@ -81,6 +87,10 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): cast(Dict[str, Any], reauth_entry.data) if reauth_entry else {} ) + if self._hassio_discovery: + # In case of Supervisor discovery, use pushed URL + user_input[CONF_URL] = self._hassio_discovery[CONF_URL] + try: # Cannot use cv.url validation in the schema itself, so # apply extra validation here. @@ -123,8 +133,12 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): # at least prevent entries with the same motionEye URL. self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) + title = user_input[CONF_URL] + if self._hassio_discovery: + title = "Add-on" + return self.async_create_entry( - title=f"{user_input[CONF_URL]}", + title=title, data=user_input, ) @@ -134,3 +148,22 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle a reauthentication flow.""" return await self.async_step_user(config_data) + + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: + """Handle Supervisor discovery.""" + self._hassio_discovery = discovery_info + await self._async_handle_discovery_without_unique_id() + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm Supervisor discovery.""" + if user_input is None and self._hassio_discovery is not None: + return self.async_show_form( + step_id="hassio_confirm", + description_placeholders={"addon": self._hassio_discovery["addon"]}, + ) + + return await self.async_step_user() diff --git a/homeassistant/components/motioneye/strings.json b/homeassistant/components/motioneye/strings.json index d365ba272ea..d89b5cab275 100644 --- a/homeassistant/components/motioneye/strings.json +++ b/homeassistant/components/motioneye/strings.json @@ -9,6 +9,10 @@ "surveillance_username": "Surveillance [%key:common::config_flow::data::username%]", "surveillance_password": "Surveillance [%key:common::config_flow::data::password%]" } + }, + "hassio_confirm": { + "title": "motionEye via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to the motionEye service provided by the add-on: {addon}?" } }, "error": { diff --git a/homeassistant/components/motioneye/translations/en.json b/homeassistant/components/motioneye/translations/en.json index dd4f337e9f9..b93e4f66894 100644 --- a/homeassistant/components/motioneye/translations/en.json +++ b/homeassistant/components/motioneye/translations/en.json @@ -11,6 +11,10 @@ "unknown": "Unexpected error" }, "step": { + "hassio_confirm": { + "description": "Do you want to configure Home Assistant to connect to the motionEye service provided by the add-on: {addon}?", + "title": "motionEye via Home Assistant add-on" + }, "user": { "data": { "admin_password": "Admin Password", diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index f193c79f3ef..fbdabdadb41 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -69,6 +69,58 @@ async def test_user_success(hass: HomeAssistant) -> None: assert mock_client.async_client_close.called +async def test_hassio_success(hass: HomeAssistant) -> None: + """Test successful Supervisor flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "hassio_confirm" + assert result.get("description_placeholders") == {"addon": "motionEye"} + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result2.get("step_id") == "user" + assert "flow_id" in result2 + + mock_client = create_mock_motioneye_client() + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "Add-on" + assert result3.get("data") == { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_client.async_client_close.called + + async def test_user_invalid_auth(hass: HomeAssistant) -> None: """Test invalid auth is handled correctly.""" result = await hass.config_entries.flow.async_init( @@ -287,3 +339,95 @@ async def test_duplicate(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert mock_client.async_client_close.called + + +async def test_hassio_already_configured(hass: HomeAssistant) -> None: + """Test we don't discover when already configured.""" + MockConfigEntry( + domain=DOMAIN, + data={CONF_URL: TEST_URL}, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_hassio_ignored(hass: HomeAssistant) -> None: + """Test Supervisor discovered instance can be ignored.""" + MockConfigEntry(domain=DOMAIN, source=config_entries.SOURCE_IGNORE).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: + """Test Supervisor discovered flow aborts if user flow in progress.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result2.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result2.get("reason") == "already_in_progress" + + +async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: + """Test Supervisor discovered flow is clean up when doing user flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert "flow_id" in result2 + + mock_client = create_mock_motioneye_client() + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(mock_setup_entry.mock_calls) == 1 + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 From 16e90f12caa15fa7d990e7d7ab90d222c2483d8a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 May 2021 17:58:06 +0200 Subject: [PATCH 776/852] Add last_reset property to Tasmota energy sensors (#51107) * Add last_reset property to Tasmota energy sensors * Correct device class for energy sensors --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 29 +++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_sensor.py | 49 +++++++++++++++++++ 5 files changed, 78 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 15b5501adce..cb869b6099c 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.13"], + "requirements": ["hatasmota==0.2.14"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 19c3e37fa57..e346f8f13ac 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -1,6 +1,8 @@ """Support for Tasmota sensors.""" from __future__ import annotations +import logging + from hatasmota import const as hc, status_sensor from homeassistant.components import sensor @@ -11,6 +13,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CO2, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -40,11 +43,14 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util import dt as dt_util from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate +_LOGGER = logging.getLogger(__name__) + DEVICE_CLASS = "device_class" STATE_CLASS = "state_class" ICON = "icon" @@ -106,13 +112,16 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, STATE_CLASS: STATE_CLASS_MEASUREMENT, }, - hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - hc.SENSOR_TOTAL: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_ENERGY}, + hc.SENSOR_TOTAL: { + DEVICE_CLASS: DEVICE_CLASS_ENERGY, + STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, hc.SENSOR_VOLTAGE: {ICON: "mdi:alpha-v-circle-outline"}, hc.SENSOR_WEIGHT: {ICON: "mdi:scale"}, - hc.SENSOR_YESTERDAY: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_YESTERDAY: {DEVICE_CLASS: DEVICE_CLASS_ENERGY}, } SENSOR_UNIT_MAP = { @@ -166,6 +175,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): """Representation of a Tasmota sensor.""" + _attr_last_reset = None + def __init__(self, **kwds): """Initialize the Tasmota sensor.""" self._state = None @@ -178,6 +189,18 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): def state_updated(self, state, **kwargs): """Handle state updates.""" self._state = state + if "last_reset" in kwargs: + try: + last_reset = dt_util.as_utc( + dt_util.parse_datetime(kwargs["last_reset"]) + ) + if last_reset is None: + raise ValueError + self._attr_last_reset = last_reset + except ValueError: + _LOGGER.warning( + "Invalid last_reset timestamp '%s'", kwargs["last_reset"] + ) self.async_write_ha_state() @property diff --git a/requirements_all.txt b/requirements_all.txt index b02a6191c9a..6ecfed46076 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.13 +hatasmota==0.2.14 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fa3d2ced9b..8ebcfc3588b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -411,7 +411,7 @@ hangups==0.4.11 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.13 +hatasmota==0.2.14 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 425fa3f26f6..e9aa291fe6d 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -233,6 +233,55 @@ async def test_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert state.state == "7.8" +async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT for sensor with last_reset property.""" + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(INDEXED_SENSOR_CONFIG) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.tasmota_energy_total") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("sensor.tasmota_energy_total") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Test periodic state update + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"ENERGY":{"Total":1.2,"TotalStartTime":"2018-11-23T15:33:47"}}', + ) + state = hass.states.get("sensor.tasmota_energy_total") + assert state.state == "1.2" + assert state.attributes["last_reset"] == "2018-11-23T15:33:47+00:00" + + # Test polled state update + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"ENERGY":{"Total":5.6,"TotalStartTime":"2018-11-23T16:33:47"}}}', + ) + state = hass.states.get("sensor.tasmota_energy_total") + assert state.state == "5.6" + assert state.attributes["last_reset"] == "2018-11-23T16:33:47+00:00" + + async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT where sensor is not matching configuration.""" config = copy.deepcopy(DEFAULT_CONFIG) From 64661ee2b7b902c09d8de9960e146f6a69831bd9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 May 2021 11:06:30 -0500 Subject: [PATCH 777/852] Add network configuration integration (#50874) Co-authored-by: Ruslan Sayfutdinov Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + .../components/default_config/manifest.json | 1 + homeassistant/components/network/__init__.py | 91 ++++ homeassistant/components/network/const.py | 27 ++ .../components/network/manifest.json | 10 + homeassistant/components/network/models.py | 31 ++ homeassistant/components/network/network.py | 78 +++ homeassistant/components/network/util.py | 158 +++++++ homeassistant/components/zeroconf/__init__.py | 82 ++-- .../components/zeroconf/manifest.json | 4 +- homeassistant/package_constraints.txt | 2 +- mypy.ini | 11 + requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- script/hassfest/dependencies.py | 2 + tests/components/network/__init__.py | 1 + tests/components/network/test_init.py | 446 ++++++++++++++++++ tests/components/zeroconf/test_init.py | 100 ++-- tests/test_requirements.py | 4 +- 19 files changed, 955 insertions(+), 106 deletions(-) create mode 100644 homeassistant/components/network/__init__.py create mode 100644 homeassistant/components/network/const.py create mode 100644 homeassistant/components/network/manifest.json create mode 100644 homeassistant/components/network/models.py create mode 100644 homeassistant/components/network/network.py create mode 100644 homeassistant/components/network/util.py create mode 100644 tests/components/network/__init__.py create mode 100644 tests/components/network/test_init.py diff --git a/.strict-typing b/.strict-typing index a72118fa843..00bc3447d22 100644 --- a/.strict-typing +++ b/.strict-typing @@ -45,6 +45,7 @@ homeassistant.components.lock.* homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.nam.* +homeassistant.components.network.* homeassistant.components.notify.* homeassistant.components.number.* homeassistant.components.onewire.* diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 74c6b228a6f..032a6845340 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -19,6 +19,7 @@ "media_source", "mobile_app", "my", + "network", "person", "scene", "script", diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py new file mode 100644 index 00000000000..3f19103acaa --- /dev/null +++ b/homeassistant/components/network/__init__.py @@ -0,0 +1,91 @@ +"""The Network Configuration integration.""" +from __future__ import annotations + +import logging + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import bind_hass + +from .const import ( + ATTR_ADAPTERS, + ATTR_CONFIGURED_ADAPTERS, + DOMAIN, + NETWORK_CONFIG_SCHEMA, +) +from .models import Adapter +from .network import Network + +ZEROCONF_DOMAIN = "zeroconf" # cannot import from zeroconf due to circular dep +_LOGGER = logging.getLogger(__name__) + + +@bind_hass +async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]: + """Get the network adapter configuration.""" + network: Network = hass.data[DOMAIN] + return network.adapters + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up network for Home Assistant.""" + + hass.data[DOMAIN] = network = Network(hass) + await network.async_setup() + if ZEROCONF_DOMAIN in config: + await network.async_migrate_from_zeroconf(config[ZEROCONF_DOMAIN]) + network.async_configure() + + _LOGGER.debug("Adapters: %s", network.adapters) + + websocket_api.async_register_command(hass, websocket_network_adapters) + websocket_api.async_register_command(hass, websocket_network_adapters_configure) + + return True + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "network"}) +@websocket_api.async_response +async def websocket_network_adapters( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, +) -> None: + """Return network preferences.""" + network: Network = hass.data[DOMAIN] + connection.send_result( + msg["id"], + { + ATTR_ADAPTERS: network.adapters, + ATTR_CONFIGURED_ADAPTERS: network.configured_adapters, + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "network/configure", + vol.Required("config", default={}): NETWORK_CONFIG_SCHEMA, + } +) +@websocket_api.async_response +async def websocket_network_adapters_configure( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, +) -> None: + """Update network config.""" + network: Network = hass.data[DOMAIN] + + await network.async_reconfig(msg["config"]) + + connection.send_result( + msg["id"], + {ATTR_CONFIGURED_ADAPTERS: network.configured_adapters}, + ) diff --git a/homeassistant/components/network/const.py b/homeassistant/components/network/const.py new file mode 100644 index 00000000000..ff69f026fef --- /dev/null +++ b/homeassistant/components/network/const.py @@ -0,0 +1,27 @@ +"""Constants for the network integration.""" +from __future__ import annotations + +from typing import Final + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +DOMAIN: Final = "network" +STORAGE_KEY: Final = "core.network" +STORAGE_VERSION: Final = 1 + +ATTR_ADAPTERS: Final = "adapters" +ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters" +DEFAULT_CONFIGURED_ADAPTERS: list[str] = [] + +MDNS_TARGET_IP: Final = "224.0.0.251" + + +NETWORK_CONFIG_SCHEMA = vol.Schema( + { + vol.Optional( + ATTR_CONFIGURED_ADAPTERS, default=DEFAULT_CONFIGURED_ADAPTERS + ): vol.Schema(vol.All(cv.ensure_list, [cv.string])), + } +) diff --git a/homeassistant/components/network/manifest.json b/homeassistant/components/network/manifest.json new file mode 100644 index 00000000000..84e86014036 --- /dev/null +++ b/homeassistant/components/network/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "network", + "name": "Network Configuration", + "documentation": "https://www.home-assistant.io/integrations/network", + "requirements": ["ifaddr==0.1.7"], + "codeowners": [], + "dependencies": ["websocket_api"], + "quality_scale": "internal", + "iot_class": "local_push" +} diff --git a/homeassistant/components/network/models.py b/homeassistant/components/network/models.py new file mode 100644 index 00000000000..a007eb8636d --- /dev/null +++ b/homeassistant/components/network/models.py @@ -0,0 +1,31 @@ +"""Models helper class for the network integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class IPv6ConfiguredAddress(TypedDict): + """Represent an IPv6 address.""" + + address: str + flowinfo: int + scope_id: int + network_prefix: int + + +class IPv4ConfiguredAddress(TypedDict): + """Represent an IPv4 address.""" + + address: str + network_prefix: int + + +class Adapter(TypedDict): + """Configured network adapters.""" + + name: str + enabled: bool + auto: bool + default: bool + ipv6: list[IPv6ConfiguredAddress] + ipv4: list[IPv4ConfiguredAddress] diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py new file mode 100644 index 00000000000..1243ba24774 --- /dev/null +++ b/homeassistant/components/network/network.py @@ -0,0 +1,78 @@ +"""Network helper class for the network integration.""" +from __future__ import annotations + +from typing import Any, cast + +from homeassistant.core import HomeAssistant, callback + +from .const import ( + ATTR_CONFIGURED_ADAPTERS, + DEFAULT_CONFIGURED_ADAPTERS, + NETWORK_CONFIG_SCHEMA, + STORAGE_KEY, + STORAGE_VERSION, +) +from .models import Adapter +from .util import ( + adapters_with_exernal_addresses, + async_load_adapters, + enable_adapters, + enable_auto_detected_adapters, +) + + +class Network: + """Network helper class for the network integration.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the Network class.""" + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._data: dict[str, Any] = {} + self.adapters: list[Adapter] = [] + + @property + def configured_adapters(self) -> list[str]: + """Return the configured adapters.""" + return self._data.get(ATTR_CONFIGURED_ADAPTERS, DEFAULT_CONFIGURED_ADAPTERS) + + async def async_setup(self) -> None: + """Set up the network config.""" + await self.async_load() + self.adapters = await async_load_adapters() + + async def async_migrate_from_zeroconf(self, zc_config: dict[str, Any]) -> None: + """Migrate configuration from zeroconf.""" + if self._data or not zc_config: + return + + from homeassistant.components.zeroconf import ( # pylint: disable=import-outside-toplevel + CONF_DEFAULT_INTERFACE, + ) + + if zc_config.get(CONF_DEFAULT_INTERFACE) is False: + self._data[ATTR_CONFIGURED_ADAPTERS] = adapters_with_exernal_addresses( + self.adapters + ) + await self._async_save() + + @callback + def async_configure(self) -> None: + """Configure from storage.""" + if not enable_adapters(self.adapters, self.configured_adapters): + enable_auto_detected_adapters(self.adapters) + + async def async_reconfig(self, config: dict[str, Any]) -> None: + """Reconfigure network.""" + config = NETWORK_CONFIG_SCHEMA(config) + self._data[ATTR_CONFIGURED_ADAPTERS] = config[ATTR_CONFIGURED_ADAPTERS] + self.async_configure() + await self._async_save() + + async def async_load(self) -> None: + """Load config.""" + if stored := await self._store.async_load(): + self._data = cast(dict, stored) + + async def _async_save(self) -> None: + """Save preferences.""" + await self._store.async_save(self._data) diff --git a/homeassistant/components/network/util.py b/homeassistant/components/network/util.py new file mode 100644 index 00000000000..eece4b38548 --- /dev/null +++ b/homeassistant/components/network/util.py @@ -0,0 +1,158 @@ +"""Network helper class for the network integration.""" +from __future__ import annotations + +from ipaddress import IPv4Address, IPv6Address, ip_address +import logging +import socket +from typing import cast + +import ifaddr + +from homeassistant.core import callback + +from .const import MDNS_TARGET_IP +from .models import Adapter, IPv4ConfiguredAddress, IPv6ConfiguredAddress + +_LOGGER = logging.getLogger(__name__) + + +async def async_load_adapters() -> list[Adapter]: + """Load adapters.""" + source_ip = async_get_source_ip(MDNS_TARGET_IP) + source_ip_address = ip_address(source_ip) if source_ip else None + + ha_adapters: list[Adapter] = [ + _ifaddr_adapter_to_ha(adapter, source_ip_address) + for adapter in ifaddr.get_adapters() + ] + + if not any(adapter["default"] and adapter["auto"] for adapter in ha_adapters): + for adapter in ha_adapters: + if _adapter_has_external_address(adapter): + adapter["auto"] = True + + return ha_adapters + + +def enable_adapters(adapters: list[Adapter], enabled_interfaces: list[str]) -> bool: + """Enable configured adapters.""" + _reset_enabled_adapters(adapters) + + if not enabled_interfaces: + return False + + found_adapter = False + for adapter in adapters: + if adapter["name"] in enabled_interfaces: + adapter["enabled"] = True + found_adapter = True + + return found_adapter + + +def enable_auto_detected_adapters(adapters: list[Adapter]) -> None: + """Enable auto detected adapters.""" + enable_adapters( + adapters, [adapter["name"] for adapter in adapters if adapter["auto"]] + ) + + +def adapters_with_exernal_addresses(adapters: list[Adapter]) -> list[str]: + """Enable all interfaces with an external address.""" + return [ + adapter["name"] + for adapter in adapters + if _adapter_has_external_address(adapter) + ] + + +def _adapter_has_external_address(adapter: Adapter) -> bool: + """Adapter has a non-loopback and non-link-local address.""" + return any( + _has_external_address(v4_config["address"]) for v4_config in adapter["ipv4"] + ) or any( + _has_external_address(v6_config["address"]) for v6_config in adapter["ipv6"] + ) + + +def _has_external_address(ip_str: str) -> bool: + return _ip_address_is_external(ip_address(ip_str)) + + +def _ip_address_is_external(ip_addr: IPv4Address | IPv6Address) -> bool: + return ( + not ip_addr.is_multicast + and not ip_addr.is_loopback + and not ip_addr.is_link_local + ) + + +def _reset_enabled_adapters(adapters: list[Adapter]) -> None: + for adapter in adapters: + adapter["enabled"] = False + + +def _ifaddr_adapter_to_ha( + adapter: ifaddr.Adapter, next_hop_address: None | IPv4Address | IPv6Address +) -> Adapter: + """Convert an ifaddr adapter to ha.""" + ip_v4s: list[IPv4ConfiguredAddress] = [] + ip_v6s: list[IPv6ConfiguredAddress] = [] + default = False + auto = False + + for ip_config in adapter.ips: + if ip_config.is_IPv6: + ip_addr = ip_address(ip_config.ip[0]) + ip_v6s.append(_ip_v6_from_adapter(ip_config)) + else: + ip_addr = ip_address(ip_config.ip) + ip_v4s.append(_ip_v4_from_adapter(ip_config)) + + if ip_addr == next_hop_address: + default = True + if _ip_address_is_external(ip_addr): + auto = True + + return { + "name": adapter.nice_name, + "enabled": False, + "auto": auto, + "default": default, + "ipv4": ip_v4s, + "ipv6": ip_v6s, + } + + +def _ip_v6_from_adapter(ip_config: ifaddr.IP) -> IPv6ConfiguredAddress: + return { + "address": ip_config.ip[0], + "flowinfo": ip_config.ip[1], + "scope_id": ip_config.ip[2], + "network_prefix": ip_config.network_prefix, + } + + +def _ip_v4_from_adapter(ip_config: ifaddr.IP) -> IPv4ConfiguredAddress: + return { + "address": ip_config.ip, + "network_prefix": ip_config.network_prefix, + } + + +@callback +def async_get_source_ip(target_ip: str) -> str | None: + """Return the source ip that will reach target_ip.""" + test_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + test_sock.setblocking(False) # must be non-blocking for async + try: + test_sock.connect((target_ip, 1)) + return cast(str, test_sock.getsockname()[0]) + except Exception: # pylint: disable=broad-except + _LOGGER.debug( + "The system could not auto detect the source ip for %s on your operating system", + target_ip, + ) + return None + finally: + test_sock.close() diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index dba0c0f6aa8..64d631e2850 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -2,16 +2,14 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine, Iterable +from collections.abc import Coroutine from contextlib import suppress import fnmatch import ipaddress -from ipaddress import ip_address import logging import socket from typing import Any, TypedDict, cast -from pyroute2 import IPRoute import voluptuous as vol from zeroconf import ( InterfaceChoice, @@ -23,6 +21,8 @@ from zeroconf import ( ) from homeassistant import config_entries, util +from homeassistant.components import network +from homeassistant.components.network.models import Adapter from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED, @@ -34,7 +34,6 @@ from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass -from homeassistant.util.network import is_loopback from .models import HaAsyncZeroconf, HaServiceBrowser, HaZeroconf from .usage import install_multiple_zeroconf_catcher @@ -69,11 +68,14 @@ MAX_NAME_LEN = 63 CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean, - vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean, - } + DOMAIN: vol.All( + cv.deprecated(CONF_DEFAULT_INTERFACE), + vol.Schema( + { + vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean, + vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean, + } + ), ) }, extra=vol.ALLOW_EXTRA, @@ -132,49 +134,11 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero return aio_zc -def _get_ip_route(dst_ip: str) -> Any: - """Get ip next hop.""" - return IPRoute().route("get", dst=dst_ip) - - -def _first_ip_nexthop_from_route(routes: Iterable) -> None | str: - """Find the first RTA_PREFSRC in the routes.""" - _LOGGER.debug("Routes: %s", routes) - for route in routes: - for key, value in route["attrs"]: - if key == "RTA_PREFSRC": - return cast(str, value) - return None - - -async def async_detect_interfaces_setting(hass: HomeAssistant) -> InterfaceChoice: - """Auto detect the interfaces setting when unset.""" - routes = [] - try: - routes = await hass.async_add_executor_job(_get_ip_route, MDNS_TARGET_IP) - except Exception as ex: # pylint: disable=broad-except - _LOGGER.debug( - "The system could not auto detect routing data on your operating system; Zeroconf will broadcast on all interfaces", - exc_info=ex, - ) - return InterfaceChoice.All - - if not (first_ip := _first_ip_nexthop_from_route(routes)): - _LOGGER.debug( - "The system could not auto detect the nexthop for %s on your operating system; Zeroconf will broadcast on all interfaces", - MDNS_TARGET_IP, - ) - return InterfaceChoice.All - - if is_loopback(ip_address(first_ip)): - _LOGGER.debug( - "The next hop for %s is %s; Zeroconf will broadcast on all interfaces", - MDNS_TARGET_IP, - first_ip, - ) - return InterfaceChoice.All - - return InterfaceChoice.Default +def _async_use_default_interface(adapters: list[Adapter]) -> bool: + for adapter in adapters: + if adapter["enabled"] and not adapter["default"]: + return False + return True async def async_setup(hass: HomeAssistant, config: dict) -> bool: @@ -182,10 +146,18 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: zc_config = config.get(DOMAIN, {}) zc_args: dict = {} - if CONF_DEFAULT_INTERFACE not in zc_config: - zc_args["interfaces"] = await async_detect_interfaces_setting(hass) - elif zc_config[CONF_DEFAULT_INTERFACE]: + adapters = await network.async_get_adapters(hass) + if _async_use_default_interface(adapters): zc_args["interfaces"] = InterfaceChoice.Default + else: + interfaces = zc_args["interfaces"] = [] + for adapter in adapters: + if not adapter["enabled"]: + continue + if ipv4s := adapter["ipv4"]: + interfaces.append(ipv4s[0]["address"]) + elif ipv6s := adapter["ipv6"]: + interfaces.append(ipv6s[0]["scope_id"]) if not zc_config.get(CONF_IPV6, DEFAULT_IPV6): zc_args["ip_version"] = IPVersion.V4Only diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 3abd8824eba..030a970d77d 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,8 +2,8 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.31.0","pyroute2==0.5.18"], - "dependencies": ["api"], + "requirements": ["zeroconf==0.31.0"], + "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e3533632af8..66e1ab9315c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,12 +19,12 @@ emoji==1.2.0 hass-nabucasa==0.43.0 home-assistant-frontend==20210518.0 httpx==0.18.0 +ifaddr==0.1.7 jinja2>=3.0.1 netdisco==2.8.3 paho-mqtt==1.5.1 pillow==8.1.2 pip>=8.0.3,<20.3 -pyroute2==0.5.18 python-slugify==4.0.1 pyyaml==5.4.1 requests==2.25.1 diff --git a/mypy.ini b/mypy.ini index 39b32b29994..c65f28336ff 100644 --- a/mypy.ini +++ b/mypy.ini @@ -506,6 +506,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.network.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.notify.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 6ecfed46076..72fe862ee52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -818,6 +818,9 @@ ibmiotf==0.3.4 # homeassistant.components.ping icmplib==2.1.1 +# homeassistant.components.network +ifaddr==0.1.7 + # homeassistant.components.iglo iglo==1.2.7 @@ -1690,9 +1693,6 @@ pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.3 -# homeassistant.components.zeroconf -pyroute2==0.5.18 - # homeassistant.components.ruckus_unleashed pyruckus==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ebcfc3588b..6758315a32a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -462,6 +462,9 @@ iaqualink==0.3.4 # homeassistant.components.ping icmplib==2.1.1 +# homeassistant.components.network +ifaddr==0.1.7 + # homeassistant.components.influxdb influxdb-client==1.14.0 @@ -947,9 +950,6 @@ pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.3 -# homeassistant.components.zeroconf -pyroute2==0.5.18 - # homeassistant.components.ruckus_unleashed pyruckus==0.12 diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index cb7458af154..1df09d6f0d5 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -136,6 +136,8 @@ IGNORE_VIOLATIONS = { ("demo", "openalpr_local"), # Migration wizard from zwave to ozw. "ozw", + # Migration of settings from zeroconf to network + ("network", "zeroconf"), # This should become a helper method that integrations can submit data to ("websocket_api", "lovelace"), ("websocket_api", "shopping_list"), diff --git a/tests/components/network/__init__.py b/tests/components/network/__init__.py new file mode 100644 index 00000000000..f3ccacbd064 --- /dev/null +++ b/tests/components/network/__init__.py @@ -0,0 +1 @@ +"""Tests for the Network Configuration integration.""" diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py new file mode 100644 index 00000000000..41d87d5a805 --- /dev/null +++ b/tests/components/network/test_init.py @@ -0,0 +1,446 @@ +"""Test the Network Configuration.""" +from unittest.mock import Mock, patch + +import ifaddr + +from homeassistant.components import network +from homeassistant.components.network.const import ( + ATTR_ADAPTERS, + ATTR_CONFIGURED_ADAPTERS, + STORAGE_KEY, + STORAGE_VERSION, +) +from homeassistant.setup import async_setup_component + +_NO_LOOPBACK_IPADDR = "192.168.1.5" +_LOOPBACK_IPADDR = "127.0.0.1" + + +def _generate_mock_adapters(): + mock_lo0 = Mock(spec=ifaddr.Adapter) + mock_lo0.nice_name = "lo0" + mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0")] + mock_eth0 = Mock(spec=ifaddr.Adapter) + mock_eth0.nice_name = "eth0" + mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0")] + mock_eth1 = Mock(spec=ifaddr.Adapter) + mock_eth1.nice_name = "eth1" + mock_eth1.ips = [ifaddr.IP("192.168.1.5", 23, "eth1")] + mock_vtun0 = Mock(spec=ifaddr.Adapter) + mock_vtun0.nice_name = "vtun0" + mock_vtun0.ips = [ifaddr.IP("169.254.3.2", 16, "vtun0")] + return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] + + +async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_storage): + """Test without default interface config and the route returns a non-loopback address.""" + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[_NO_LOOPBACK_IPADDR], + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == [] + + assert network_obj.adapters == [ + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + +async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage): + """Test without default interface config and the route returns a loopback address.""" + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[_LOOPBACK_IPADDR], + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == [] + assert network_obj.adapters == [ + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": True, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + +async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): + """Test without default interface config and the route returns nothing.""" + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[], + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == [] + assert network_obj.adapters == [ + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + +async def test_async_detect_interfaces_setting_exception(hass, hass_storage): + """Test without default interface config and the route throws an exception.""" + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + side_effect=AttributeError, + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == [] + assert network_obj.adapters == [ + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + +async def test_interfaces_configured_from_storage(hass, hass_storage): + """Test settings from storage are preferred over auto configure.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]}, + } + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[_NO_LOOPBACK_IPADDR], + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"] + + assert network_obj.adapters == [ + { + "auto": False, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": True, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + +async def test_interfaces_configured_from_storage_websocket_update( + hass, hass_ws_client, hass_storage +): + """Test settings from storage can be updated via websocket api.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]}, + } + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[_NO_LOOPBACK_IPADDR], + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"] + ws_client = await hass_ws_client(hass) + await ws_client.send_json({"id": 1, "type": "network"}) + + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"][ATTR_CONFIGURED_ADAPTERS] == ["eth0", "eth1", "vtun0"] + assert response["result"][ATTR_ADAPTERS] == [ + { + "auto": False, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": True, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + await ws_client.send_json( + {"id": 2, "type": "network/configure", "config": {ATTR_CONFIGURED_ADAPTERS: []}} + ) + response = await ws_client.receive_json() + assert response["result"][ATTR_CONFIGURED_ADAPTERS] == [] + + await ws_client.send_json({"id": 3, "type": "network"}) + response = await ws_client.receive_json() + assert response["result"][ATTR_CONFIGURED_ADAPTERS] == [] + assert response["result"][ATTR_ADAPTERS] == [ + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 2f66404f27b..ef0ab1fda60 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,5 +1,5 @@ """Test Zeroconf component setup process.""" -from unittest.mock import patch +from unittest.mock import call, patch from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange @@ -697,13 +697,27 @@ async def test_removed_ignored(hass, mock_zeroconf): assert mock_service_info.mock_calls[1][1][0] == "_service.updated.local." -async def test_async_detect_interfaces_setting_non_loopback_route(hass, mock_zeroconf): +_ADAPTER_WITH_DEFAULT_ENABLED = [ + { + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + } +] + + +async def test_async_detect_interfaces_setting_non_loopback_route(hass): """Test without default interface config and the route returns a non-loopback address.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( + with patch( + "homeassistant.components.zeroconf.models.HaZeroconf" + ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock ), patch( - "homeassistant.components.zeroconf.IPRoute.route", - return_value=_ROUTE_NO_LOOPBACK, + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTER_WITH_DEFAULT_ENABLED, ), patch( "homeassistant.components.zeroconf.ServiceInfo", side_effect=get_service_info_mock, @@ -712,47 +726,53 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, mock_zer hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.Default) + assert mock_zc.mock_calls[0] == call(interfaces=InterfaceChoice.Default) -async def test_async_detect_interfaces_setting_loopback_route(hass, mock_zeroconf): - """Test without default interface config and the route returns a loopback address.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.IPRoute.route", return_value=_ROUTE_LOOPBACK - ), patch( - "homeassistant.components.zeroconf.ServiceInfo", - side_effect=get_service_info_mock, - ): - assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All) +_ADAPTERS_WITH_MANUAL_CONFIG = [ + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, +] -async def test_async_detect_interfaces_setting_empty_route(hass, mock_zeroconf): +async def test_async_detect_interfaces_setting_empty_route(hass): """Test without default interface config and the route returns nothing.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ), patch("homeassistant.components.zeroconf.IPRoute.route", return_value=[]), patch( - "homeassistant.components.zeroconf.ServiceInfo", - side_effect=get_service_info_mock, - ): - assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All) - - -async def test_async_detect_interfaces_setting_exception(hass, mock_zeroconf): - """Test without default interface config and the route throws an exception.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( + with patch( + "homeassistant.components.zeroconf.models.HaZeroconf" + ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock ), patch( - "homeassistant.components.zeroconf.IPRoute.route", side_effect=AttributeError + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, ), patch( "homeassistant.components.zeroconf.ServiceInfo", side_effect=get_service_info_mock, @@ -761,4 +781,4 @@ async def test_async_detect_interfaces_setting_exception(hass, mock_zeroconf): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All) + assert mock_zc.mock_calls[0] == call(interfaces=[1, "192.168.1.5"]) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index ff3f5bcab87..f68601e889e 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -361,7 +361,7 @@ async def test_discovery_requirements_ssdp(hass): ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") - assert len(mock_process.mock_calls) == 3 + assert len(mock_process.mock_calls) == 4 assert mock_process.mock_calls[0][1][2] == ssdp.requirements # Ensure zeroconf is a dep for ssdp assert mock_process.mock_calls[1][1][1] == "zeroconf" @@ -386,7 +386,7 @@ async def test_discovery_requirements_zeroconf(hass, partial_manifest): ) as mock_process: await async_get_integration_with_requirements(hass, "comp") - assert len(mock_process.mock_calls) == 2 # zeroconf also depends on http + assert len(mock_process.mock_calls) == 3 # zeroconf also depends on http assert mock_process.mock_calls[0][1][2] == zeroconf.requirements From ffb9ab21c15f2a6b1b8b96e0895f66733f6f12ac Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Wed, 26 May 2021 09:25:47 -0700 Subject: [PATCH 778/852] Add binary sensor for smarttub errors (#49364) --- .../components/smarttub/binary_sensor.py | 63 ++++++++++++++++++- homeassistant/components/smarttub/const.py | 1 + .../components/smarttub/controller.py | 5 +- tests/components/smarttub/conftest.py | 2 + .../components/smarttub/test_binary_sensor.py | 44 ++++++++++++- 5 files changed, 111 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index d1019f7f432..7ab343d2015 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -1,7 +1,7 @@ """Platform for binary sensor integration.""" import logging -from smarttub import SpaReminder +from smarttub import SpaError, SpaReminder import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.helpers import entity_platform -from .const import ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER +from .const import ATTR_ERRORS, ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubEntity, SmartTubSensorBase _LOGGER = logging.getLogger(__name__) @@ -19,6 +19,13 @@ _LOGGER = logging.getLogger(__name__) # whether the reminder has been snoozed (bool) ATTR_REMINDER_SNOOZED = "snoozed" +ATTR_ERROR_CODE = "error_code" +ATTR_ERROR_TITLE = "error_title" +ATTR_ERROR_DESCRIPTION = "error_description" +ATTR_ERROR_TYPE = "error_type" +ATTR_CREATED_AT = "created_at" +ATTR_UPDATED_AT = "updated_at" + # how many days to snooze the reminder for ATTR_SNOOZE_DAYS = "days" SNOOZE_REMINDER_SCHEMA = { @@ -34,6 +41,7 @@ async def async_setup_entry(hass, entry, async_add_entities): entities = [] for spa in controller.spas: entities.append(SmartTubOnline(controller.coordinator, spa)) + entities.append(SmartTubError(controller.coordinator, spa)) entities.extend( SmartTubReminder(controller.coordinator, spa, reminder) for reminder in controller.coordinator.data[spa.id][ATTR_REMINDERS].values() @@ -119,3 +127,54 @@ class SmartTubReminder(SmartTubEntity, BinarySensorEntity): """Snooze this reminder for the specified number of days.""" await self.reminder.snooze(days) await self.coordinator.async_request_refresh() + + +class SmartTubError(SmartTubEntity, BinarySensorEntity): + """Indicates whether an error code is present. + + There may be 0 or more errors. If there are >0, we show the first one. + """ + + def __init__(self, coordinator, spa): + """Initialize the entity.""" + super().__init__( + coordinator, + spa, + "Error", + ) + + @property + def error(self) -> SpaError: + """Return the underlying SpaError object for this entity.""" + errors = self.coordinator.data[self.spa.id][ATTR_ERRORS] + if len(errors) == 0: + return None + return errors[0] + + @property + def is_on(self) -> bool: + """Return true if an error is signaled.""" + return self.error is not None + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + + error = self.error + + if error is None: + return {} + + return { + ATTR_ERROR_CODE: error.code, + ATTR_ERROR_TITLE: error.title, + ATTR_ERROR_DESCRIPTION: error.description, + ATTR_ERROR_TYPE: error.error_type, + ATTR_CREATED_AT: error.created_at.isoformat(), + ATTR_UPDATED_AT: error.updated_at.isoformat(), + } + + @property + def device_class(self) -> str: + """Return the device class for this entity.""" + return DEVICE_CLASS_PROBLEM diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py index 23bd8bd8ec0..f97ef65a54c 100644 --- a/homeassistant/components/smarttub/const.py +++ b/homeassistant/components/smarttub/const.py @@ -21,6 +21,7 @@ DEFAULT_LIGHT_EFFECT = "purple" # default to 50% brightness DEFAULT_LIGHT_BRIGHTNESS = 128 +ATTR_ERRORS = "errors" ATTR_LIGHTS = "lights" ATTR_PUMPS = "pumps" ATTR_REMINDERS = "reminders" diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 06b0989233c..48b1d603c5c 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -16,6 +16,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( + ATTR_ERRORS, ATTR_LIGHTS, ATTR_PUMPS, ATTR_REMINDERS, @@ -92,15 +93,17 @@ class SmartTubController: return data async def _get_spa_data(self, spa): - full_status, reminders = await asyncio.gather( + full_status, reminders, errors = await asyncio.gather( spa.get_status_full(), spa.get_reminders(), + spa.get_errors(), ) return { ATTR_STATUS: full_status, ATTR_PUMPS: {pump.id: pump for pump in full_status.pumps}, ATTR_LIGHTS: {light.zone: light for light in full_status.lights}, ATTR_REMINDERS: {reminder.id: reminder for reminder in reminders}, + ATTR_ERRORS: errors, } async def async_register_devices(self, entry): diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index 2b6991fbbe0..c05762a903d 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -87,6 +87,8 @@ def mock_spa(spa_state): mock_spa.get_reminders.return_value = [mock_filter_reminder] + mock_spa.get_errors.return_value = [] + return mock_spa diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py index b39986ef394..16b4f60d3e4 100644 --- a/tests/components/smarttub/test_binary_sensor.py +++ b/tests/components/smarttub/test_binary_sensor.py @@ -1,5 +1,11 @@ """Test the SmartTub binary sensor platform.""" -from homeassistant.components.binary_sensor import STATE_OFF +from datetime import datetime +from unittest.mock import create_autospec + +import pytest +import smarttub + +from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON async def test_binary_sensors(spa, setup_entry, hass): @@ -10,6 +16,11 @@ async def test_binary_sensors(spa, setup_entry, hass): # disabled by default assert state is None + entity_id = f"binary_sensor.{spa.brand}_{spa.model}_error" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + async def test_reminders(spa, setup_entry, hass): """Test the reminder sensor.""" @@ -21,6 +32,37 @@ async def test_reminders(spa, setup_entry, hass): assert state.attributes["snoozed"] is False +@pytest.fixture +def mock_error(spa): + """Mock error.""" + error = create_autospec(smarttub.SpaError, instance=True) + error.code = 11 + error.title = "Flow Switch Stuck Open" + error.description = None + error.active = True + error.created_at = datetime.now() + error.updated_at = datetime.now() + error.error_type = "TUB_ERROR" + return error + + +async def test_error(spa, hass, config_entry, mock_error): + """Test the error sensor.""" + + spa.get_errors.return_value = [mock_error] + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_id = f"binary_sensor.{spa.brand}_{spa.model}_error" + state = hass.states.get(entity_id) + assert state is not None + + assert state.state == STATE_ON + assert state.attributes["error_code"] == 11 + + async def test_snooze(spa, setup_entry, hass): """Test snoozing a reminder.""" From 31c07e710ac322f23beef59aa281d270042beebd Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Wed, 26 May 2021 18:31:23 +0200 Subject: [PATCH 779/852] bump garage_amsterdam lib to v2.1.1 (#51111) --- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index 3e4d90a38aa..ef90655276b 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -3,7 +3,7 @@ "name": "Garages Amsterdam", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", - "requirements": ["garages-amsterdam==2.0.5"], + "requirements": ["garages-amsterdam==2.1.1"], "codeowners": ["@klaasnicolaas"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 72fe862ee52..df880235caf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -632,7 +632,7 @@ fritzconnection==1.4.2 gTTS==2.2.2 # homeassistant.components.garages_amsterdam -garages-amsterdam==2.0.5 +garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect garminconnect==0.1.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6758315a32a..9419635bace 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ fritzconnection==1.4.2 gTTS==2.2.2 # homeassistant.components.garages_amsterdam -garages-amsterdam==2.0.5 +garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect garminconnect==0.1.19 From bcd91cc2bd720d4774df674743df1e9b976342ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 May 2021 09:31:35 -0700 Subject: [PATCH 780/852] Drop certificate filling in for cloudmqtt (#51112) --- homeassistant/components/mqtt/__init__.py | 14 +---------- .../mqtt/addtrustexternalcaroot.crt | 25 ------------------- 2 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 homeassistant/components/mqtt/addtrustexternalcaroot.crt diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 16379aa7923..de7fb69b8b6 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -7,7 +7,6 @@ import inspect from itertools import groupby import logging from operator import attrgetter -import os import ssl import time from typing import Any, Callable, Union @@ -633,18 +632,7 @@ class MQTT: certificate = self.conf.get(CONF_CERTIFICATE) - # For cloudmqtt.com, secured connection, auto fill in certificate - if ( - certificate is None - and 19999 < self.conf[CONF_PORT] < 30000 - and self.conf[CONF_BROKER].endswith(".cloudmqtt.com") - ): - certificate = os.path.join( - os.path.dirname(__file__), "addtrustexternalcaroot.crt" - ) - - # When the certificate is set to auto, use bundled certs from certifi - elif certificate == "auto": + if certificate == "auto": certificate = certifi.where() client_key = self.conf.get(CONF_CLIENT_KEY) diff --git a/homeassistant/components/mqtt/addtrustexternalcaroot.crt b/homeassistant/components/mqtt/addtrustexternalcaroot.crt deleted file mode 100644 index 20585f1c01e..00000000000 --- a/homeassistant/components/mqtt/addtrustexternalcaroot.crt +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU -MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs -IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 -MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux -FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h -bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v -dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt -H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 -uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX -mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX -a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN -E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 -WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD -VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 -Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU -cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx -IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN -AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH -YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 -6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC -Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX -c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a -mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= ------END CERTIFICATE----- From 42b92748f65a08bf95fea59f020f5295582bfad8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 26 May 2021 18:31:46 +0200 Subject: [PATCH 781/852] Update frontend to 20210526.0 (#51110) --- 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 73743f21d9d..57380b53be8 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==20210518.0" + "home-assistant-frontend==20210526.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 66e1ab9315c..b61e0d24785 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210518.0 +home-assistant-frontend==20210526.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index df880235caf..fb30eb5bd68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210518.0 +home-assistant-frontend==20210526.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9419635bace..d2aa89f3d53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210518.0 +home-assistant-frontend==20210526.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5ac81ccb4cd69c21c8fa84bb0568255de88d1e34 Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Wed, 26 May 2021 17:39:44 +0100 Subject: [PATCH 782/852] Openhome component now uses asyncio and handles unavailability (#49574) --- .../components/openhome/manifest.json | 2 +- .../components/openhome/media_player.py | 205 +++++++++++------- requirements_all.txt | 2 +- 3 files changed, 133 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index f45d6d31cef..c83b135cb8a 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -2,7 +2,7 @@ "domain": "openhome", "name": "Linn / OpenHome", "documentation": "https://www.home-assistant.io/integrations/openhome", - "requirements": ["openhomedevice==0.7.2"], + "requirements": ["openhomedevice==2.0.1"], "codeowners": ["@bazwilliams"], "iot_class": "local_polling" } diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 7e333f7432b..26393dad179 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -1,7 +1,11 @@ """Support for Openhome Devices.""" +import asyncio +import functools import logging -from openhomedevice.Device import Device +import aiohttp +from async_upnp_client.client import UpnpError +from openhomedevice.device import Device import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity @@ -43,25 +47,45 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.info("Openhome device found: %s", name) device = await hass.async_add_executor_job(Device, description) + await device.init() # if device has already been discovered - if device.Uuid() in openhome_data: + if device.uuid() in openhome_data: return True entity = OpenhomeDevice(hass, device) async_add_entities([entity]) - openhome_data.add(device.Uuid()) + openhome_data.add(device.uuid()) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_INVOKE_PIN, {vol.Required(ATTR_PIN_INDEX): cv.positive_int}, - "invoke_pin", + "async_invoke_pin", ) +def catch_request_errors(): + """Catch asyncio.TimeoutError, aiohttp.ClientError, UpnpError errors.""" + + def call_wrapper(func): + """Call wrapper for decorator.""" + + @functools.wraps(func) + async def wrapper(self, *args, **kwargs): + """Catch asyncio.TimeoutError, aiohttp.ClientError, UpnpError errors.""" + try: + return await func(self, *args, **kwargs) + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + _LOGGER.error("Error during call %s", func.__name__) + + return wrapper + + return call_wrapper + + class OpenhomeDevice(MediaPlayerEntity): """Representation of an Openhome device.""" @@ -80,64 +104,80 @@ class OpenhomeDevice(MediaPlayerEntity): self._source = {} self._name = None self._state = STATE_PLAYING + self._available = True - def update(self): + @property + def available(self): + """Device is available.""" + return self._available + + async def async_update(self): """Update state of device.""" - self._in_standby = self._device.IsInStandby() - self._transport_state = self._device.TransportState() - self._track_information = self._device.TrackInfo() - self._source = self._device.Source() - self._name = self._device.Room().decode("utf-8") - self._supported_features = SUPPORT_OPENHOME - source_index = {} - source_names = [] + try: + self._in_standby = await self._device.is_in_standby() + self._transport_state = await self._device.transport_state() + self._track_information = await self._device.track_info() + self._source = await self._device.source() + self._name = await self._device.room() + self._supported_features = SUPPORT_OPENHOME + source_index = {} + source_names = [] - if self._device.VolumeEnabled(): - self._supported_features |= ( - SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET - ) - self._volume_level = self._device.VolumeLevel() / 100.0 - self._volume_muted = self._device.IsMuted() + if self._device.volume_enabled: + self._supported_features |= ( + SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET + ) + self._volume_level = await self._device.volume() / 100.0 + self._volume_muted = await self._device.is_muted() - for source in self._device.Sources(): - source_names.append(source["name"]) - source_index[source["name"]] = source["index"] + for source in await self._device.sources(): + source_names.append(source["name"]) + source_index[source["name"]] = source["index"] - self._source_index = source_index - self._source_names = source_names + self._source_index = source_index + self._source_names = source_names - if self._source["type"] == "Radio": - self._supported_features |= SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA - if self._source["type"] in ("Playlist", "Spotify"): - self._supported_features |= ( - SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - ) + if self._source["type"] == "Radio": + self._supported_features |= ( + SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA + ) + if self._source["type"] in ("Playlist", "Spotify"): + self._supported_features |= ( + SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + ) - if self._in_standby: - self._state = STATE_OFF - elif self._transport_state == "Paused": - self._state = STATE_PAUSED - elif self._transport_state in ("Playing", "Buffering"): - self._state = STATE_PLAYING - elif self._transport_state == "Stopped": - self._state = STATE_IDLE - else: - # Device is playing an external source with no transport controls - self._state = STATE_PLAYING + if self._in_standby: + self._state = STATE_OFF + elif self._transport_state == "Paused": + self._state = STATE_PAUSED + elif self._transport_state in ("Playing", "Buffering"): + self._state = STATE_PLAYING + elif self._transport_state == "Stopped": + self._state = STATE_IDLE + else: + # Device is playing an external source with no transport controls + self._state = STATE_PLAYING - def turn_on(self): + self._available = True + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + self._available = False + + @catch_request_errors() + async def async_turn_on(self): """Bring device out of standby.""" - self._device.SetStandby(False) + await self._device.set_standby(False) - def turn_off(self): + @catch_request_errors() + async def async_turn_off(self): """Put device in standby.""" - self._device.SetStandby(True) + await self._device.set_standby(True) - def play_media(self, media_type, media_id, **kwargs): + @catch_request_errors() + async def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" if media_type != MEDIA_TYPE_MUSIC: _LOGGER.error( @@ -147,35 +187,48 @@ class OpenhomeDevice(MediaPlayerEntity): ) return track_details = {"title": "Home Assistant", "uri": media_id} - self._device.PlayMedia(track_details) + await self._device.play_media(track_details) - def media_pause(self): + @catch_request_errors() + async def async_media_pause(self): """Send pause command.""" - self._device.Pause() + await self._device.pause() - def media_stop(self): + @catch_request_errors() + async def async_media_stop(self): """Send stop command.""" - self._device.Stop() + await self._device.stop() - def media_play(self): + @catch_request_errors() + async def async_media_play(self): """Send play command.""" - self._device.Play() + await self._device.play() - def media_next_track(self): + @catch_request_errors() + async def async_media_next_track(self): """Send next track command.""" - self._device.Skip(1) + await self._device.skip(1) - def media_previous_track(self): + @catch_request_errors() + async def async_media_previous_track(self): """Send previous track command.""" - self._device.Skip(-1) + await self._device.skip(-1) - def select_source(self, source): + @catch_request_errors() + async def async_select_source(self, source): """Select input source.""" - self._device.SetSource(self._source_index[source]) + await self._device.set_source(self._source_index[source]) - def invoke_pin(self, pin): + @catch_request_errors() + async def async_invoke_pin(self, pin): """Invoke pin.""" - self._device.InvokePin(pin) + try: + if self._device.pins_enabled: + await self._device.invoke_pin(pin) + else: + _LOGGER.error("Pins service not supported") + except (UpnpError): + _LOGGER.error("Error invoking pin %s", pin) @property def name(self): @@ -190,7 +243,7 @@ class OpenhomeDevice(MediaPlayerEntity): @property def unique_id(self): """Return a unique ID.""" - return self._device.Uuid() + return self._device.uuid() @property def state(self): @@ -239,18 +292,22 @@ class OpenhomeDevice(MediaPlayerEntity): """Return true if volume is muted.""" return self._volume_muted - def volume_up(self): + @catch_request_errors() + async def async_volume_up(self): """Volume up media player.""" - self._device.IncreaseVolume() + await self._device.increase_volume() - def volume_down(self): + @catch_request_errors() + async def async_volume_down(self): """Volume down media player.""" - self._device.DecreaseVolume() + await self._device.decrease_volume() - def set_volume_level(self, volume): + @catch_request_errors() + async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" - self._device.SetVolumeLevel(int(volume * 100)) + await self._device.set_volume(int(volume * 100)) - def mute_volume(self, mute): + @catch_request_errors() + async def async_mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" - self._device.SetMute(mute) + await self._device.set_mute(mute) diff --git a/requirements_all.txt b/requirements_all.txt index fb30eb5bd68..98774e9a9ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1076,7 +1076,7 @@ openerz-api==0.1.0 openevsewifi==1.1.0 # homeassistant.components.openhome -openhomedevice==0.7.2 +openhomedevice==2.0.1 # homeassistant.components.opensensemap opensensemap-api==0.1.5 From daff62f42d14fb4c04feae9bfe92bc6cf68da61b Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 26 May 2021 18:46:06 +0200 Subject: [PATCH 783/852] Add AsusWRT model and firmware information for device (#51102) --- homeassistant/components/asuswrt/router.py | 24 +++++++++++++++++++++- tests/components/asuswrt/test_sensor.py | 7 +++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index f82bf74e4a3..0912869abb7 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -195,6 +195,8 @@ class AsusWrtRouter: self._api: AsusWrt = None self._protocol = entry.data[CONF_PROTOCOL] self._host = entry.data[CONF_HOST] + self._model = "Asus Router" + self._sw_v = None self._devices: dict[str, Any] = {} self._connected_devices = 0 @@ -224,6 +226,14 @@ class AsusWrtRouter: if not self._api.is_connected: raise ConfigEntryNotReady + # System + model = await _get_nvram_info(self._api, "MODEL") + if model: + self._model = model["model"] + firmware = await _get_nvram_info(self._api, "FIRMWARE") + if firmware: + self._sw_v = f"{firmware['firmver']} (build {firmware['buildno']})" + # Load tracked entities from registry entity_registry = await self.hass.helpers.entity_registry.async_get_registry() track_entries = ( @@ -373,8 +383,9 @@ class AsusWrtRouter: return { "identifiers": {(DOMAIN, "AsusWRT")}, "name": self._host, - "model": "Asus Router", + "model": self._model, "manufacturer": "Asus", + "sw_version": self._sw_v, } @property @@ -408,6 +419,17 @@ class AsusWrtRouter: return self._api +async def _get_nvram_info(api: AsusWrt, info_type): + """Get AsusWrt router info from nvram.""" + info = {} + try: + info = await api.async_get_nvram(info_type) + except OSError as exc: + _LOGGER.warning("Error calling method async_get_nvram(%s): %s", info_type, exc) + + return info + + def get_api(conf: dict, options: dict | None = None) -> AsusWrt: """Get the AsusWrt API.""" opt = options or {} diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 60ce6b1aa68..87c3fadb978 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -56,6 +56,13 @@ def mock_controller_connect(mock_devices): service_mock.return_value.connection.async_connect = AsyncMock() service_mock.return_value.is_connected = True service_mock.return_value.connection.disconnect = Mock() + service_mock.return_value.async_get_nvram = AsyncMock( + return_value={ + "model": "abcd", + "firmver": "efg", + "buildno": "123", + } + ) service_mock.return_value.async_get_connected_devices = AsyncMock( return_value=mock_devices ) From 67536b52c4bdbcb291e9699c9243db460bffc9a1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 18:47:04 +0200 Subject: [PATCH 784/852] Use entity class vars in Flo (#50991) --- homeassistant/components/flo/binary_sensor.py | 14 +--- homeassistant/components/flo/entity.py | 28 ++----- homeassistant/components/flo/sensor.py | 78 +++++-------------- 3 files changed, 28 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py index bd623aa38bb..88675e571e7 100644 --- a/homeassistant/components/flo/binary_sensor.py +++ b/homeassistant/components/flo/binary_sensor.py @@ -37,6 +37,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity): """Binary sensor that reports on if there are any pending system alerts.""" + _attr_device_class = DEVICE_CLASS_PROBLEM + def __init__(self, device): """Initialize the pending alerts binary sensor.""" super().__init__("pending_system_alerts", "Pending System Alerts", device) @@ -57,15 +59,12 @@ class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity): "critical": self._device.pending_critical_alerts_count, } - @property - def device_class(self): - """Return the device class for the binary sensor.""" - return DEVICE_CLASS_PROBLEM - class FloWaterDetectedBinarySensor(FloEntity, BinarySensorEntity): """Binary sensor that reports if water is detected (for leak detectors).""" + _attr_device_class = DEVICE_CLASS_PROBLEM + def __init__(self, device): """Initialize the pending alerts binary sensor.""" super().__init__("water_detected", "Water Detected", device) @@ -74,8 +73,3 @@ class FloWaterDetectedBinarySensor(FloEntity, BinarySensorEntity): def is_on(self): """Return true if the Flo device is detecting water.""" return self._device.water_detected - - @property - def device_class(self): - """Return the device class for the binary sensor.""" - return DEVICE_CLASS_PROBLEM diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 94683e2cb20..9f0e8029888 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -13,6 +13,9 @@ from .device import FloDeviceDataUpdateCoordinator class FloEntity(Entity): """A base class for Flo entities.""" + _attr_force_update = False + _attr_should_poll = False + def __init__( self, entity_type: str, @@ -21,21 +24,12 @@ class FloEntity(Entity): **kwargs, ) -> None: """Init Flo entity.""" - self._unique_id: str = f"{device.mac_address}_{entity_type}" - self._name: str = name + self._attr_name = name + self._attr_unique_id = f"{device.mac_address}_{entity_type}" + self._device: FloDeviceDataUpdateCoordinator = device self._state: Any = None - @property - def name(self) -> str: - """Return Entity's default name.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id - @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" @@ -53,16 +47,6 @@ class FloEntity(Entity): """Return True if device is available.""" return self._device.available - @property - def force_update(self) -> bool: - """Force update this entity.""" - return False - - @property - def should_poll(self) -> bool: - """Poll state from device.""" - return False - async def async_update(self): """Update Flo entity.""" await self._device.async_request_refresh() diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index 1e362e75f8c..0504d451e14 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -60,16 +60,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class FloDailyUsageSensor(FloEntity, SensorEntity): """Monitors the daily water usage.""" + _attr_icon = WATER_ICON + _attr_unit_of_measurement = VOLUME_GALLONS + def __init__(self, device): """Initialize the daily water usage sensor.""" super().__init__("daily_consumption", NAME_DAILY_USAGE, device) self._state: float = None - @property - def icon(self) -> str: - """Return the daily usage icon.""" - return WATER_ICON - @property def state(self) -> float | None: """Return the current daily usage.""" @@ -77,11 +75,6 @@ class FloDailyUsageSensor(FloEntity, SensorEntity): return None return round(self._device.consumption_today, 1) - @property - def unit_of_measurement(self) -> str: - """Return gallons as the unit measurement for water.""" - return VOLUME_GALLONS - class FloSystemModeSensor(FloEntity, SensorEntity): """Monitors the current Flo system mode.""" @@ -102,16 +95,14 @@ class FloSystemModeSensor(FloEntity, SensorEntity): class FloCurrentFlowRateSensor(FloEntity, SensorEntity): """Monitors the current water flow rate.""" + _attr_icon = GAUGE_ICON + _attr_unit_of_measurement = "gpm" + def __init__(self, device): """Initialize the flow rate sensor.""" super().__init__("current_flow_rate", NAME_FLOW_RATE, device) self._state: float = None - @property - def icon(self) -> str: - """Return the daily usage icon.""" - return GAUGE_ICON - @property def state(self) -> float | None: """Return the current flow rate.""" @@ -119,15 +110,13 @@ class FloCurrentFlowRateSensor(FloEntity, SensorEntity): return None return round(self._device.current_flow_rate, 1) - @property - def unit_of_measurement(self) -> str: - """Return the unit measurement.""" - return "gpm" - class FloTemperatureSensor(FloEntity, SensorEntity): """Monitors the temperature.""" + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_unit_of_measurement = TEMP_FAHRENHEIT + def __init__(self, name, device): """Initialize the temperature sensor.""" super().__init__("temperature", name, device) @@ -140,20 +129,13 @@ class FloTemperatureSensor(FloEntity, SensorEntity): return None return round(self._device.temperature, 1) - @property - def unit_of_measurement(self) -> str: - """Return fahrenheit as the unit measurement for temperature.""" - return TEMP_FAHRENHEIT - - @property - def device_class(self) -> str | None: - """Return the device class for this sensor.""" - return DEVICE_CLASS_TEMPERATURE - class FloHumiditySensor(FloEntity, SensorEntity): """Monitors the humidity.""" + _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_unit_of_measurement = PERCENTAGE + def __init__(self, device): """Initialize the humidity sensor.""" super().__init__("humidity", NAME_HUMIDITY, device) @@ -166,20 +148,13 @@ class FloHumiditySensor(FloEntity, SensorEntity): return None return round(self._device.humidity, 1) - @property - def unit_of_measurement(self) -> str: - """Return percent as the unit measurement for humidity.""" - return PERCENTAGE - - @property - def device_class(self) -> str | None: - """Return the device class for this sensor.""" - return DEVICE_CLASS_HUMIDITY - class FloPressureSensor(FloEntity, SensorEntity): """Monitors the water pressure.""" + _attr_device_class = DEVICE_CLASS_PRESSURE + _attr_unit_of_measurement = PRESSURE_PSI + def __init__(self, device): """Initialize the pressure sensor.""" super().__init__("water_pressure", NAME_WATER_PRESSURE, device) @@ -192,20 +167,13 @@ class FloPressureSensor(FloEntity, SensorEntity): return None return round(self._device.current_psi, 1) - @property - def unit_of_measurement(self) -> str: - """Return gallons as the unit measurement for water.""" - return PRESSURE_PSI - - @property - def device_class(self) -> str | None: - """Return the device class for this sensor.""" - return DEVICE_CLASS_PRESSURE - class FloBatterySensor(FloEntity, SensorEntity): """Monitors the battery level for battery-powered leak detectors.""" + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + def __init__(self, device): """Initialize the battery sensor.""" super().__init__("battery", NAME_BATTERY, device) @@ -215,13 +183,3 @@ class FloBatterySensor(FloEntity, SensorEntity): def state(self) -> float | None: """Return the current battery level.""" return self._device.battery_level - - @property - def unit_of_measurement(self) -> str: - """Return percentage as the unit measurement for battery.""" - return PERCENTAGE - - @property - def device_class(self) -> str | None: - """Return the device class for this sensor.""" - return DEVICE_CLASS_BATTERY From 22dd7df66cacbead9ccb433878787700af9e2be0 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Wed, 26 May 2021 09:48:44 -0700 Subject: [PATCH 785/852] Improve totalconnect config flow user experience (#47926) --- homeassistant/components/totalconnect/config_flow.py | 12 +++++++----- homeassistant/components/totalconnect/strings.json | 4 ++-- .../components/totalconnect/translations/en.json | 4 ++-- tests/components/totalconnect/test_config_flow.py | 6 +++--- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 122b1ad88b4..8f39346f47e 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -65,10 +65,10 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.usercodes[location_id] is None: valid = await self.hass.async_add_executor_job( self.client.locations[location_id].set_usercode, - user_entry[CONF_LOCATION], + user_entry[CONF_USERCODES], ) if valid: - self.usercodes[location_id] = user_entry[CONF_LOCATION] + self.usercodes[location_id] = user_entry[CONF_USERCODES] else: errors[CONF_LOCATION] = "usercode" break @@ -93,12 +93,14 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # show the next location that needs a usercode location_codes = {} + location_for_user = "" for location_id in self.usercodes: if self.usercodes[location_id] is None: + location_for_user = location_id location_codes[ vol.Required( - CONF_LOCATION, - default=location_id, + CONF_USERCODES, + default="0000", ) ] = str break @@ -108,7 +110,7 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="locations", data_schema=data_schema, errors=errors, - description_placeholders={"base": "description"}, + description_placeholders={"location_id": location_for_user}, ) async def async_step_reauth(self, config): diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index f284e4b86da..5c32d19b348 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -10,9 +10,9 @@ }, "locations": { "title": "Location Usercodes", - "description": "Enter the usercode for this user at this location", + "description": "Enter the usercode for this user at location {location_id}", "data": { - "location": "[%key:common::config_flow::data::location%]" + "usercode": "Usercode" } }, "reauth_confirm": { diff --git a/homeassistant/components/totalconnect/translations/en.json b/homeassistant/components/totalconnect/translations/en.json index 5071e623701..02ea1bfccbd 100644 --- a/homeassistant/components/totalconnect/translations/en.json +++ b/homeassistant/components/totalconnect/translations/en.json @@ -11,9 +11,9 @@ "step": { "locations": { "data": { - "location": "Location" + "usercode": "Usercode" }, - "description": "Enter the usercode for this user at this location", + "description": "Enter the usercode for this user at location {location_id}", "title": "Location Usercodes" }, "reauth_confirm": { diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 2f89beab0e0..3751abfc361 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant import data_entry_flow -from homeassistant.components.totalconnect.const import CONF_LOCATION, DOMAIN +from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD @@ -67,7 +67,7 @@ async def test_user_show_locations(hass): # user enters an invalid usercode result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_LOCATION: "bad"}, + user_input={CONF_USERCODES: "bad"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "locations" @@ -77,7 +77,7 @@ async def test_user_show_locations(hass): # user enters a valid usercode result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], - user_input={CONF_LOCATION: "7890"}, + user_input={CONF_USERCODES: "7890"}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY # client should have sent another request to validate usercode From 09b9218511413d22b1a88f18bc789d1f2d485388 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Wed, 26 May 2021 17:54:02 +0100 Subject: [PATCH 786/852] Handle updating config entries in Vera (#49605) Co-authored-by: Paulus Schoutsen --- homeassistant/components/vera/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index bf5eb9182e6..096c6a8aa15 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -159,6 +159,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) ) + config_entry.async_on_unload( + config_entry.add_update_listener(_async_update_listener) + ) return True @@ -176,6 +179,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + def map_vera_device(vera_device: veraApi.VeraDevice, remap: list[int]) -> str: """Map vera classes to Home Assistant types.""" From 762f15a0d3e301e4829b438dcf5ade5490e03442 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 19:44:48 +0200 Subject: [PATCH 787/852] Bumped version to 2021.6.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 00e06f1e8d0..581c08c5f94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 255577436e614425906dee46deae051cd2a098fa Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 27 May 2021 10:55:47 +0200 Subject: [PATCH 788/852] Followup PR for SIA integration (#51108) * Updates based on Martin's review * fix strings and cleaned up constants --- .../components/sia/alarm_control_panel.py | 62 +++++-------- homeassistant/components/sia/config_flow.py | 30 +++---- homeassistant/components/sia/const.py | 34 +++----- homeassistant/components/sia/hub.py | 24 +++-- homeassistant/components/sia/manifest.json | 2 +- homeassistant/components/sia/strings.json | 2 +- homeassistant/components/sia/utils.py | 87 +++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 114 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 9d5f62b02de..fe5b95b639e 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -2,18 +2,14 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any from pysiaalarm import SIAEvent -from homeassistant.components.alarm_control_panel import ( - ENTITY_ID_FORMAT as ALARM_ENTITY_ID_FORMAT, - AlarmControlPanelEntity, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PORT, - CONF_ZONE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_NIGHT, @@ -21,8 +17,10 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType @@ -33,7 +31,6 @@ from .const import ( CONF_PING_INTERVAL, CONF_ZONES, DOMAIN, - SIA_ENTITY_ID_FORMAT, SIA_EVENT, SIA_NAME_FORMAT, SIA_UNIQUE_ID_FORMAT_ALARM, @@ -76,21 +73,17 @@ CODE_CONSEQUENCES: dict[str, StateType] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[..., None], -) -> bool: + async_add_entities: AddEntitiesCallback, +) -> None: """Set up SIA alarm_control_panel(s) from a config entry.""" async_add_entities( - [ - SIAAlarmControlPanel(entry, account_data, zone) - for account_data in entry.data[CONF_ACCOUNTS] - for zone in range( - 1, - entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES] - + 1, - ) - ] + SIAAlarmControlPanel(entry, account_data, zone) + for account_data in entry.data[CONF_ACCOUNTS] + for zone in range( + 1, + entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES] + 1, + ) ) - return True class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): @@ -111,18 +104,7 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): self._account: str = self._account_data[CONF_ACCOUNT] self._ping_interval: int = self._account_data[CONF_PING_INTERVAL] - self.entity_id: str = ALARM_ENTITY_ID_FORMAT.format( - SIA_ENTITY_ID_FORMAT.format( - self._port, self._account, self._zone, DEVICE_CLASS_ALARM - ) - ) - - self._attr: dict[str, Any] = { - CONF_PORT: self._port, - CONF_ACCOUNT: self._account, - CONF_ZONE: self._zone, - CONF_PING_INTERVAL: f"{self._ping_interval} minute(s)", - } + self._attr: dict[str, Any] = {} self._available: bool = True self._state: StateType = None @@ -134,16 +116,17 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): Overridden from Entity. - 1. start the event listener and add the callback to on_remove + 1. register the dispatcher and add the callback to on_remove 2. get previous state from storage 3. if previous state: restore 4. if previous state is unavailable: set _available to False and return 5. if available: create availability cb """ self.async_on_remove( - self.hass.bus.async_listen( - event_type=SIA_EVENT.format(self._port, self._account), - listener=self.async_handle_event, + async_dispatcher_connect( + self.hass, + SIA_EVENT.format(self._port, self._account), + self.async_handle_event, ) ) last_state = await self.async_get_last_state() @@ -162,14 +145,11 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): if self._cancel_availability_cb: self._cancel_availability_cb() - async def async_handle_event(self, event: Event) -> None: - """Listen to events for this port and account and update state and attributes. + async def async_handle_event(self, sia_event: SIAEvent) -> None: + """Listen to dispatcher events for this port and account and update state and attributes. If the port and account combo receives any message it means it is online and can therefore be set to available. """ - sia_event: SIAEvent = SIAEvent.from_dict( # pylint: disable=no-member - event.data - ) _LOGGER.debug("Received event: %s", sia_event) if int(sia_event.ri) == self._zone: self._attr.update(get_attr_from_sia_event(sia_event)) diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index fe49ec65777..a9b49765c19 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -18,6 +18,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType from .const import ( @@ -104,7 +105,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data: ConfigType = {} self._options: Mapping[str, Any] = {CONF_ACCOUNTS: {}} - async def async_step_user(self, user_input: ConfigType = None): + async def async_step_user(self, user_input: ConfigType = None) -> FlowResult: """Handle the initial user step.""" errors: dict[str, str] | None = None if user_input is not None: @@ -115,7 +116,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_handle_data_and_route(user_input) - async def async_step_add_account(self, user_input: ConfigType = None): + async def async_step_add_account(self, user_input: ConfigType = None) -> FlowResult: """Handle the additional accounts steps.""" errors: dict[str, str] | None = None if user_input is not None: @@ -126,11 +127,11 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_handle_data_and_route(user_input) - async def async_handle_data_and_route(self, user_input: ConfigType): + async def async_handle_data_and_route(self, user_input: ConfigType) -> FlowResult: """Handle the user_input, check if configured and route to the right next step or create entry.""" self._update_data(user_input) - if self._data and self._port_already_configured(): - return self.async_abort(reason="already_configured") + + self._async_abort_entries_match({CONF_PORT: self._data[CONF_PORT]}) if user_input[CONF_ADDITIONAL_ACCOUNTS]: return await self.async_step_add_account() @@ -163,13 +164,6 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._options[CONF_ACCOUNTS].setdefault(account, deepcopy(DEFAULT_OPTIONS)) self._options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] - def _port_already_configured(self): - """See if we already have a SIA entry matching the port.""" - for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_PORT] == self._data[CONF_PORT]: - return True - return False - class SIAOptionsFlowHandler(config_entries.OptionsFlow): """Handle SIA options.""" @@ -181,14 +175,15 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.hub: SIAHub | None = None self.accounts_todo: list = [] - async def async_step_init(self, user_input: ConfigType = None): + async def async_step_init(self, user_input: ConfigType = None) -> FlowResult: """Manage the SIA options.""" self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id] - if self.hub is not None and self.hub.sia_accounts is not None: - self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] - return await self.async_step_options() + assert self.hub is not None + assert self.hub.sia_accounts is not None + self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] + return await self.async_step_options() - async def async_step_options(self, user_input: ConfigType = None): + async def async_step_options(self, user_input: ConfigType = None) -> FlowResult: """Create the options step for a account.""" errors: dict[str, str] | None = None if user_input is not None: @@ -223,7 +218,6 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] if self.accounts_todo: return await self.async_step_options() - _LOGGER.warning("Updating SIA Options with %s", self.options) return self.async_create_entry(title="", data=self.options) @property diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py index ceeaac75923..916cdb9621c 100644 --- a/homeassistant/components/sia/const.py +++ b/homeassistant/components/sia/const.py @@ -5,34 +5,24 @@ from homeassistant.components.alarm_control_panel import ( PLATFORMS = [ALARM_CONTROL_PANEL_DOMAIN] +DOMAIN = "sia" + +ATTR_CODE = "last_code" +ATTR_ZONE = "zone" +ATTR_MESSAGE = "last_message" +ATTR_ID = "last_id" +ATTR_TIMESTAMP = "last_timestamp" + +TITLE = "SIA Alarm on port {}" CONF_ACCOUNT = "account" CONF_ACCOUNTS = "accounts" CONF_ADDITIONAL_ACCOUNTS = "additional_account" -CONF_PING_INTERVAL = "ping_interval" CONF_ENCRYPTION_KEY = "encryption_key" -CONF_ZONES = "zones" CONF_IGNORE_TIMESTAMPS = "ignore_timestamps" +CONF_PING_INTERVAL = "ping_interval" +CONF_ZONES = "zones" -DOMAIN = "sia" -TITLE = "SIA Alarm on port {}" -SIA_EVENT = "sia_event_{}_{}" SIA_NAME_FORMAT = "{} - {} - zone {} - {}" -SIA_NAME_FORMAT_HUB = "{} - {} - {}" -SIA_ENTITY_ID_FORMAT = "{}_{}_{}_{}" -SIA_ENTITY_ID_FORMAT_HUB = "{}_{}_{}" SIA_UNIQUE_ID_FORMAT_ALARM = "{}_{}_{}" -SIA_UNIQUE_ID_FORMAT = "{}_{}_{}_{}" -HUB_SENSOR_NAME = "last_heartbeat" -HUB_ZONE = 0 -PING_INTERVAL_MARGIN = 30 -DEFAULT_TIMEBAND = (80, 40) -IGNORED_TIMEBAND = (3600, 1800) - -EVENT_CODE = "last_code" -EVENT_ACCOUNT = "account" -EVENT_ZONE = "zone" -EVENT_PORT = "port" -EVENT_MESSAGE = "last_message" -EVENT_ID = "last_id" -EVENT_TIMESTAMP = "last_timestamp" +SIA_EVENT = "sia_event_{}_{}" diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index e5dc7b85ed8..387c2273606 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -9,8 +9,9 @@ from pysiaalarm.aio import CommunicationsProtocol, SIAAccount, SIAClient, SIAEve from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PORT, CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, EventOrigin, HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_ACCOUNT, @@ -18,16 +19,19 @@ from .const import ( CONF_ENCRYPTION_KEY, CONF_IGNORE_TIMESTAMPS, CONF_ZONES, - DEFAULT_TIMEBAND, DOMAIN, - IGNORED_TIMEBAND, PLATFORMS, SIA_EVENT, ) +from .utils import get_event_data_from_sia_event _LOGGER = logging.getLogger(__name__) +DEFAULT_TIMEBAND = (80, 40) +IGNORED_TIMEBAND = (3600, 1800) + + class SIAHub: """Class for SIA Hubs.""" @@ -39,7 +43,7 @@ class SIAHub: """Create the SIAHub.""" self._hass: HomeAssistant = hass self._entry: ConfigEntry = entry - self._port: int = int(entry.data[CONF_PORT]) + self._port: int = entry.data[CONF_PORT] self._title: str = entry.title self._accounts: list[dict[str, Any]] = deepcopy(entry.data[CONF_ACCOUNTS]) self._protocol: str = entry.data[CONF_PROTOCOL] @@ -69,21 +73,23 @@ class SIAHub: await self.sia_client.stop() async def async_create_and_fire_event(self, event: SIAEvent) -> None: - """Create a event on HA's bus, with the data from the SIAEvent. + """Create a event on HA dispatcher and then on HA's bus, with the data from the SIAEvent. The created event is handled by default for only a small subset for each platform (there are about 320 SIA Codes defined, only 22 of those are used in the alarm_control_panel), a user can choose to build other automation or even entities on the same event for SIA codes not handled by the built-in platforms. """ _LOGGER.debug( - "Adding event to bus for code %s for port %s and account %s", + "Adding event to dispatch and bus for code %s for port %s and account %s", event.code, self._port, event.account, ) + async_dispatcher_send( + self._hass, SIA_EVENT.format(self._port, event.account), event + ) self._hass.bus.async_fire( event_type=SIA_EVENT.format(self._port, event.account), - event_data=event.to_dict(encode_json=True), - origin=EventOrigin.remote, + event_data=get_event_data_from_sia_event(event), ) def update_accounts(self): @@ -115,7 +121,7 @@ class SIAHub: options = dict(self._entry.options) for acc in self._accounts: acc_id = acc[CONF_ACCOUNT] - if acc_id in options[CONF_ACCOUNTS].keys(): + if acc_id in options[CONF_ACCOUNTS]: acc[CONF_IGNORE_TIMESTAMPS] = options[CONF_ACCOUNTS][acc_id][ CONF_IGNORE_TIMESTAMPS ] diff --git a/homeassistant/components/sia/manifest.json b/homeassistant/components/sia/manifest.json index 67c2a0e91a1..eaeb4547167 100644 --- a/homeassistant/components/sia/manifest.json +++ b/homeassistant/components/sia/manifest.json @@ -3,7 +3,7 @@ "name": "SIA Alarm Systems", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sia", - "requirements": ["pysiaalarm==3.0.0b12"], + "requirements": ["pysiaalarm==3.0.0"], "codeowners": ["@eavanvalkenburg"], "iot_class": "local_push" } diff --git a/homeassistant/components/sia/strings.json b/homeassistant/components/sia/strings.json index b091fdd341d..f837d41056a 100644 --- a/homeassistant/components/sia/strings.json +++ b/homeassistant/components/sia/strings.json @@ -27,7 +27,7 @@ }, "error": { "invalid_key_format": "The key is not a hex value, please use only 0-9 and A-F.", - "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 characters hex characters.", + "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 hex characters.", "invalid_account_format": "The account is not a hex value, please use only 0-9 and A-F.", "invalid_account_length": "The account is not the right length, it has to be between 3 and 16 characters.", "invalid_ping": "The ping interval needs to be between 1 and 1440 minutes.", diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 9b02025aa8d..08e0fce8ab2 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -6,19 +6,9 @@ from typing import Any from pysiaalarm import SIAEvent -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from .const import ATTR_CODE, ATTR_ID, ATTR_MESSAGE, ATTR_TIMESTAMP, ATTR_ZONE -from .const import ( - EVENT_ACCOUNT, - EVENT_CODE, - EVENT_ID, - EVENT_MESSAGE, - EVENT_TIMESTAMP, - EVENT_ZONE, - HUB_SENSOR_NAME, - HUB_ZONE, - PING_INTERVAL_MARGIN, -) +PING_INTERVAL_MARGIN = 30 def get_unavailability_interval(ping: int) -> float: @@ -26,32 +16,55 @@ def get_unavailability_interval(ping: int) -> float: return timedelta(minutes=ping, seconds=PING_INTERVAL_MARGIN).total_seconds() -def get_name(port: int, account: str, zone: int, entity_type: str) -> str: - """Give back a entity_id and name according to the variables.""" - if zone == HUB_ZONE: - return f"{port} - {account} - {'Last Heartbeat' if entity_type == DEVICE_CLASS_TIMESTAMP else 'Power'}" - return f"{port} - {account} - zone {zone} - {entity_type}" - - -def get_entity_id(port: int, account: str, zone: int, entity_type: str) -> str: - """Give back a entity_id according to the variables.""" - if zone == HUB_ZONE: - return f"{port}_{account}_{HUB_SENSOR_NAME if entity_type == DEVICE_CLASS_TIMESTAMP else entity_type}" - return f"{port}_{account}_{zone}_{entity_type}" - - -def get_unique_id(entry_id: str, account: str, zone: int, domain: str) -> str: - """Return the unique id.""" - return f"{entry_id}_{account}_{zone}_{domain}" - - def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]: """Create the attributes dict from a SIAEvent.""" return { - EVENT_ACCOUNT: event.account, - EVENT_ZONE: event.ri, - EVENT_CODE: event.code, - EVENT_MESSAGE: event.message, - EVENT_ID: event.id, - EVENT_TIMESTAMP: event.timestamp, + ATTR_ZONE: event.ri, + ATTR_CODE: event.code, + ATTR_MESSAGE: event.message, + ATTR_ID: event.id, + ATTR_TIMESTAMP: event.timestamp.isoformat(), + } + + +def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: + """Create a dict from the SIA Event for the HA Event.""" + return { + "message_type": event.message_type, + "receiver": event.receiver, + "line": event.line, + "account": event.account, + "sequence": event.sequence, + "content": event.content, + "ti": event.ti, + "id": event.id, + "ri": event.ri, + "code": event.code, + "message": event.message, + "x_data": event.x_data, + "timestamp": event.timestamp.isoformat(), + "event_qualifier": event.qualifier, + "event_type": event.event_type, + "partition": event.partition, + "extended_data": [ + { + "identifier": xd.identifier, + "name": xd.name, + "description": xd.description, + "length": xd.length, + "characters": xd.characters, + "value": xd.value, + } + for xd in event.extended_data + ] + if event.extended_data is not None + else None, + "sia_code": { + "code": event.sia_code.code, + "type": event.sia_code.type, + "description": event.sia_code.description, + "concerns": event.sia_code.concerns, + } + if event.sia_code is not None + else None, } diff --git a/requirements_all.txt b/requirements_all.txt index 98774e9a9ef..cb3b05fed6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1723,7 +1723,7 @@ pysesame2==1.0.1 pysher==1.0.1 # homeassistant.components.sia -pysiaalarm==3.0.0b12 +pysiaalarm==3.0.0 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2aa89f3d53..148936e126e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -962,7 +962,7 @@ pyserial-asyncio==0.5 pyserial==3.5 # homeassistant.components.sia -pysiaalarm==3.0.0b12 +pysiaalarm==3.0.0 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 From c2c760eb8b89fd6cfabed8e15d77da0ed90036a7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 27 May 2021 00:27:35 -0400 Subject: [PATCH 789/852] Fix zwave_js.set_value schema (#51114) * fix zwave_js.set_value schema * wrap all schemas in vol.Schema * readd removed assertions --- homeassistant/components/zwave_js/services.py | 106 ++++++++++-------- tests/components/zwave_js/test_services.py | 15 +++ 2 files changed, 75 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 48719063376..c2ebe965fdd 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -67,22 +67,26 @@ class ZWaveServices: const.DOMAIN, const.SERVICE_SET_CONFIG_PARAMETER, self.async_set_config_parameter, - schema=vol.All( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( - vol.Coerce(int), cv.string - ), - vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( - vol.Coerce(int), BITMASK_SCHEMA - ), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), cv.string - ), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), - parameter_name_does_not_need_bitmask, + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( + vol.Coerce(int), cv.string + ), + vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( + vol.Coerce(int), BITMASK_SCHEMA + ), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), cv.string + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + parameter_name_does_not_need_bitmask, + ), ), ) @@ -90,21 +94,25 @@ class ZWaveServices: const.DOMAIN, const.SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, self.async_bulk_set_partial_config_parameters, - schema=vol.All( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), - { - vol.Any( - vol.Coerce(int), BITMASK_SCHEMA, cv.string - ): vol.Any(vol.Coerce(int), cv.string) - }, - ), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), + { + vol.Any( + vol.Coerce(int), BITMASK_SCHEMA, cv.string + ): vol.Any(vol.Coerce(int), cv.string) + }, + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), ), ) @@ -125,21 +133,27 @@ class ZWaveServices: const.SERVICE_SET_VALUE, self.async_set_value, schema=vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int), - vol.Required(const.ATTR_PROPERTY): vol.Any(vol.Coerce(int), str), - vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any( - vol.Coerce(int), str - ), - vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): vol.Any( - bool, vol.Coerce(int), vol.Coerce(float), cv.string - ), - vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int), + vol.Required(const.ATTR_PROPERTY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), + vol.Required(const.ATTR_VALUE): vol.Any( + bool, vol.Coerce(int), vol.Coerce(float), cv.string + ), + vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), ), ) diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 956361d3953..3c08c49a36f 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -599,6 +599,7 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): ) assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 5 @@ -619,3 +620,17 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): "value": 0, } assert args["value"] == 2 + + # Test missing device and entities keys + with pytest.raises(vol.MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + ATTR_WAIT_FOR_RESULT: True, + }, + blocking=True, + ) From 4ebc0d97bcec65caa4cee85bd9ca04b2c69a2dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 27 May 2021 06:04:05 +0200 Subject: [PATCH 790/852] Handle blank string in location name for mobile app (#51130) --- homeassistant/components/mobile_app/device_tracker.py | 4 +++- tests/components/mobile_app/test_device_tracker.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 1b006f69827..1deebf6b531 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -94,7 +94,9 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): @property def location_name(self): """Return a location name for the current location of the device.""" - return self._data.get(ATTR_LOCATION_NAME) + if location_name := self._data.get(ATTR_LOCATION_NAME): + return location_name + return None @property def name(self): diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index 164b90a5290..b755a0a8d09 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -48,6 +48,7 @@ async def test_sending_location(hass, create_registrations, webhook_client): "course": 6, "speed": 7, "vertical_accuracy": 8, + "location_name": "", }, }, ) @@ -82,7 +83,6 @@ async def test_restoring_location(hass, create_registrations, webhook_client): "course": 60, "speed": 70, "vertical_accuracy": 80, - "location_name": "bar", }, }, ) @@ -104,6 +104,7 @@ async def test_restoring_location(hass, create_registrations, webhook_client): assert state_1 is not state_2 assert state_2.name == "Test 1" + assert state_2.state == "not_home" assert state_2.attributes["source_type"] == "gps" assert state_2.attributes["latitude"] == 10 assert state_2.attributes["longitude"] == 20 From 74e397dc73a287651a68f82be655673ca5272e02 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 27 May 2021 00:12:43 -0500 Subject: [PATCH 791/852] Fix Sonos TV source attribute (#51131) --- homeassistant/components/sonos/speaker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 7ce51176a88..e827b35b16d 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -819,7 +819,9 @@ class SonosSpeaker: if variables and "transport_state" in variables: self.media.play_mode = variables["current_play_mode"] - track_uri = variables["enqueued_transport_uri"] + track_uri = ( + variables["enqueued_transport_uri"] or variables["current_track_uri"] + ) music_source = self.soco.music_source_from_uri(track_uri) else: self.media.play_mode = self.soco.play_mode From f3639c60e202e1a94490125391bdd8785dd33012 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 27 May 2021 03:53:51 -0500 Subject: [PATCH 792/852] Fix Sonos media position with radio sources (#51137) --- homeassistant/components/sonos/speaker.py | 25 +++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index e827b35b16d..c1f1dbb9104 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -844,7 +844,8 @@ class SonosSpeaker: if music_source == MUSIC_SRC_RADIO: self.update_media_radio(variables) else: - self.update_media_music(update_position, track_info) + self.update_media_music(track_info) + self.update_media_position(update_position, track_info) self.write_entity_states() @@ -907,11 +908,25 @@ class SonosSpeaker: if fav.reference.get_uri() == media_info["uri"]: self.media.source_name = fav.title - def update_media_music(self, update_media_position: bool, track_info: dict) -> None: + def update_media_music(self, track_info: dict) -> None: + """Update state when playing music tracks.""" + self.media.image_url = track_info.get("album_art") + + playlist_position = int(track_info.get("playlist_position")) # type: ignore + if playlist_position > 0: + self.media.queue_position = playlist_position - 1 + + def update_media_position( + self, update_media_position: bool, track_info: dict + ) -> None: """Update state when playing music tracks.""" self.media.duration = _timespan_secs(track_info.get("duration")) current_position = _timespan_secs(track_info.get("position")) + if self.media.duration == 0: + self.media.clear_position() + return + # player started reporting position? if current_position is not None and self.media.position is None: update_media_position = True @@ -935,9 +950,3 @@ class SonosSpeaker: elif update_media_position: self.media.position = current_position self.media.position_updated_at = dt_util.utcnow() - - self.media.image_url = track_info.get("album_art") - - playlist_position = int(track_info.get("playlist_position")) # type: ignore - if playlist_position > 0: - self.media.queue_position = playlist_position - 1 From b92db104dcefa5549fc0761a654fcb93adbbdfe9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 May 2021 11:01:28 +0200 Subject: [PATCH 793/852] Add deprecated backwards compatible history.LazyState (#51144) --- homeassistant/components/history/__init__.py | 20 +++--- homeassistant/helpers/deprecation.py | 72 +++++++++++++------- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index c92718a87e4..ac8a13e69ad 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -13,8 +13,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import history -from homeassistant.components.recorder.models import States +from homeassistant.components.recorder import history, models as history_models from homeassistant.components.recorder.statistics import statistics_during_period from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( @@ -26,7 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.deprecation import deprecated_function +from homeassistant.helpers.deprecation import deprecated_class, deprecated_function from homeassistant.helpers.entityfilter import ( CONF_ENTITY_GLOBS, INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -110,6 +109,11 @@ async def async_setup(hass, config): return True +@deprecated_class("homeassistant.components.recorder.models.LazyState") +class LazyState(history_models.LazyState): + """A lazy version of core State.""" + + @websocket_api.websocket_command( { vol.Required("type"): "history/statistics_during_period", @@ -345,17 +349,17 @@ class Filters: """Generate the entity filter query.""" includes = [] if self.included_domains: - includes.append(States.domain.in_(self.included_domains)) + includes.append(history_models.States.domain.in_(self.included_domains)) if self.included_entities: - includes.append(States.entity_id.in_(self.included_entities)) + includes.append(history_models.States.entity_id.in_(self.included_entities)) for glob in self.included_entity_globs: includes.append(_glob_to_like(glob)) excludes = [] if self.excluded_domains: - excludes.append(States.domain.in_(self.excluded_domains)) + excludes.append(history_models.States.domain.in_(self.excluded_domains)) if self.excluded_entities: - excludes.append(States.entity_id.in_(self.excluded_entities)) + excludes.append(history_models.States.entity_id.in_(self.excluded_entities)) for glob in self.excluded_entity_globs: excludes.append(_glob_to_like(glob)) @@ -373,7 +377,7 @@ class Filters: def _glob_to_like(glob_str): """Translate glob to sql.""" - return States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) + return history_models.States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) def _entities_may_have_state_changes_after( diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 06f09327dc9..adf3d8a5d88 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -80,6 +80,23 @@ def get_deprecated( return config.get(new_name, default) +def deprecated_class(replacement: str) -> Any: + """Mark class as deprecated and provide a replacement class to be used instead.""" + + def deprecated_decorator(cls: Any) -> Any: + """Decorate class as deprecated.""" + + @functools.wraps(cls) + def deprecated_cls(*args: tuple, **kwargs: dict[str, Any]) -> Any: + """Wrap for the original class.""" + _print_deprecation_warning(cls, replacement, "class") + return cls(*args, **kwargs) + + return deprecated_cls + + return deprecated_decorator + + def deprecated_function(replacement: str) -> Callable[..., Callable]: """Mark function as deprecated and provide a replacement function to be used instead.""" @@ -89,32 +106,39 @@ def deprecated_function(replacement: str) -> Callable[..., Callable]: @functools.wraps(func) def deprecated_func(*args: tuple, **kwargs: dict[str, Any]) -> Any: """Wrap for the original function.""" - logger = logging.getLogger(func.__module__) - try: - _, integration, path = get_integration_frame() - if path == "custom_components/": - logger.warning( - "%s was called from %s, this is a deprecated function. Use %s instead, please report this to the maintainer of %s", - func.__name__, - integration, - replacement, - integration, - ) - else: - logger.warning( - "%s was called from %s, this is a deprecated function. Use %s instead", - func.__name__, - integration, - replacement, - ) - except MissingIntegrationFrame: - logger.warning( - "%s is a deprecated function. Use %s instead", - func.__name__, - replacement, - ) + _print_deprecation_warning(func, replacement, "function") return func(*args, **kwargs) return deprecated_func return deprecated_decorator + + +def _print_deprecation_warning(obj: Any, replacement: str, description: str) -> None: + logger = logging.getLogger(obj.__module__) + try: + _, integration, path = get_integration_frame() + if path == "custom_components/": + logger.warning( + "%s was called from %s, this is a deprecated %s. Use %s instead, please report this to the maintainer of %s", + obj.__name__, + integration, + description, + replacement, + integration, + ) + else: + logger.warning( + "%s was called from %s, this is a deprecated %s. Use %s instead", + obj.__name__, + integration, + description, + replacement, + ) + except MissingIntegrationFrame: + logger.warning( + "%s is a deprecated %s. Use %s instead", + obj.__name__, + description, + replacement, + ) From 27e32bbb19d3151b90f1e5506fccfe32e990c094 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 May 2021 13:16:52 +0200 Subject: [PATCH 794/852] Weight sensor average statistics by state durations (#51150) * Weight sensor average statistics by state durations * Fix test --- homeassistant/components/sensor/recorder.py | 45 +++++++++++++++++--- tests/components/recorder/test_statistics.py | 2 +- tests/components/sensor/test_recorder.py | 24 +++++------ 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index a75aa6298bc..fb6c8d2fba3 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -3,7 +3,6 @@ from __future__ import annotations import datetime import itertools -from statistics import fmean from homeassistant.components.recorder import history, statistics from homeassistant.components.sensor import ( @@ -16,7 +15,7 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, ) from homeassistant.const import ATTR_DEVICE_CLASS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State import homeassistant.util.dt as dt_util from . import DOMAIN @@ -53,6 +52,44 @@ def _is_number(s: str) -> bool: # pylint: disable=invalid-name return s.replace(".", "", 1).isdigit() +def _time_weighted_average( + fstates: list[tuple[float, State]], start: datetime.datetime, end: datetime.datetime +) -> float: + """Calculate a time weighted average. + + The average is calculated by, weighting the states by duration in seconds between + state changes. + Note: there's no interpolation of values between state changes. + """ + old_fstate: float | None = None + old_start_time: datetime.datetime | None = None + accumulated = 0.0 + + for fstate, state in fstates: + # The recorder will give us the last known state, which may be well + # before the requested start time for the statistics + start_time = start if state.last_updated < start else state.last_updated + if old_start_time is None: + # Adjust start time, if there was no last known state + start = start_time + else: + duration = start_time - old_start_time + # Accumulate the value, weighted by duration until next state change + assert old_fstate is not None + accumulated += old_fstate * duration.total_seconds() + + old_fstate = fstate + old_start_time = start_time + + if old_fstate is not None: + # Accumulate the value, weighted by duration until end of the period + assert old_start_time is not None + duration = end - old_start_time + accumulated += old_fstate * duration.total_seconds() + + return accumulated / (end - start).total_seconds() + + def compile_statistics( hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime ) -> dict: @@ -91,10 +128,8 @@ def compile_statistics( if "min" in wanted_statistics: result[entity_id]["min"] = min(*itertools.islice(zip(*fstates), 1)) - # Note: The average calculation will be incorrect for unevenly spaced readings, - # this needs to be improved by weighting with time between measurements if "mean" in wanted_statistics: - result[entity_id]["mean"] = fmean(*itertools.islice(zip(*fstates), 1)) + result[entity_id]["mean"] = _time_weighted_average(fstates, start, end) if "sum" in wanted_statistics: last_reset = old_last_reset = None diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 74be1075626..cffb67937fe 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -30,7 +30,7 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 15.0, + "mean": 14.915254237288135, "min": 10.0, "max": 20.0, "last_reset": None, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 5d86ac520a5..37cc7387f25 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -31,9 +31,9 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 15.0, + "mean": 16.440677966101696, "min": 10.0, - "max": 20.0, + "max": 30.0, "last_reset": None, "state": None, "sum": None, @@ -243,9 +243,9 @@ def test_compile_hourly_statistics_unchanged(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(four), - "mean": 20.0, - "min": 20.0, - "max": 20.0, + "mean": 30.0, + "min": 30.0, + "max": 30.0, "last_reset": None, "state": None, "sum": None, @@ -271,7 +271,7 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 17.5, + "mean": 21.1864406779661, "min": 10.0, "max": 25.0, "last_reset": None, @@ -318,9 +318,9 @@ def record_states(hass): zero = dt_util.utcnow() one = zero + timedelta(minutes=1) - two = one + timedelta(minutes=15) - three = two + timedelta(minutes=30) - four = three + timedelta(minutes=15) + two = one + timedelta(minutes=10) + three = two + timedelta(minutes=40) + four = three + timedelta(minutes=10) states = {mp: [], sns1: [], sns2: [], sns3: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): @@ -340,9 +340,9 @@ def record_states(hass): states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): - states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "20", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "20", attributes=sns3_attr)) + states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) return zero, four, states From e86e70f327666d8380fed07b14d8dc2072595123 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Thu, 27 May 2021 20:01:04 +0100 Subject: [PATCH 795/852] Bump pyroon to 0.0.37 (#51164) --- homeassistant/components/roon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 09fcaad5f1f..354117e8fe4 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.0.36"], + "requirements": ["roonapi==0.0.37"], "codeowners": ["@pavoni"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index cb3b05fed6e..6e288fcd333 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2015,7 +2015,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.36 +roonapi==0.0.37 # homeassistant.components.rova rova==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 148936e126e..4e6f5a0ef1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1091,7 +1091,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.36 +roonapi==0.0.37 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From a6a18effee4228490aa82727d19234ccdee9595a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 05:07:58 -0500 Subject: [PATCH 796/852] Improve Sonos polling (#51170) * Improve Sonos polling Warn user if polling is being used Provide callback IP:port to help user fix networking Fix radio handling when polling (no event payload) Clarify dispatch target to reflect polling action * Lint * Revert method removal --- .../components/sonos/binary_sensor.py | 3 +-- homeassistant/components/sonos/const.py | 2 +- homeassistant/components/sonos/entity.py | 18 +++++++++++++++--- homeassistant/components/sonos/media_player.py | 7 +++---- homeassistant/components/sonos/sensor.py | 3 +-- homeassistant/components/sonos/speaker.py | 17 +++++++++++++---- homeassistant/components/sonos/switch.py | 2 +- 7 files changed, 35 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 21e0c077136..8583132521c 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -1,7 +1,6 @@ """Entity representing a Sonos power sensor.""" from __future__ import annotations -import datetime import logging from typing import Any @@ -50,7 +49,7 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): """Return the entity's device class.""" return DEVICE_CLASS_BATTERY_CHARGING - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Poll the device for the current state.""" await self.speaker.async_poll_battery() diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index c32f981e345..0a70844e6b5 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -136,7 +136,7 @@ SONOS_CREATE_ALARM = "sonos_create_alarm" SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" -SONOS_ENTITY_UPDATE = "sonos_entity_update" +SONOS_POLL_UPDATE = "sonos_poll_update" SONOS_GROUP_UPDATE = "sonos_group_update" SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated" SONOS_ALARM_UPDATE = "sonos_alarm_update" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 8632357d618..8c47c69b2d7 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -1,6 +1,7 @@ """Entity representing a Sonos player.""" from __future__ import annotations +import datetime import logging from pysonos.core import SoCo @@ -15,8 +16,8 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DOMAIN, SONOS_ENTITY_CREATED, - SONOS_ENTITY_UPDATE, SONOS_HOUSEHOLD_UPDATED, + SONOS_POLL_UPDATE, SONOS_STATE_UPDATED, ) from .speaker import SonosSpeaker @@ -38,8 +39,8 @@ class SonosEntity(Entity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}", - self.async_update, # pylint: disable=no-member + f"{SONOS_POLL_UPDATE}-{self.soco.uid}", + self.async_poll, ) ) self.async_on_remove( @@ -60,6 +61,17 @@ class SonosEntity(Entity): self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain ) + async def async_poll(self, now: datetime.datetime) -> None: + """Poll the entity if subscriptions fail.""" + if self.speaker.is_first_poll: + _LOGGER.warning( + "%s cannot reach [%s], falling back to polling, functionality may be limited", + self.speaker.zone_name, + self.speaker.subscription_address, + ) + self.speaker.is_first_poll = False + await self.async_update() # pylint: disable=no-member + @property def soco(self) -> SoCo: """Return the speaker SoCo instance.""" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 9ca4b21425b..e75400b06ab 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -292,13 +292,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): return STATE_PLAYING return STATE_IDLE - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Retrieve latest state.""" - await self.hass.async_add_executor_job(self._update, now) + await self.hass.async_add_executor_job(self._update) - def _update(self, now: datetime.datetime | None = None) -> None: + def _update(self) -> None: """Retrieve latest state.""" - _LOGGER.debug("Polling speaker %s", self.speaker.zone_name) try: self.speaker.update_groups() self.speaker.update_volume() diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index d9ff19af581..9e5277819a7 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -1,7 +1,6 @@ """Entity representing a Sonos battery level.""" from __future__ import annotations -import datetime import logging from homeassistant.components.sensor import SensorEntity @@ -50,7 +49,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): """Get the unit of measurement.""" return PERCENTAGE - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Poll the device for the current state.""" await self.speaker.async_poll_battery() diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index c1f1dbb9104..b4b403e0445 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -44,8 +44,8 @@ from .const import ( SONOS_CREATE_BATTERY, SONOS_CREATE_MEDIA_PLAYER, SONOS_ENTITY_CREATED, - SONOS_ENTITY_UPDATE, SONOS_GROUP_UPDATE, + SONOS_POLL_UPDATE, SONOS_SEEN, SONOS_STATE_PLAYING, SONOS_STATE_TRANSITIONING, @@ -138,6 +138,7 @@ class SonosSpeaker: self.household_id: str = soco.household_id self.media = SonosMedia(soco) + self.is_first_poll: bool = True self._is_ready: bool = False self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None @@ -322,7 +323,7 @@ class SonosSpeaker: partial( async_dispatcher_send, self.hass, - f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}", + f"{SONOS_POLL_UPDATE}-{self.soco.uid}", ), SCAN_INTERVAL, ) @@ -418,7 +419,7 @@ class SonosSpeaker: ): async_dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) - async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE, self) + async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE) self.async_write_entity_states() @@ -877,7 +878,7 @@ class SonosSpeaker: if not self.media.artist: try: self.media.artist = variables["current_track_meta_data"].creator - except (KeyError, AttributeError): + except (TypeError, KeyError, AttributeError): pass # Radios without tagging can have part of the radio URI as title. @@ -950,3 +951,11 @@ class SonosSpeaker: elif update_media_position: self.media.position = current_position self.media.position_updated_at = dt_util.utcnow() + + @property + def subscription_address(self) -> str | None: + """Return the current subscription callback address if any.""" + if self._subscriptions: + addr, port = self._subscriptions[0].event_listener.address + return ":".join([addr, str(port)]) + return None diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 967bc21da59..8ea30bfe7a4 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -106,7 +106,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): return False - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Poll the device for the current state.""" if await self.async_check_if_available(): await self.hass.async_add_executor_job(self.update_alarm) From 6388203f73ef8d6da55677e3e9d841af5e170b92 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 28 May 2021 08:00:11 +0200 Subject: [PATCH 797/852] Fix Netatmo data class update (#51177) --- homeassistant/components/netatmo/data_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 83215bd3af5..11415417ee1 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -99,7 +99,8 @@ class NetatmoDataHandler: time() + data_class["interval"] ) - await self.async_fetch_data(data_class["name"]) + if data_class_name := data_class["name"]: + await self.async_fetch_data(data_class_name) self._queue.rotate(BATCH_SIZE) From 3e57b6178de395bd6ad78482ae7b8575a797917f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 28 May 2021 12:02:35 +0200 Subject: [PATCH 798/852] Use get with default for consider home (#51194) --- homeassistant/components/fritz/common.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index ad906e8956b..ec7e402f760 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -16,7 +16,10 @@ from fritzconnection.core.exceptions import ( from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus -from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -143,7 +146,9 @@ class FritzBoxTools: """Scan for new devices and return a list of found device ids.""" _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) - consider_home = self._options[CONF_CONSIDER_HOME] + consider_home = self._options.get( + CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() + ) new_device = False for known_host in self._update_info(): From fe84d060d69b3fcf99f0d2c37ac9d388c974db30 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 28 May 2021 13:36:22 +0200 Subject: [PATCH 799/852] Fix Netatmo sensor initialization (#51195) --- homeassistant/components/netatmo/__init__.py | 7 +++++++ homeassistant/components/netatmo/data_handler.py | 10 ++++++---- .../components/netatmo/netatmo_entity_base.py | 4 ---- homeassistant/components/netatmo/sensor.py | 14 +++++--------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 354ce2cf942..f6805099211 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -204,9 +204,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.services.async_register(DOMAIN, "register_webhook", register_webhook) hass.services.async_register(DOMAIN, "unregister_webhook", unregister_webhook) + entry.add_update_listener(async_config_entry_updated) + return True +async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle signals of config entry being updated.""" + async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" if CONF_WEBHOOK_ID in entry.data: diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 11415417ee1..12376e5ac78 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -95,12 +95,14 @@ class NetatmoDataHandler: for data_class in islice(self._queue, 0, BATCH_SIZE): if data_class[NEXT_SCAN] > time(): continue - self.data_classes[data_class["name"]][NEXT_SCAN] = ( - time() + data_class["interval"] - ) if data_class_name := data_class["name"]: - await self.async_fetch_data(data_class_name) + self.data_classes[data_class_name][NEXT_SCAN] = ( + time() + data_class["interval"] + ) + + if self.data_classes[data_class_name]["subscriptions"]: + await self.async_fetch_data(data_class_name) self._queue.rotate(BATCH_SIZE) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 1fcd4a121d8..5d43a46e89b 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -1,16 +1,12 @@ """Base class for Netatmo entities.""" from __future__ import annotations -import logging - from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import Entity from .const import DATA_DEVICE_IDS, DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler -_LOGGER = logging.getLogger(__name__) - class NetatmoBase(Entity): """Netatmo entity base class.""" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index e56847386a3..eeaab52a21a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -2,7 +2,6 @@ import logging from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -21,7 +20,7 @@ from homeassistant.const import ( SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.device_registry import async_entries_for_config_entry from homeassistant.helpers.dispatcher import ( @@ -131,6 +130,7 @@ PUBLIC = "public" async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] + platform_not_ready = False async def find_entities(data_class_name): """Find all entities.""" @@ -184,7 +184,7 @@ async def async_setup_entry(hass, entry, async_add_entities): data_class = data_handler.data.get(data_class_name) if not data_class or not data_class.raw_data: - raise PlatformNotReady + platform_not_ready = True async_add_entities(await find_entities(data_class_name), True) @@ -241,14 +241,10 @@ async def async_setup_entry(hass, entry, async_add_entities): hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities ) - entry.add_update_listener(async_config_entry_updated) - await add_public_entities(False) - -async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle signals of config entry being updated.""" - async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") + if platform_not_ready: + raise PlatformNotReady class NetatmoSensor(NetatmoBase, SensorEntity): From ae914be44f8cb1992518db12f94ffaefa6bfab6d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 28 May 2021 13:32:26 +0200 Subject: [PATCH 800/852] Only run philips_js notify service while TV is turned on (#51196) Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- .../components/philips_js/__init__.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index f21337f512e..a2e5dd4cbc2 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -116,8 +116,21 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): ), ) + @property + def _notify_wanted(self): + """Return if the notify feature should be active. + + We only run it when TV is considered fully on. When powerstate is in standby, the TV + will go in low power states and seemingly break the http server in odd ways. + """ + return ( + self.api.on + and self.api.powerstate == "On" + and self.api.notify_change_supported + ) + async def _notify_task(self): - while self.api.on and self.api.notify_change_supported: + while self._notify_wanted: res = await self.api.notifyChange(130) if res: self.async_set_updated_data(None) @@ -133,11 +146,10 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): @callback def _async_notify_schedule(self): - if ( - (self._notify_future is None or self._notify_future.done()) - and self.api.on - and self.api.notify_change_supported - ): + if self._notify_future and not self._notify_future.done(): + return + + if self._notify_wanted: self._notify_future = asyncio.create_task(self._notify_task()) @callback From de575fdb7be15af5d35cb3fac5c02cd969c96fcc Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 May 2021 13:22:58 +0200 Subject: [PATCH 801/852] Update base image to 2021.05.0 (#51198) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index de5f895af2a..c3e5d83dc78 100644 --- a/build.json +++ b/build.json @@ -2,11 +2,11 @@ "image": "homeassistant/{arch}-homeassistant", "shadow_repository": "ghcr.io/home-assistant", "build_from": { - "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.04.3", - "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.04.3", - "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.04.3", - "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.04.3", - "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.04.3" + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.05.0", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.05.0", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.05.0", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.05.0", + "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.05.0" }, "labels": { "io.hass.type": "core", From 0c9c113528b4df2e0810549665c29ce912f2bc21 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 28 May 2021 14:38:01 +0200 Subject: [PATCH 802/852] Update frontend to 20210528.0 (#51199) --- 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 57380b53be8..9ee97c851e9 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==20210526.0" + "home-assistant-frontend==20210528.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b61e0d24785..7e6be2f0016 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210526.0 +home-assistant-frontend==20210528.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 6e288fcd333..18d6df72c70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210526.0 +home-assistant-frontend==20210528.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e6f5a0ef1d..4aa6218067e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210526.0 +home-assistant-frontend==20210528.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 0de8604631d15d8f1604842db66555c89077c0f6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 May 2021 14:51:21 +0200 Subject: [PATCH 803/852] Bumped version to 2021.6.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 581c08c5f94..a38655119bc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 215869b3df92810e4e5a1a0abc5713f0c15bee38 Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Fri, 28 May 2021 17:48:30 +0300 Subject: [PATCH 804/852] Update to pymelcloud 2.5.3 (#51043) Previous version of pymelcloud performs requests that are not permitted for guest users. Bypassing these requests results only in less detailed device info. --- homeassistant/components/melcloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 641a4df583e..4aff46a22b6 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -3,7 +3,7 @@ "name": "MELCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", - "requirements": ["pymelcloud==2.5.2"], + "requirements": ["pymelcloud==2.5.3"], "codeowners": ["@vilppuvuorinen"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 18d6df72c70..55d07ae773d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1557,7 +1557,7 @@ pymazda==0.1.6 pymediaroom==0.6.4.1 # homeassistant.components.melcloud -pymelcloud==2.5.2 +pymelcloud==2.5.3 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4aa6218067e..9f95763e54f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -868,7 +868,7 @@ pymata-express==1.19 pymazda==0.1.6 # homeassistant.components.melcloud -pymelcloud==2.5.2 +pymelcloud==2.5.3 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 From e980365a9c39080ccbf23b9ac820125c86a6e436 Mon Sep 17 00:00:00 2001 From: Aaron David Schneider Date: Thu, 27 May 2021 19:56:59 +0200 Subject: [PATCH 805/852] Add tests for sonos switch platform (#51142) * add tests * refactor async_added_to_hass * fix tests and race condition * use async_get * typo --- homeassistant/components/sonos/speaker.py | 2 - homeassistant/components/sonos/switch.py | 18 ++++++--- tests/components/sonos/conftest.py | 48 +++++++++++++++++++---- tests/components/sonos/test_switch.py | 35 +++++++++++++++-- 4 files changed, 84 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index b4b403e0445..81c95f6e33f 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -421,8 +421,6 @@ class SonosSpeaker: async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE) - self.async_write_entity_states() - async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" self._last_battery_event = dt_util.utcnow() diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 8ea30bfe7a4..83449c846c6 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -43,11 +43,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity = SonosAlarmEntity(alarm_id, speaker) async_add_entities([entity]) configured_alarms.add(alarm_id) - config_entry.async_on_unload( - async_dispatcher_connect( - hass, SONOS_ALARM_UPDATE, entity.async_update - ) - ) config_entry.async_on_unload( async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_entity) @@ -64,9 +59,20 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): self._alarm_id = alarm_id self.entity_id = ENTITY_ID_FORMAT.format(f"sonos_alarm_{self.alarm_id}") + async def async_added_to_hass(self) -> None: + """Handle switch setup when added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SONOS_ALARM_UPDATE, + self.async_update, + ) + ) + @property def alarm(self): - """Return the ID of the alarm.""" + """Return the alarm instance.""" return self.hass.data[DATA_SONOS].alarms[self.alarm_id] @property diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 2feb2b54896..aa14dcaa5cf 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -30,7 +30,7 @@ def config_entry_fixture(): @pytest.fixture(name="soco") def soco_fixture( - music_library, speaker_info, battery_info, dummy_soco_service, alarmClock + music_library, speaker_info, battery_info, dummy_soco_service, alarm_clock ): """Create a mock pysonos SoCo fixture.""" with patch("pysonos.SoCo", autospec=True) as mock, patch( @@ -46,7 +46,7 @@ def soco_fixture( mock_soco.zoneGroupTopology = dummy_soco_service mock_soco.contentDirectory = dummy_soco_service mock_soco.deviceProperties = dummy_soco_service - mock_soco.alarmClock = alarmClock + mock_soco.alarmClock = alarm_clock mock_soco.mute = False mock_soco.night_mode = True mock_soco.dialog_mode = True @@ -90,12 +90,28 @@ def music_library_fixture(): return music_library -@pytest.fixture(name="alarmClock") -def alarmClock_fixture(): +@pytest.fixture(name="alarm_clock") +def alarm_clock_fixture(): """Create alarmClock fixture.""" - alarmClock = Mock() - alarmClock.subscribe = AsyncMock() - alarmClock.ListAlarms.return_value = { + alarm_clock = Mock() + alarm_clock.subscribe = AsyncMock() + alarm_clock.ListAlarms.return_value = { + "CurrentAlarmList": "" + '' + " " + } + return alarm_clock + + +@pytest.fixture(name="alarm_clock_extended") +def alarm_clock_fixture_extended(): + """Create alarmClock fixture.""" + alarm_clock = Mock() + alarm_clock.subscribe = AsyncMock() + alarm_clock.ListAlarms.return_value = { "CurrentAlarmList": "" '' " " } - return alarmClock + return alarm_clock @pytest.fixture(name="speaker_info") @@ -141,3 +157,19 @@ def battery_event_fixture(soco): "more_info": "BattChg:NOT_CHARGING,RawBattPct:100,BattPct:100,BattTmp:25", } return SonosMockEvent(soco, variables) + + +@pytest.fixture(name="alarm_event") +def alarm_event_fixture(soco): + """Create alarm_event fixture.""" + variables = { + "time_zone": "ffc40a000503000003000502ffc4", + "time_server": "0.sonostime.pool.ntp.org,1.sonostime.pool.ntp.org,2.sonostime.pool.ntp.org,3.sonostime.pool.ntp.org", + "time_generation": "20000001", + "alarm_list_version": "RINCON_test", + "time_format": "INV", + "date_format": "INV", + "daily_index_refresh_time": None, + } + + return SonosMockEvent(soco, variables) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index c33c472ee27..d4448d22b32 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -9,17 +9,18 @@ from homeassistant.components.sonos.switch import ( ATTR_VOLUME, ) from homeassistant.const import ATTR_TIME, STATE_ON +from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.setup import async_setup_component async def setup_platform(hass, config_entry, config): - """Set up the media player platform for testing.""" + """Set up the switch platform for testing.""" config_entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() -async def test_entity_registry(hass, config_entry, config, soco): +async def test_entity_registry(hass, config_entry, config): """Test sonos device with alarm registered in the device registry.""" await setup_platform(hass, config_entry, config) @@ -29,7 +30,7 @@ async def test_entity_registry(hass, config_entry, config, soco): assert "switch.sonos_alarm_14" in entity_registry.entities -async def test_alarm_attributes(hass, config_entry, config, soco): +async def test_alarm_attributes(hass, config_entry, config): """Test for correct sonos alarm state.""" await setup_platform(hass, config_entry, config) @@ -45,3 +46,31 @@ async def test_alarm_attributes(hass, config_entry, config, soco): assert alarm_state.attributes.get(ATTR_VOLUME) == 0.25 assert alarm_state.attributes.get(ATTR_PLAY_MODE) == "SHUFFLE_NOREPEAT" assert not alarm_state.attributes.get(ATTR_INCLUDE_LINKED_ZONES) + + +async def test_alarm_create_delete( + hass, config_entry, config, soco, alarm_clock, alarm_clock_extended, alarm_event +): + """Test for correct creation and deletion of alarms during runtime.""" + soco.alarmClock = alarm_clock_extended + + await setup_platform(hass, config_entry, config) + + subscription = alarm_clock_extended.subscribe.return_value + sub_callback = subscription.callback + + sub_callback(event=alarm_event) + await hass.async_block_till_done() + + entity_registry = async_get_entity_registry(hass) + + assert "switch.sonos_alarm_14" in entity_registry.entities + assert "switch.sonos_alarm_15" in entity_registry.entities + + alarm_clock_extended.ListAlarms.return_value = alarm_clock.ListAlarms.return_value + + sub_callback(event=alarm_event) + await hass.async_block_till_done() + + assert "switch.sonos_alarm_14" in entity_registry.entities + assert "switch.sonos_alarm_15" not in entity_registry.entities From a0696fe9230bba9bd57c6f765ef1ab043c075ab4 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 21:32:50 -0500 Subject: [PATCH 806/852] Centralize Sonos subscription logic (#51172) * Centralize Sonos subscription logic * Clean up mocked Sonos Service instances, use subscription callback * Use existing mocked attributes * Use event dispatcher dict, move methods together, make update_alarms sync * Create dispatcher dict once --- homeassistant/components/sonos/favorites.py | 4 +- homeassistant/components/sonos/speaker.py | 158 ++++++++++---------- tests/components/sonos/conftest.py | 47 +++--- tests/components/sonos/test_sensor.py | 8 +- 4 files changed, 111 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py index 19dcb5184e5..2f5cab23be2 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -41,7 +41,9 @@ class SonosFavorites: Updated favorites are not always immediately available. """ - event_id = event.variables["favorites_update_id"] + if not (event_id := event.variables.get("favorites_update_id")): + return + if not self._event_version: self._event_version = event_id return diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 81c95f6e33f..079b916a4bc 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -61,6 +61,14 @@ EVENT_CHARGING = { "CHARGING": True, "NOT_CHARGING": False, } +SUBSCRIPTION_SERVICES = [ + "alarmClock", + "avTransport", + "contentDirectory", + "deviceProperties", + "renderingControl", + "zoneGroupTopology", +] UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} @@ -140,8 +148,12 @@ class SonosSpeaker: self.is_first_poll: bool = True self._is_ready: bool = False + + # Subscriptions and events self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None + self._event_dispatchers: dict[str, Callable] = {} + self._poll_timer: Callable | None = None self._seen_timer: Callable | None = None self._platforms_ready: set[str] = set() @@ -209,6 +221,15 @@ class SonosSpeaker: else: self._platforms_ready.add(SWITCH_DOMAIN) + self._event_dispatchers = { + "AlarmClock": self.async_dispatch_alarms, + "AVTransport": self.async_dispatch_media_update, + "ContentDirectory": self.favorites.async_delayed_update, + "DeviceProperties": self.async_dispatch_device_properties, + "RenderingControl": self.async_update_volume, + "ZoneGroupTopology": self.async_update_groups, + } + dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) async def async_handle_new_entity(self, entity_type: str) -> None: @@ -238,6 +259,9 @@ class SonosSpeaker: """Return whether this speaker is available.""" return self._seen_timer is not None + # + # Subscription handling and event dispatchers + # async def async_subscribe(self) -> bool: """Initiate event subscriptions.""" _LOGGER.debug("Creating subscriptions for %s", self.zone_name) @@ -250,18 +274,11 @@ class SonosSpeaker: f"when existing subscriptions exist: {self._subscriptions}" ) - await asyncio.gather( - self._subscribe(self.soco.avTransport, self.async_update_media), - self._subscribe(self.soco.renderingControl, self.async_update_volume), - self._subscribe(self.soco.contentDirectory, self.async_update_content), - self._subscribe( - self.soco.zoneGroupTopology, self.async_dispatch_groups - ), - self._subscribe( - self.soco.deviceProperties, self.async_dispatch_properties - ), - self._subscribe(self.soco.alarmClock, self.async_dispatch_alarms), - ) + subscriptions = [ + self._subscribe(getattr(self.soco, service), self.async_dispatch_event) + for service in SUBSCRIPTION_SERVICES + ] + await asyncio.gather(*subscriptions) return True except SoCoException as ex: _LOGGER.warning("Could not connect %s: %s", self.zone_name, ex) @@ -279,26 +296,58 @@ class SonosSpeaker: self._subscriptions.append(subscription) @callback - def async_dispatch_properties(self, event: SonosEvent | None = None) -> None: - """Update properties from event.""" - self.hass.async_create_task(self.async_update_device_properties(event)) - - @callback - def async_dispatch_alarms(self, event: SonosEvent | None = None) -> None: - """Update alarms from event.""" - self.hass.async_create_task(self.async_update_alarms(event)) - - @callback - def async_dispatch_groups(self, event: SonosEvent | None = None) -> None: - """Update groups from event.""" - if event and self._poll_timer: + def async_dispatch_event(self, event: SonosEvent) -> None: + """Handle callback event and route as needed.""" + if self._poll_timer: _LOGGER.debug( "Received event, cancelling poll timer for %s", self.zone_name ) self._poll_timer() self._poll_timer = None - self.async_update_groups(event) + dispatcher = self._event_dispatchers[event.service.service_type] + dispatcher(event) + + @callback + def async_dispatch_alarms(self, event: SonosEvent) -> None: + """Create a task to update alarms from an event.""" + self.hass.async_add_executor_job(self.update_alarms) + + @callback + def async_dispatch_device_properties(self, event: SonosEvent) -> None: + """Update device properties from an event.""" + self.hass.async_create_task(self.async_update_device_properties(event)) + + async def async_update_device_properties(self, event: SonosEvent) -> None: + """Update device properties from an event.""" + if (more_info := event.variables.get("more_info")) is not None: + battery_dict = dict(x.split(":") for x in more_info.split(",")) + await self.async_update_battery_info(battery_dict) + self.async_write_entity_states() + + @callback + def async_dispatch_media_update(self, event: SonosEvent) -> None: + """Update information about currently playing media from an event.""" + self.hass.async_add_executor_job(self.update_media, event) + + @callback + def async_update_volume(self, event: SonosEvent) -> None: + """Update information about currently volume settings.""" + variables = event.variables + + if "volume" in variables: + self.volume = int(variables["volume"]["Master"]) + + if "mute" in variables: + self.muted = variables["mute"]["Master"] == "1" + + if "night_mode" in variables: + self.night_mode = variables["night_mode"] == "1" + + if "dialog_level" in variables: + self.dialog_mode = variables["dialog_level"] == "1" + + self.async_write_entity_states() async def async_seen(self, soco: SoCo | None = None) -> None: """Record that this speaker was seen right now.""" @@ -376,17 +425,6 @@ class SonosSpeaker: self._subscriptions = [] - async def async_update_device_properties(self, event: SonosEvent = None) -> None: - """Update device properties using the provided SonosEvent.""" - if event is None: - return - - if (more_info := event.variables.get("more_info")) is not None: - battery_dict = dict(x.split(":") for x in more_info.split(",")) - await self.async_update_battery_info(battery_dict) - - self.async_write_entity_states() - def update_alarms_for_speaker(self) -> set[str]: """Update current alarm instances. @@ -409,17 +447,11 @@ class SonosSpeaker: return new_alarms - async def async_update_alarms(self, event: SonosEvent | None = None) -> None: - """Update device properties using the provided SonosEvent.""" - if event is None: - return - - if new_alarms := await self.hass.async_add_executor_job( - self.update_alarms_for_speaker - ): - async_dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) - - async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE) + def update_alarms(self) -> None: + """Update alarms from an event.""" + if new_alarms := self.update_alarms_for_speaker(): + dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) + dispatcher_send(self.hass, SONOS_ALARM_UPDATE) async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" @@ -759,12 +791,6 @@ class SonosSpeaker: """Return the SonosFavorites instance for this household.""" return self.hass.data[DATA_SONOS].favorites[self.household_id] - @callback - def async_update_content(self, event: SonosEvent | None = None) -> None: - """Update information about available content.""" - if event and "favorites_update_id" in event.variables: - self.favorites.async_delayed_update(event) - def update_volume(self) -> None: """Update information about current volume settings.""" self.volume = self.soco.volume @@ -772,30 +798,6 @@ class SonosSpeaker: self.night_mode = self.soco.night_mode self.dialog_mode = self.soco.dialog_mode - @callback - def async_update_volume(self, event: SonosEvent) -> None: - """Update information about currently volume settings.""" - variables = event.variables - - if "volume" in variables: - self.volume = int(variables["volume"]["Master"]) - - if "mute" in variables: - self.muted = variables["mute"]["Master"] == "1" - - if "night_mode" in variables: - self.night_mode = variables["night_mode"] == "1" - - if "dialog_level" in variables: - self.dialog_mode = variables["dialog_level"] == "1" - - self.async_write_entity_states() - - @callback - def async_update_media(self, event: SonosEvent | None = None) -> None: - """Update information about currently playing media.""" - self.hass.async_add_executor_job(self.update_media, event) - def update_media(self, event: SonosEvent | None = None) -> None: """Update information about currently playing media.""" variables = event and event.variables diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index aa14dcaa5cf..fc5ef84c2d6 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -10,15 +10,24 @@ from homeassistant.const import CONF_HOSTS from tests.common import MockConfigEntry +class SonosMockService: + """Mock a Sonos Service used in callbacks.""" + + def __init__(self, service_type): + """Initialize the instance.""" + self.service_type = service_type + self.subscribe = AsyncMock() + + class SonosMockEvent: """Mock a sonos Event used in callbacks.""" - def __init__(self, soco, variables): + def __init__(self, soco, service, variables): """Initialize the instance.""" self.sid = f"{soco.uid}_sub0000000001" self.seq = "0" self.timestamp = 1621000000.0 - self.service = dummy_soco_service_fixture + self.service = service self.variables = variables @@ -29,9 +38,7 @@ def config_entry_fixture(): @pytest.fixture(name="soco") -def soco_fixture( - music_library, speaker_info, battery_info, dummy_soco_service, alarm_clock -): +def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): """Create a mock pysonos SoCo fixture.""" with patch("pysonos.SoCo", autospec=True) as mock, patch( "socket.gethostbyname", return_value="192.168.42.2" @@ -41,11 +48,11 @@ def soco_fixture( mock_soco.play_mode = "NORMAL" mock_soco.music_library = music_library mock_soco.get_speaker_info.return_value = speaker_info - mock_soco.avTransport = dummy_soco_service - mock_soco.renderingControl = dummy_soco_service - mock_soco.zoneGroupTopology = dummy_soco_service - mock_soco.contentDirectory = dummy_soco_service - mock_soco.deviceProperties = dummy_soco_service + mock_soco.avTransport = SonosMockService("AVTransport") + mock_soco.renderingControl = SonosMockService("RenderingControl") + mock_soco.zoneGroupTopology = SonosMockService("ZoneGroupTopology") + mock_soco.contentDirectory = SonosMockService("ContentDirectory") + mock_soco.deviceProperties = SonosMockService("DeviceProperties") mock_soco.alarmClock = alarm_clock mock_soco.mute = False mock_soco.night_mode = True @@ -74,14 +81,6 @@ def config_fixture(): return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.1"]}}} -@pytest.fixture(name="dummy_soco_service") -def dummy_soco_service_fixture(): - """Create dummy_soco_service fixture.""" - service = Mock() - service.subscribe = AsyncMock() - return service - - @pytest.fixture(name="music_library") def music_library_fixture(): """Create music_library fixture.""" @@ -93,8 +92,8 @@ def music_library_fixture(): @pytest.fixture(name="alarm_clock") def alarm_clock_fixture(): """Create alarmClock fixture.""" - alarm_clock = Mock() - alarm_clock.subscribe = AsyncMock() + alarm_clock = SonosMockService("AlarmClock") + alarm_clock.ListAlarms = Mock() alarm_clock.ListAlarms.return_value = { "CurrentAlarmList": "" '" ' Date: Fri, 28 May 2021 09:06:17 -0500 Subject: [PATCH 807/852] Fix samsungtv yaml import without configured name (#51204) --- .../components/samsungtv/config_flow.py | 2 +- .../components/samsungtv/test_config_flow.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index b45f6c5670b..46800e1653b 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -173,7 +173,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) except socket.gaierror as err: raise data_entry_flow.AbortFlow(RESULT_UNKNOWN_HOST) from err - self._name = user_input[CONF_NAME] + self._name = user_input.get(CONF_NAME, self._host) self._title = self._name async def async_step_user(self, user_input=None): diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 04dffd31801..5b85ecf7048 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -54,6 +54,9 @@ MOCK_IMPORT_DATA = { CONF_NAME: "fake", CONF_PORT: 55000, } +MOCK_IMPORT_DATA_WITHOUT_NAME = { + CONF_HOST: "fake_host", +} MOCK_IMPORT_WSDATA = { CONF_HOST: "fake_host", CONF_NAME: "fake", @@ -509,6 +512,26 @@ async def test_import_legacy(hass: HomeAssistant): assert result["result"].unique_id is None +async def test_import_legacy_without_name(hass: HomeAssistant): + """Test importing from yaml without a name.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA_WITHOUT_NAME, + ) + await hass.async_block_till_done() + assert result["type"] == "create_entry" + assert result["title"] == "fake_host" + assert result["data"][CONF_METHOD] == METHOD_LEGACY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["result"].unique_id is None + + async def test_import_websocket(hass: HomeAssistant): """Test importing from yaml with hostname.""" with patch( From f32309273b3eb495aa08caccda3b1aea77361866 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 23:28:07 -0500 Subject: [PATCH 808/852] Fix use of async in Sonos switch (#51210) * Fix use of async in Sonos switch * Simplify * Convert to callback --- homeassistant/components/sonos/switch.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 83449c846c6..4783795a343 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -8,6 +8,7 @@ from pysonos.exceptions import SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.const import ATTR_TIME +from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -99,7 +100,8 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): str(self.alarm.start_time)[0:5], ) - async def async_check_if_available(self): + @callback + def async_check_if_available(self): """Check if alarm exists and remove alarm entity if not available.""" if self.alarm_id in self.hass.data[DATA_SONOS].alarms: return True @@ -114,12 +116,10 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): async def async_update(self) -> None: """Poll the device for the current state.""" - if await self.async_check_if_available(): - await self.hass.async_add_executor_job(self.update_alarm) + if not self.async_check_if_available(): + return - def update_alarm(self): - """Update the state of the alarm.""" - _LOGGER.debug("Updating the state of the alarm") + _LOGGER.debug("Updating alarm: %s", self.entity_id) if self.speaker.soco.uid != self.alarm.zone.uid: self.speaker = self.hass.data[DATA_SONOS].discovered.get( self.alarm.zone.uid @@ -129,11 +129,12 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): "No configured Sonos speaker has been found to match the alarm." ) - self._update_device() + self._async_update_device() - self.schedule_update_ha_state() + self.async_write_ha_state() - def _update_device(self): + @callback + def _async_update_device(self): """Update the device, since this alarm moved to a different player.""" device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) From fa7837bb127c9622b4096abd189e13dc3384817f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 17:45:43 -0500 Subject: [PATCH 809/852] Improve Sonos alarm logging (#51212) --- homeassistant/components/sonos/switch.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 4783795a343..4b24224f6a0 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -106,7 +106,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): if self.alarm_id in self.hass.data[DATA_SONOS].alarms: return True - _LOGGER.debug("The alarm is removed from hass because it has been deleted") + _LOGGER.debug("%s has been deleted", self.entity_id) entity_registry = er.async_get(self.hass) if entity_registry.async_get(self.entity_id): @@ -151,7 +151,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): connections={(dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address)}, ) if not entity_registry.async_get(self.entity_id).device_id == new_device.id: - _LOGGER.debug("The alarm is switching the sonos player") + _LOGGER.debug("%s is moving to %s", self.entity_id, new_device.name) # pylint: disable=protected-access entity_registry._async_update_entity( self.entity_id, device_id=new_device.id @@ -200,10 +200,8 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): async def async_handle_switch_on_off(self, turn_on: bool) -> None: """Handle turn on/off of alarm switch.""" try: - _LOGGER.debug("Switching the state of the alarm") + _LOGGER.debug("Toggling the state of %s", self.entity_id) self.alarm.enabled = turn_on await self.hass.async_add_executor_job(self.alarm.save) except SoCoUPnPException as exc: - _LOGGER.warning( - "Home Assistant couldn't switch the alarm %s", exc, exc_info=True - ) + _LOGGER.error("Could not update %s: %s", self.entity_id, exc, exc_info=True) From b75f4b1f4da2c16b01785e582829109c7b979c1d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 May 2021 22:37:17 +0200 Subject: [PATCH 810/852] Fix flaky statistics tests (#51214) * Fix flaky statistics tests * Tweak --- tests/components/recorder/test_statistics.py | 8 ++- tests/components/sensor/test_recorder.py | 68 ++++++++++---------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index cffb67937fe..1ec0f2284b4 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -3,6 +3,8 @@ from datetime import timedelta from unittest.mock import patch, sentinel +from pytest import approx + from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat @@ -30,9 +32,9 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 14.915254237288135, - "min": 10.0, - "max": 20.0, + "mean": approx(14.915254237288135), + "min": approx(10.0), + "max": approx(20.0), "last_reset": None, "state": None, "sum": None, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 37cc7387f25..47a950f9eaa 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -3,6 +3,8 @@ from datetime import timedelta from unittest.mock import patch, sentinel +from pytest import approx + from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat @@ -31,9 +33,9 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 16.440677966101696, - "min": 10.0, - "max": 30.0, + "mean": approx(16.440677966101696), + "min": approx(10.0), + "max": approx(30.0), "last_reset": None, "state": None, "sum": None, @@ -75,8 +77,8 @@ def test_compile_hourly_energy_statistics(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 20.0, - "sum": 10.0, + "state": approx(20.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -85,8 +87,8 @@ def test_compile_hourly_energy_statistics(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 40.0, - "sum": 10.0, + "state": approx(40.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -95,8 +97,8 @@ def test_compile_hourly_energy_statistics(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 70.0, - "sum": 40.0, + "state": approx(70.0), + "sum": approx(40.0), }, ] } @@ -135,8 +137,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 20.0, - "sum": 10.0, + "state": approx(20.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -145,8 +147,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 40.0, - "sum": 10.0, + "state": approx(40.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -155,8 +157,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 70.0, - "sum": 40.0, + "state": approx(70.0), + "sum": approx(40.0), }, ], "sensor.test2": [ @@ -167,8 +169,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 130.0, - "sum": 20.0, + "state": approx(130.0), + "sum": approx(20.0), }, { "statistic_id": "sensor.test2", @@ -177,8 +179,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 45.0, - "sum": -95.0, + "state": approx(45.0), + "sum": approx(-95.0), }, { "statistic_id": "sensor.test2", @@ -187,8 +189,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 75.0, - "sum": -65.0, + "state": approx(75.0), + "sum": approx(-65.0), }, ], "sensor.test3": [ @@ -199,8 +201,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 5.0, - "sum": 5.0, + "state": approx(5.0), + "sum": approx(5.0), }, { "statistic_id": "sensor.test3", @@ -209,8 +211,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 50.0, - "sum": 30.0, + "state": approx(50.0), + "sum": approx(30.0), }, { "statistic_id": "sensor.test3", @@ -219,8 +221,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 90.0, - "sum": 70.0, + "state": approx(90.0), + "sum": approx(70.0), }, ], } @@ -243,9 +245,9 @@ def test_compile_hourly_statistics_unchanged(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(four), - "mean": 30.0, - "min": 30.0, - "max": 30.0, + "mean": approx(30.0), + "min": approx(30.0), + "max": approx(30.0), "last_reset": None, "state": None, "sum": None, @@ -271,9 +273,9 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 21.1864406779661, - "min": 10.0, - "max": 25.0, + "mean": approx(21.1864406779661), + "min": approx(10.0), + "max": approx(25.0), "last_reset": None, "state": None, "sum": None, From 51d98bb9c8d5dc8876736be079c9ef80078af2ef Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 May 2021 14:10:45 +0200 Subject: [PATCH 811/852] Fix Netatmo data class update (#51215) * Catch if data class entry is None * Guard --- homeassistant/components/netatmo/data_handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 12376e5ac78..1c092c40930 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -101,8 +101,7 @@ class NetatmoDataHandler: time() + data_class["interval"] ) - if self.data_classes[data_class_name]["subscriptions"]: - await self.async_fetch_data(data_class_name) + await self.async_fetch_data(data_class_name) self._queue.rotate(BATCH_SIZE) @@ -133,6 +132,9 @@ class NetatmoDataHandler: async def async_fetch_data(self, data_class_entry): """Fetch data and notify.""" + if self.data[data_class_entry] is None: + return + try: await self.data[data_class_entry].async_update() From 835a9efc6453fe99c9c4d615882a607ce8ab2907 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 29 May 2021 09:08:46 -0500 Subject: [PATCH 812/852] Reorganize SonosSpeaker class for readability (#51222) --- homeassistant/components/sonos/speaker.py | 112 ++++++++++++++-------- 1 file changed, 71 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 079b916a4bc..701fe5aa8c4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -146,36 +146,43 @@ class SonosSpeaker: self.household_id: str = soco.household_id self.media = SonosMedia(soco) + # Synchronization helpers self.is_first_poll: bool = True self._is_ready: bool = False + self._platforms_ready: set[str] = set() # Subscriptions and events self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None self._event_dispatchers: dict[str, Callable] = {} + # Scheduled callback handles self._poll_timer: Callable | None = None self._seen_timer: Callable | None = None - self._platforms_ready: set[str] = set() + # Dispatcher handles self._entity_creation_dispatcher: Callable | None = None self._group_dispatcher: Callable | None = None self._seen_dispatcher: Callable | None = None + # Device information self.mac_address = speaker_info["mac_address"] self.model_name = speaker_info["model_name"] self.version = speaker_info["display_version"] self.zone_name = speaker_info["zone_name"] + # Battery self.battery_info: dict[str, Any] | None = None self._last_battery_event: datetime.datetime | None = None self._battery_poll_timer: Callable | None = None + # Volume / Sound self.volume: int | None = None self.muted: bool | None = None self.night_mode: bool | None = None self.dialog_mode: bool | None = None + # Grouping self.coordinator: SonosSpeaker | None = None self.sonos_group: list[SonosSpeaker] = [self] self.sonos_group_entities: list[str] = [] @@ -232,6 +239,9 @@ class SonosSpeaker: dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) + # + # Entity management + # async def async_handle_new_entity(self, entity_type: str) -> None: """Listen to new entities to trigger first subscription.""" self._platforms_ready.add(entity_type) @@ -254,11 +264,32 @@ class SonosSpeaker: self.media.play_mode = self.soco.play_mode self.update_volume() + # + # Properties + # @property def available(self) -> bool: """Return whether this speaker is available.""" return self._seen_timer is not None + @property + def favorites(self) -> SonosFavorites: + """Return the SonosFavorites instance for this household.""" + return self.hass.data[DATA_SONOS].favorites[self.household_id] + + @property + def is_coordinator(self) -> bool: + """Return true if player is a coordinator.""" + return self.coordinator is None + + @property + def subscription_address(self) -> str | None: + """Return the current subscription callback address if any.""" + if self._subscriptions: + addr, port = self._subscriptions[0].event_listener.address + return ":".join([addr, str(port)]) + return None + # # Subscription handling and event dispatchers # @@ -295,6 +326,30 @@ class SonosSpeaker: subscription.auto_renew_fail = self.async_renew_failed self._subscriptions.append(subscription) + @callback + def async_renew_failed(self, exception: Exception) -> None: + """Handle a failed subscription renewal.""" + self.hass.async_create_task(self.async_resubscribe(exception)) + + async def async_resubscribe(self, exception: Exception) -> None: + """Attempt to resubscribe when a renewal failure is detected.""" + async with self._resubscription_lock: + if not self.available: + return + + if getattr(exception, "status", None) == 412: + _LOGGER.warning( + "Subscriptions for %s failed, speaker may have lost power", + self.zone_name, + ) + else: + _LOGGER.error( + "Subscription renewals for %s failed", + self.zone_name, + exc_info=exception, + ) + await self.async_unseen() + @callback def async_dispatch_event(self, event: SonosEvent) -> None: """Handle callback event and route as needed.""" @@ -349,6 +404,9 @@ class SonosSpeaker: self.async_write_entity_states() + # + # Speaker availability methods + # async def async_seen(self, soco: SoCo | None = None) -> None: """Record that this speaker was seen right now.""" if soco is not None: @@ -386,28 +444,6 @@ class SonosSpeaker: self.async_write_entity_states() - async def async_resubscribe(self, exception: Exception) -> None: - """Attempt to resubscribe when a renewal failure is detected.""" - async with self._resubscription_lock: - if self.available: - if getattr(exception, "status", None) == 412: - _LOGGER.warning( - "Subscriptions for %s failed, speaker may have lost power", - self.zone_name, - ) - else: - _LOGGER.error( - "Subscription renewals for %s failed", - self.zone_name, - exc_info=exception, - ) - await self.async_unseen() - - @callback - def async_renew_failed(self, exception: Exception) -> None: - """Handle a failed subscription renewal.""" - self.hass.async_create_task(self.async_resubscribe(exception)) - async def async_unseen(self, now: datetime.datetime | None = None) -> None: """Make this player unavailable when it was not seen recently.""" self.async_write_entity_states() @@ -425,6 +461,9 @@ class SonosSpeaker: self._subscriptions = [] + # + # Alarm management + # def update_alarms_for_speaker(self) -> set[str]: """Update current alarm instances. @@ -453,6 +492,9 @@ class SonosSpeaker: dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) dispatcher_send(self.hass, SONOS_ALARM_UPDATE) + # + # Battery management + # async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" self._last_battery_event = dt_util.utcnow() @@ -477,11 +519,6 @@ class SonosSpeaker: ): self.battery_info = battery_info - @property - def is_coordinator(self) -> bool: - """Return true if player is a coordinator.""" - return self.coordinator is None - @property def power_source(self) -> str | None: """Return the name of the current power source. @@ -516,6 +553,9 @@ class SonosSpeaker: self.battery_info = battery_info self.async_write_entity_states() + # + # Group management + # def update_groups(self, event: SonosEvent | None = None) -> None: """Handle callback for topology change event.""" coro = self.create_update_groups_coro(event) @@ -786,11 +826,9 @@ class SonosSpeaker: for speaker in hass.data[DATA_SONOS].discovered.values(): speaker.soco._zgs_cache.clear() # pylint: disable=protected-access - @property - def favorites(self) -> SonosFavorites: - """Return the SonosFavorites instance for this household.""" - return self.hass.data[DATA_SONOS].favorites[self.household_id] - + # + # Media and playback state handlers + # def update_volume(self) -> None: """Update information about current volume settings.""" self.volume = self.soco.volume @@ -951,11 +989,3 @@ class SonosSpeaker: elif update_media_position: self.media.position = current_position self.media.position_updated_at = dt_util.utcnow() - - @property - def subscription_address(self) -> str | None: - """Return the current subscription callback address if any.""" - if self._subscriptions: - addr, port = self._subscriptions[0].event_listener.address - return ":".join([addr, str(port)]) - return None From d236e07046442c1b7630cc87b1067c6eca4ca6f2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 30 May 2021 23:03:53 -0500 Subject: [PATCH 813/852] Skip processed Sonos alarm updates (#51217) * Skip processed Sonos alarm updates * Fix bad conflict merge --- homeassistant/components/sonos/__init__.py | 3 ++- homeassistant/components/sonos/speaker.py | 10 ++++++++++ tests/components/sonos/conftest.py | 11 ++++++++++- tests/components/sonos/test_switch.py | 1 + 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index a904ae58db6..7f772aec6af 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections import OrderedDict +from collections import OrderedDict, deque import datetime import logging import socket @@ -73,6 +73,7 @@ class SonosData: self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict() self.favorites: dict[str, SonosFavorites] = {} self.alarms: dict[str, Alarm] = {} + self.processed_alarm_events = deque(maxlen=5) self.topology_condition = asyncio.Condition() self.discovery_thread = None self.hosts_heartbeat = None diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 701fe5aa8c4..acd53e1f877 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections import deque from collections.abc import Coroutine import contextlib import datetime @@ -282,6 +283,11 @@ class SonosSpeaker: """Return true if player is a coordinator.""" return self.coordinator is None + @property + def processed_alarm_events(self) -> deque[str]: + """Return the container of processed alarm events.""" + return self.hass.data[DATA_SONOS].processed_alarm_events + @property def subscription_address(self) -> str | None: """Return the current subscription callback address if any.""" @@ -366,6 +372,10 @@ class SonosSpeaker: @callback def async_dispatch_alarms(self, event: SonosEvent) -> None: """Create a task to update alarms from an event.""" + update_id = event.variables["alarm_list_version"] + if update_id in self.processed_alarm_events: + return + self.processed_alarm_events.append(update_id) self.hass.async_add_executor_job(self.update_alarms) @callback diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index fc5ef84c2d6..62fd3254d60 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -30,6 +30,15 @@ class SonosMockEvent: self.service = service self.variables = variables + def increment_variable(self, var_name): + """Increment the value of the var_name key in variables dict attribute. + + Assumes value has a format of :. + """ + base, count = self.variables[var_name].split(":") + newcount = int(count) + 1 + self.variables[var_name] = ":".join([base, str(newcount)]) + @pytest.fixture(name="config_entry") def config_entry_fixture(): @@ -165,7 +174,7 @@ def alarm_event_fixture(soco): "time_zone": "ffc40a000503000003000502ffc4", "time_server": "0.sonostime.pool.ntp.org,1.sonostime.pool.ntp.org,2.sonostime.pool.ntp.org,3.sonostime.pool.ntp.org", "time_generation": "20000001", - "alarm_list_version": "RINCON_test", + "alarm_list_version": "RINCON_test:1", "time_format": "INV", "date_format": "INV", "daily_index_refresh_time": None, diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index d4448d22b32..41cb241d377 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -68,6 +68,7 @@ async def test_alarm_create_delete( assert "switch.sonos_alarm_15" in entity_registry.entities alarm_clock_extended.ListAlarms.return_value = alarm_clock.ListAlarms.return_value + alarm_event.increment_variable("alarm_list_version") sub_callback(event=alarm_event) await hass.async_block_till_done() From 99fd5be36983ef152d3eb2020bc7f48f553ff7bb Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Sat, 29 May 2021 18:50:45 +0200 Subject: [PATCH 814/852] Bump pyialarm to 1.7 (#51233) --- homeassistant/components/ialarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 5cdc0ead3ea..e112a26003e 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": ["pyialarm==1.5"], + "requirements": ["pyialarm==1.7"], "codeowners": ["@RyuzakiKK"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 55d07ae773d..4c74f67ad19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1464,7 +1464,7 @@ pyhomematic==0.1.72 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.5 +pyialarm==1.7 # homeassistant.components.icloud pyicloud==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f95763e54f..44b7c1bc799 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -808,7 +808,7 @@ pyhiveapi==0.4.2 pyhomematic==0.1.72 # homeassistant.components.ialarm -pyialarm==1.5 +pyialarm==1.7 # homeassistant.components.icloud pyicloud==0.10.2 From 8fd3761893014069858e09d42f64d37f1dccd024 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 29 May 2021 16:00:36 +0200 Subject: [PATCH 815/852] Fix flaky statistics tests (#51242) --- tests/components/history/test_init.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 36dd3f30156..bf8d34e6ffe 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,6 +5,7 @@ import json from unittest.mock import patch, sentinel import pytest +from pytest import approx from homeassistant.components import history, recorder from homeassistant.components.recorder.history import get_significant_states @@ -884,9 +885,9 @@ async def test_statistics_during_period(hass, hass_ws_client): { "statistic_id": "sensor.test", "start": now.isoformat(), - "mean": 10.0, - "min": 10.0, - "max": 10.0, + "mean": approx(10.0), + "min": approx(10.0), + "max": approx(10.0), "last_reset": None, "state": None, "sum": None, From 9dfd578b65ae85a698272cf2f88d04261e4b7edc Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 31 May 2021 05:55:45 +0200 Subject: [PATCH 816/852] Fix unnecessary API calls in Netatmo (#51260) --- homeassistant/components/netatmo/data_handler.py | 8 +++++++- homeassistant/components/netatmo/sensor.py | 12 +++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 1c092c40930..e93e602d6a7 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -158,7 +158,13 @@ class NetatmoDataHandler: ): """Register data class.""" if data_class_entry in self.data_classes: - self.data_classes[data_class_entry]["subscriptions"].append(update_callback) + if ( + update_callback + not in self.data_classes[data_class_entry]["subscriptions"] + ): + self.data_classes[data_class_entry]["subscriptions"].append( + update_callback + ) return self.data_classes[data_class_entry] = { diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index eeaab52a21a..ed75ddf2f7f 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -130,7 +130,7 @@ PUBLIC = "public" async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - platform_not_ready = False + platform_not_ready = True async def find_entities(data_class_name): """Find all entities.""" @@ -183,8 +183,8 @@ async def async_setup_entry(hass, entry, async_add_entities): await data_handler.register_data_class(data_class_name, data_class_name, None) data_class = data_handler.data.get(data_class_name) - if not data_class or not data_class.raw_data: - platform_not_ready = True + if not (data_class and data_class.raw_data): + platform_not_ready = False async_add_entities(await find_entities(data_class_name), True) @@ -226,6 +226,12 @@ async def async_setup_entry(hass, entry, async_add_entities): lat_sw=area.lat_sw, lon_sw=area.lon_sw, ) + data_class = data_handler.data.get(signal_name) + + if not (data_class and data_class.raw_data): + nonlocal platform_not_ready + platform_not_ready = False + for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: new_entities.append( NetatmoPublicSensor(data_handler, area, sensor_type) From c20ac0efb2f5ca906ea15597c78ef5f687f09fab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 May 2021 21:04:13 -0700 Subject: [PATCH 817/852] Updated frontend to 20210531.0 (#51281) --- 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 9ee97c851e9..61da986b06b 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==20210528.0" + "home-assistant-frontend==20210531.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7e6be2f0016..81bbaf1ff5d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210528.0 +home-assistant-frontend==20210531.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4c74f67ad19..c72b147bf38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210528.0 +home-assistant-frontend==20210531.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44b7c1bc799..5ff64645bcc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210528.0 +home-assistant-frontend==20210531.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 792d7bb3f522b25a46b6e102c238b80edbdccd63 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 May 2021 21:13:41 -0700 Subject: [PATCH 818/852] Bumped version to 2021.6.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a38655119bc..76015e87360 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From a08fffea1783cbcb6d0db331bc3bc89a47a28cec Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 31 May 2021 23:38:33 +0200 Subject: [PATCH 819/852] Fix Garmin Connect integration with python-garminconnect-aio (#50865) --- .../components/garmin_connect/__init__.py | 51 ++++++++----------- .../components/garmin_connect/config_flow.py | 19 ++++--- .../components/garmin_connect/const.py | 5 +- .../components/garmin_connect/manifest.json | 2 +- .../components/garmin_connect/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../garmin_connect/test_config_flow.py | 40 ++++++++++----- 8 files changed, 67 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index 4ac157707fc..45c71bf1f07 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -1,8 +1,8 @@ """The Garmin Connect integration.""" -from datetime import date, timedelta +from datetime import date import logging -from garminconnect import ( +from garminconnect_aio import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -13,25 +13,27 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle -from .const import DOMAIN +from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] -MIN_SCAN_INTERVAL = timedelta(minutes=10) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Garmin Connect from a config entry.""" - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] - garmin_client = Garmin(username, password) + websession = async_get_clientsession(hass) + username: str = entry.data[CONF_USERNAME] + password: str = entry.data[CONF_PASSWORD] + + garmin_client = Garmin(websession, username, password) try: - await hass.async_add_executor_job(garmin_client.login) + await garmin_client.login() except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, @@ -73,38 +75,29 @@ class GarminConnectData: self.client = client self.data = None - async def _get_combined_alarms_of_all_devices(self): - """Combine the list of active alarms from all garmin devices.""" - alarms = [] - devices = await self.hass.async_add_executor_job(self.client.get_devices) - for device in devices: - device_settings = await self.hass.async_add_executor_job( - self.client.get_device_settings, device["deviceId"] - ) - alarms += device_settings["alarms"] - return alarms - - @Throttle(MIN_SCAN_INTERVAL) + @Throttle(DEFAULT_UPDATE_INTERVAL) async def async_update(self): - """Update data via library.""" + """Update data via API wrapper.""" today = date.today() try: - self.data = await self.hass.async_add_executor_job( - self.client.get_stats_and_body, today.isoformat() - ) - self.data["nextAlarm"] = await self._get_combined_alarms_of_all_devices() + summary = await self.client.get_user_summary(today.isoformat()) + body = await self.client.get_body_composition(today.isoformat()) + + self.data = { + **summary, + **body["totalAverage"], + } + self.data["nextAlarm"] = await self.client.get_device_alarms() except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, GarminConnectConnectionError, ) as err: _LOGGER.error( - "Error occurred during Garmin Connect get activity request: %s", err + "Error occurred during Garmin Connect update requests: %s", err ) - return except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Unknown error occurred during Garmin Connect get activity request" + "Unknown error occurred during Garmin Connect update requests" ) - return diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py index 218a98ba9a4..8e26e2bf608 100644 --- a/homeassistant/components/garmin_connect/config_flow.py +++ b/homeassistant/components/garmin_connect/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Garmin Connect integration.""" import logging -from garminconnect import ( +from garminconnect_aio import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -37,11 +38,15 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return await self._show_setup_form() - garmin_client = Garmin(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + websession = async_get_clientsession(self.hass) + + garmin_client = Garmin( + websession, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) errors = {} try: - await self.hass.async_add_executor_job(garmin_client.login) + username = await garmin_client.login() except GarminConnectConnectionError: errors["base"] = "cannot_connect" return await self._show_setup_form(errors) @@ -56,15 +61,13 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" return await self._show_setup_form(errors) - unique_id = garmin_client.get_full_name() - - await self.async_set_unique_id(unique_id) + await self.async_set_unique_id(username) self._abort_if_unique_id_configured() return self.async_create_entry( - title=unique_id, + title=username, data={ - CONF_ID: unique_id, + CONF_ID: username, CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], }, diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index 991ac90526a..19ed4ca4d94 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -1,4 +1,6 @@ """Constants for the Garmin Connect integration.""" +from datetime import timedelta + from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, LENGTH_METERS, @@ -8,7 +10,8 @@ from homeassistant.const import ( ) DOMAIN = "garmin_connect" -ATTRIBUTION = "Data provided by garmin.com" +ATTRIBUTION = "connect.garmin.com" +DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10) GARMIN_ENTITY_LIST = { "totalSteps": ["Total Steps", "steps", "mdi:walk", None, True], diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 913e85de954..2495249e4a4 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect==0.1.19"], + "requirements": ["garminconnect_aio==0.1.1"], "codeowners": ["@cyberjunky"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 0d946d5e88e..eb1690c9765 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from garminconnect import ( +from garminconnect_aio import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, diff --git a/requirements_all.txt b/requirements_all.txt index c72b147bf38..67c8adb70ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect==0.1.19 +garminconnect_aio==0.1.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ff64645bcc..e4aa37d8634 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect==0.1.19 +garminconnect_aio==0.1.1 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index f3784d5e2e2..2ad36ffa29c 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Garmin Connect config flow.""" from unittest.mock import patch -from garminconnect import ( +from garminconnect_aio import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, @@ -15,7 +15,7 @@ from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry MOCK_CONF = { - CONF_ID: "First Lastname", + CONF_ID: "my@email.address", CONF_USERNAME: "my@email.address", CONF_PASSWORD: "mypassw0rd", } @@ -23,27 +23,33 @@ MOCK_CONF = { @pytest.fixture(name="mock_garmin_connect") def mock_garmin(): - """Mock Garmin.""" + """Mock Garmin Connect.""" with patch( "homeassistant.components.garmin_connect.config_flow.Garmin", ) as garmin: - garmin.return_value.get_full_name.return_value = MOCK_CONF[CONF_ID] + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] yield garmin.return_value async def test_show_form(hass): """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result["errors"] == {} + assert result["step_id"] == config_entries.SOURCE_USER -async def test_step_user(hass, mock_garmin_connect): +async def test_step_user(hass): """Test registering an integration and finishing flow works.""" with patch( + "homeassistant.components.garmin_connect.Garmin.login", + return_value="my@email.address", + ), patch( "homeassistant.components.garmin_connect.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_init( @@ -95,12 +101,18 @@ async def test_unknown_error(hass, mock_garmin_connect): assert result["errors"] == {"base": "unknown"} -async def test_abort_if_already_setup(hass, mock_garmin_connect): +async def test_abort_if_already_setup(hass): """Test abort if already setup.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + MockConfigEntry( + domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID] + ).add_to_hass(hass) + with patch( + "homeassistant.components.garmin_connect.config_flow.Garmin", autospec=True + ) as garmin: + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 273f57261c24ebc5e378b7b8a688a058b72e6e6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 May 2021 13:47:12 -0500 Subject: [PATCH 820/852] Upgrade HAP-python to 3.5.0 (#51261) * Upgrade HAP-python to 3.4.2 - Fixes for malformed event sending - Performance improvements * Bump * update tests to point to async --- homeassistant/components/homekit/__init__.py | 8 ++++---- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/conftest.py | 2 +- tests/components/homekit/test_homekit.py | 17 ++++++++++------- tests/components/homekit/test_type_cameras.py | 2 +- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 87742104b86..ef228f3ae60 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -456,7 +456,7 @@ class HomeKit: self.bridge = None self.driver = None - def setup(self, zeroconf_instance): + def setup(self, async_zeroconf_instance): """Set up bridge and accessory driver.""" ip_addr = self._ip_address or get_local_ip() persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) @@ -471,7 +471,7 @@ class HomeKit: port=self._port, persist_file=persist_file, advertised_address=self._advertise_ip, - zeroconf_instance=zeroconf_instance, + async_zeroconf_instance=async_zeroconf_instance, ) # If we do not load the mac address will be wrong @@ -595,8 +595,8 @@ class HomeKit: if self.status != STATUS_READY: return self.status = STATUS_WAIT - zc_instance = await zeroconf.async_get_instance(self.hass) - await self.hass.async_add_executor_job(self.setup, zc_instance) + async_zc_instance = await zeroconf.async_get_async_instance(self.hass) + await self.hass.async_add_executor_job(self.setup, async_zc_instance) self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id) await self.aid_storage.async_initialize() await self._async_create_accessories() diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 483279d55f3..d2c2f094a0f 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/integrations/homekit", "requirements": [ - "HAP-python==3.4.1", + "HAP-python==3.5.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index 67c8adb70ac..3ca655541aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.4.1 +HAP-python==3.5.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4aa37d8634..a42813bf174 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.homekit -HAP-python==3.4.1 +HAP-python==3.5.0 # homeassistant.components.flick_electric PyFlick==0.0.2 diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 469a0a7deb7..5441bcc195c 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -12,7 +12,7 @@ from tests.common import async_capture_events @pytest.fixture def hk_driver(loop): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" - with patch("pyhap.accessory_driver.Zeroconf"), patch( + with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" ), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch( "pyhap.accessory_driver.HAPServer.async_start" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index fd7d74aeaba..bd7af3b3596 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -250,7 +250,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - zeroconf_instance=zeroconf_mock, + async_zeroconf_instance=zeroconf_mock, ) assert homekit.driver.safe_mode is False @@ -290,7 +290,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - zeroconf_instance=mock_zeroconf, + async_zeroconf_instance=mock_zeroconf, ) @@ -315,10 +315,10 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): entry_title=entry.title, ) - zeroconf_instance = MagicMock() + async_zeroconf_instance = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: - await hass.async_add_executor_job(homekit.setup, zeroconf_instance) + await hass.async_add_executor_job(homekit.setup, async_zeroconf_instance) mock_driver.assert_called_with( hass, entry.entry_id, @@ -329,7 +329,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address="192.168.1.100", - zeroconf_instance=zeroconf_instance, + async_zeroconf_instance=async_zeroconf_instance, ) @@ -851,7 +851,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): options={}, ) assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}}) - system_zc = await zeroconf.async_get_instance(hass) + system_async_zc = await zeroconf.async_get_async_instance(hass) with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( f"{PATH_HOMEKIT}.HomeKit.async_stop" @@ -859,7 +859,10 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN][entry.entry_id][HOMEKIT].driver.advertiser == system_zc + assert ( + hass.data[DOMAIN][entry.entry_id][HOMEKIT].driver.advertiser + == system_async_zc + ) assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index ba08ea3caaf..354db900470 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -80,7 +80,7 @@ async def _async_stop_stream(hass, acc, session_info): @pytest.fixture() def run_driver(hass): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" - with patch("pyhap.accessory_driver.Zeroconf"), patch( + with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" ), patch("pyhap.accessory_driver.HAPServer"), patch( "pyhap.accessory_driver.AccessoryDriver.publish" From c4a98755a38cf2e56fc7d01437335af5e3982fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 31 May 2021 14:06:11 +0200 Subject: [PATCH 821/852] Resolve addon repository slug for device registry (#51287) * Resolve addon repository slug for device registry * typo * Adjust onboarding test * Use /store --- homeassistant/components/hassio/__init__.py | 28 +++++++++++++++++++-- homeassistant/components/hassio/handler.py | 8 ++++++ tests/components/hassio/test_init.py | 23 +++++++++++------ tests/components/onboarding/test_views.py | 3 +++ 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e33c689c59e..d391817f964 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -71,6 +71,7 @@ CONFIG_SCHEMA = vol.Schema( DATA_CORE_INFO = "hassio_core_info" DATA_HOST_INFO = "hassio_host_info" +DATA_STORE = "hassio_store" DATA_INFO = "hassio_info" DATA_OS_INFO = "hassio_os_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" @@ -291,6 +292,16 @@ def get_host_info(hass): return hass.data.get(DATA_HOST_INFO) +@callback +@bind_hass +def get_store(hass): + """Return store information. + + Async friendly. + """ + return hass.data.get(DATA_STORE) + + @callback @bind_hass def get_supervisor_info(hass): @@ -456,6 +467,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: try: hass.data[DATA_INFO] = await hassio.get_info() hass.data[DATA_HOST_INFO] = await hassio.get_host_info() + hass.data[DATA_STORE] = await hassio.get_store() hass.data[DATA_CORE_INFO] = await hassio.get_core_info() hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info() hass.data[DATA_OS_INFO] = await hassio.get_os_info() @@ -627,10 +639,22 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" new_data = {} - addon_data = get_supervisor_info(self.hass) + supervisor_info = get_supervisor_info(self.hass) + store_data = get_store(self.hass) + + repositories = { + repo[ATTR_SLUG]: repo[ATTR_NAME] + for repo in store_data.get("repositories", []) + } new_data["addons"] = { - addon[ATTR_SLUG]: addon for addon in addon_data.get("addons", []) + addon[ATTR_SLUG]: { + **addon, + ATTR_REPOSITORY: repositories.get( + addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") + ), + } + for addon in supervisor_info.get("addons", []) } if self.is_hass_os: new_data["os"] = get_os_info(self.hass) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 301d353faf0..37b645eb7d3 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -118,6 +118,14 @@ class HassIO: """ return self.send_command(f"/addons/{addon}/info", method="get") + @api_data + def get_store(self): + """Return data from the store. + + This method return a coroutine. + """ + return self.send_command("/store", method="get") + @api_data def get_ingress_panels(self): """Return data for Add-on ingress panels. diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 5bf8a45ab52..7e9d7cd91c8 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -32,6 +32,13 @@ def mock_all(aioclient_mock, request): "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, }, ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) aioclient_mock.get( "http://127.0.0.1/host/info", json={ @@ -67,6 +74,7 @@ def mock_all(aioclient_mock, request): "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", + "repository": "core", "url": "https://github.com/home-assistant/addons/test", }, { @@ -76,6 +84,7 @@ def mock_all(aioclient_mock, request): "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", + "repository": "core", "url": "https://github.com", }, ], @@ -92,7 +101,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -131,7 +140,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -147,7 +156,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -159,7 +168,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -206,7 +215,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -220,7 +229,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -237,7 +246,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index d8ae50b851f..a921dfe39d4 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -73,6 +73,9 @@ async def mock_supervisor_fixture(hass, aioclient_mock): ), patch( "homeassistant.components.hassio.HassIO.get_host_info", return_value={}, + ), patch( + "homeassistant.components.hassio.HassIO.get_store", + return_value={}, ), patch( "homeassistant.components.hassio.HassIO.get_supervisor_info", return_value={"diagnostics": True}, From b3ccc44ee9749351023401cb8bf3dc232a176c1f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 31 May 2021 14:03:26 +0200 Subject: [PATCH 822/852] Revert "GRPC is fixed, don't need a workaround" (#51289) This reverts commit 9d174e8a0504a83831530ef9c9178bbbe5a58872. --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 81bbaf1ff5d..e7e458efd70 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -48,6 +48,10 @@ h11>=0.12.0 # https://github.com/advisories/GHSA-93xj-8mrv-444m httplib2>=0.19.0 +# gRPC 1.32+ currently causes issues on ARMv7, see: +# https://github.com/home-assistant/core/issues/40148 +grpcio==1.31.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 79d4c05b0b6..4fd96cb1b04 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,6 +68,10 @@ h11>=0.12.0 # https://github.com/advisories/GHSA-93xj-8mrv-444m httplib2>=0.19.0 +# gRPC 1.32+ currently causes issues on ARMv7, see: +# https://github.com/home-assistant/core/issues/40148 +grpcio==1.31.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From 127a230703bdcdeb0bd0c00ec0a3302e11f3d5cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 15:36:40 -0700 Subject: [PATCH 823/852] Add system option to disable polling (#51299) --- .../components/config/config_entries.py | 36 +++++---- homeassistant/config_entries.py | 27 +++++-- homeassistant/helpers/entity_platform.py | 7 +- homeassistant/helpers/update_coordinator.py | 9 ++- .../components/config/test_config_entries.py | 73 +++++++++++++------ tests/helpers/test_entity_platform.py | 13 ++++ tests/helpers/test_update_coordinator.py | 12 ++- 7 files changed, 123 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index efc60288439..9d88a9b5311 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -31,7 +31,6 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command(config_entry_disable) hass.components.websocket_api.async_register_command(config_entry_update) hass.components.websocket_api.async_register_command(config_entries_progress) - hass.components.websocket_api.async_register_command(system_options_list) hass.components.websocket_api.async_register_command(system_options_update) hass.components.websocket_api.async_register_command(ignore_config_flow) @@ -231,20 +230,6 @@ def config_entries_progress(hass, connection, msg): ) -@websocket_api.require_admin -@websocket_api.async_response -@websocket_api.websocket_command( - {"type": "config_entries/system_options/list", "entry_id": str} -) -async def system_options_list(hass, connection, msg): - """List all system options for a config entry.""" - entry_id = msg["entry_id"] - entry = hass.config_entries.async_get_entry(entry_id) - - if entry: - connection.send_result(msg["id"], entry.system_options.as_dict()) - - def send_entry_not_found(connection, msg_id): """Send Config entry not found error.""" connection.send_error( @@ -267,6 +252,7 @@ def get_entry(hass, connection, entry_id, msg_id): "type": "config_entries/system_options/update", "entry_id": str, vol.Optional("disable_new_entities"): bool, + vol.Optional("disable_polling"): bool, } ) async def system_options_update(hass, connection, msg): @@ -280,8 +266,25 @@ async def system_options_update(hass, connection, msg): if entry is None: return + old_disable_polling = entry.system_options.disable_polling + hass.config_entries.async_update_entry(entry, system_options=changes) - connection.send_result(msg["id"], entry.system_options.as_dict()) + + result = { + "system_options": entry.system_options.as_dict(), + "require_restart": False, + } + + if ( + old_disable_polling != entry.system_options.disable_polling + and entry.state is config_entries.ConfigEntryState.LOADED + ): + if not await hass.config_entries.async_reload(entry.entry_id): + result["require_restart"] = ( + entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD + ) + + connection.send_result(msg["id"], result) @websocket_api.require_admin @@ -388,6 +391,7 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "state": entry.state.value, "supports_options": supports_options, "supports_unload": entry.supports_unload, + "system_options": entry.system_options.as_dict(), "disabled_by": entry.disabled_by, "reason": entry.reason, } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b3589d03b92..33c18fc0d7c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -994,12 +994,10 @@ class ConfigEntries: changed = True entry.options = MappingProxyType(options) - if ( - system_options is not UNDEFINED - and entry.system_options.as_dict() != system_options - ): - changed = True + if system_options is not UNDEFINED: + old_system_options = entry.system_options.as_dict() entry.system_options.update(**system_options) + changed = entry.system_options.as_dict() != old_system_options if not changed: return False @@ -1408,14 +1406,27 @@ class SystemOptions: """Config entry system options.""" disable_new_entities: bool = attr.ib(default=False) + disable_polling: bool = attr.ib(default=False) - def update(self, *, disable_new_entities: bool) -> None: + def update( + self, + *, + disable_new_entities: bool | UndefinedType = UNDEFINED, + disable_polling: bool | UndefinedType = UNDEFINED, + ) -> None: """Update properties.""" - self.disable_new_entities = disable_new_entities + if disable_new_entities is not UNDEFINED: + self.disable_new_entities = disable_new_entities + + if disable_polling is not UNDEFINED: + self.disable_polling = disable_polling def as_dict(self) -> dict[str, Any]: """Return dictionary version of this config entries system options.""" - return {"disable_new_entities": self.disable_new_entities} + return { + "disable_new_entities": self.disable_new_entities, + "disable_polling": self.disable_polling, + } class EntityRegistryDisabledHandler: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 26bfdb43e66..f0d691a1c8d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -214,6 +214,7 @@ class EntityPlatform: @callback def async_create_setup_task() -> Coroutine: """Get task to set up platform.""" + config_entries.current_entry.set(config_entry) return platform.async_setup_entry( # type: ignore[no-any-return,union-attr] self.hass, config_entry, self._async_schedule_add_entities ) @@ -395,8 +396,10 @@ class EntityPlatform: ) raise - if self._async_unsub_polling is not None or not any( - entity.should_poll for entity in self.entities.values() + if ( + (self.config_entry and self.config_entry.system_options.disable_polling) + or self._async_unsub_polling is not None + or not any(entity.should_poll for entity in self.entities.values()) ): return diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index c15d6534626..e91acfaf82f 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -49,6 +49,7 @@ class DataUpdateCoordinator(Generic[T]): self.name = name self.update_method = update_method self.update_interval = update_interval + self.config_entry = config_entries.current_entry.get() # It's None before the first successful update. # Components should call async_config_entry_first_refresh @@ -110,6 +111,9 @@ class DataUpdateCoordinator(Generic[T]): if self.update_interval is None: return + if self.config_entry and self.config_entry.system_options.disable_polling: + return + if self._unsub_refresh: self._unsub_refresh() self._unsub_refresh = None @@ -229,9 +233,8 @@ class DataUpdateCoordinator(Generic[T]): if raise_on_auth_failed: raise - config_entry = config_entries.current_entry.get() - if config_entry: - config_entry.async_start_reauth(self.hass) + if self.config_entry: + self.config_entry.async_start_reauth(self.hass) except NotImplementedError as err: self.last_exception = err raise err diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 10fc3aadba0..570d847e86e 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -87,6 +87,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": True, "supports_unload": True, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": None, "reason": None, }, @@ -97,6 +101,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.SETUP_ERROR.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": None, "reason": "Unsupported API", }, @@ -107,6 +115,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": core_ce.DISABLED_USER, "reason": None, }, @@ -328,6 +340,10 @@ async def test_create_account(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "title": "Test Entry", "reason": None, }, @@ -399,6 +415,10 @@ async def test_two_step_flow(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "title": "user-title", "reason": None, }, @@ -678,35 +698,17 @@ async def test_two_step_options_flow(hass, client): } -async def test_list_system_options(hass, hass_ws_client): - """Test that we can list an entries system options.""" - assert await async_setup_component(hass, "config", {}) - ws_client = await hass_ws_client(hass) - - entry = MockConfigEntry(domain="demo") - entry.add_to_hass(hass) - - await ws_client.send_json( - { - "id": 5, - "type": "config_entries/system_options/list", - "entry_id": entry.entry_id, - } - ) - response = await ws_client.receive_json() - - assert response["success"] - assert response["result"] == {"disable_new_entities": False} - - async def test_update_system_options(hass, hass_ws_client): """Test that we can update system options.""" assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) - entry = MockConfigEntry(domain="demo") + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) + assert entry.system_options.disable_new_entities is False + assert entry.system_options.disable_polling is False + await ws_client.send_json( { "id": 5, @@ -718,8 +720,31 @@ async def test_update_system_options(hass, hass_ws_client): response = await ws_client.receive_json() assert response["success"] - assert response["result"]["disable_new_entities"] - assert entry.system_options.disable_new_entities + assert response["result"] == { + "require_restart": False, + "system_options": {"disable_new_entities": True, "disable_polling": False}, + } + assert entry.system_options.disable_new_entities is True + assert entry.system_options.disable_polling is False + + await ws_client.send_json( + { + "id": 6, + "type": "config_entries/system_options/update", + "entry_id": entry.entry_id, + "disable_new_entities": False, + "disable_polling": True, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == { + "require_restart": True, + "system_options": {"disable_new_entities": False, "disable_polling": True}, + } + assert entry.system_options.disable_new_entities is False + assert entry.system_options.disable_polling is True async def test_update_system_options_nonexisting(hass, hass_ws_client): diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 6675e441adf..944f02d46c0 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -57,6 +57,19 @@ async def test_polling_only_updates_entities_it_should_poll(hass): assert poll_ent.async_update.called +async def test_polling_disabled_by_config_entry(hass): + """Test the polling of only updated entities.""" + entity_platform = MockEntityPlatform(hass) + entity_platform.config_entry = MockConfigEntry( + system_options={"disable_polling": True} + ) + + poll_ent = MockEntity(should_poll=True) + + await entity_platform.async_add_entities([poll_ent]) + assert entity_platform._async_unsub_polling is None + + async def test_polling_updates_entities_with_exception(hass): """Test the updated entities that not break with an exception.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 244e221f53a..a0ce751aed8 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -9,13 +9,14 @@ import aiohttp import pytest import requests +from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -371,3 +372,12 @@ async def test_async_config_entry_first_refresh_success(crd, caplog): await crd.async_config_entry_first_refresh() assert crd.last_update_success is True + + +async def test_not_schedule_refresh_if_system_option_disable_polling(hass): + """Test we do not schedule a refresh if disable polling in config entry.""" + entry = MockConfigEntry(system_options={"disable_polling": True}) + config_entries.current_entry.set(entry) + crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) + crd.async_add_listener(lambda: None) + assert crd._unsub_refresh is None From 1e2913ad4cd738f665493acb3dc608f0a00b70e9 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 31 May 2021 23:35:33 +0200 Subject: [PATCH 824/852] Fix stream profiles not available as expected (#51305) --- homeassistant/components/axis/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 8753114d86e..d313e4b2745 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -243,8 +243,7 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlow): # Stream profiles - if vapix.params.stream_profiles_max_groups > 0: - + if vapix.stream_profiles or vapix.params.stream_profiles_max_groups > 0: stream_profiles = [DEFAULT_STREAM_PROFILE] for profile in vapix.streaming_profiles: stream_profiles.append(profile.name) From cbc75ffe8a01fb1c57e2d0b3167033f345bbd50b Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 31 May 2021 23:28:14 +0100 Subject: [PATCH 825/852] Bump aiohomekit to 0.2.66 (#51310) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 46fe126ebf0..c18ee9e574f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.2.65"], + "requirements": ["aiohomekit==0.2.66"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 3ca655541aa..3d27fa83bb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.65 +aiohomekit==0.2.66 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a42813bf174..04eaa89b32a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.65 +aiohomekit==0.2.66 # homeassistant.components.emulated_hue # homeassistant.components.http From 14db5a0999ce106a7855f4d95e5a664fa30d3f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 1 Jun 2021 00:32:03 +0200 Subject: [PATCH 826/852] Move version validation to resolver (#51311) --- homeassistant/loader.py | 60 +++++++++---------- tests/test_loader.py | 57 +++++++----------- .../test_bad_version/__init__.py | 1 + .../test_bad_version/manifest.json | 4 ++ .../test_no_version/__init__.py | 1 + .../test_no_version/manifest.json | 3 + 6 files changed, 58 insertions(+), 68 deletions(-) create mode 100644 tests/testing_config/custom_components/test_bad_version/__init__.py create mode 100644 tests/testing_config/custom_components/test_bad_version/manifest.json create mode 100644 tests/testing_config/custom_components/test_no_version/__init__.py create mode 100644 tests/testing_config/custom_components/test_no_version/manifest.json diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 444e35add33..06bf5045c9f 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -47,7 +47,7 @@ DATA_CUSTOM_COMPONENTS = "custom_components" PACKAGE_CUSTOM_COMPONENTS = "custom_components" PACKAGE_BUILTIN = "homeassistant.components" CUSTOM_WARNING = ( - "You are using a custom integration %s which has not " + "We found a custom integration %s which has not " "been tested by Home Assistant. This component might " "cause stability problems, be sure to disable it if you " "experience issues with Home Assistant" @@ -290,13 +290,39 @@ class Integration: ) continue - return cls( + integration = cls( hass, f"{root_module.__name__}.{domain}", manifest_path.parent, manifest, ) + if integration.is_built_in: + return integration + + _LOGGER.warning(CUSTOM_WARNING, integration.domain) + try: + AwesomeVersion( + integration.version, + [ + AwesomeVersionStrategy.CALVER, + AwesomeVersionStrategy.SEMVER, + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, + ], + ) + except AwesomeVersionException: + _LOGGER.error( + "The custom integration '%s' does not have a " + "valid version key (%s) in the manifest file and was blocked from loading. " + "See https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions for more details", + integration.domain, + integration.version, + ) + return None + return integration + return None def __init__( @@ -523,8 +549,6 @@ async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integratio # Instead of using resolve_from_root we use the cache of custom # components to find the integration. if integration := (await async_get_custom_components(hass)).get(domain): - validate_custom_integration_version(integration) - _LOGGER.warning(CUSTOM_WARNING, integration.domain) return integration from homeassistant import components # pylint: disable=import-outside-toplevel @@ -744,31 +768,3 @@ def _lookup_path(hass: HomeAssistant) -> list[str]: if hass.config.safe_mode: return [PACKAGE_BUILTIN] return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] - - -def validate_custom_integration_version(integration: Integration) -> None: - """ - Validate the version of custom integrations. - - Raises IntegrationNotFound when version is missing or not valid - """ - try: - AwesomeVersion( - integration.version, - [ - AwesomeVersionStrategy.CALVER, - AwesomeVersionStrategy.SEMVER, - AwesomeVersionStrategy.SIMPLEVER, - AwesomeVersionStrategy.BUILDVER, - AwesomeVersionStrategy.PEP440, - ], - ) - except AwesomeVersionException: - _LOGGER.error( - "The custom integration '%s' does not have a " - "valid version key (%s) in the manifest file and was blocked from loading. " - "See https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions for more details", - integration.domain, - integration.version, - ) - raise IntegrationNotFound(integration.domain) from None diff --git a/tests/test_loader.py b/tests/test_loader.py index e696f27351d..20dcf90d90e 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -127,37 +127,30 @@ async def test_log_warning_custom_component(hass, caplog, enable_custom_integrat """Test that we log a warning when loading a custom component.""" await loader.async_get_integration(hass, "test_package") - assert "You are using a custom integration test_package" in caplog.text + assert "We found a custom integration test_package" in caplog.text await loader.async_get_integration(hass, "test") - assert "You are using a custom integration test " in caplog.text + assert "We found a custom integration test " in caplog.text -async def test_custom_integration_version_not_valid(hass, caplog): +async def test_custom_integration_version_not_valid( + hass, caplog, enable_custom_integrations +): """Test that we log a warning when custom integrations have a invalid version.""" - test_integration1 = loader.Integration( - hass, "custom_components.test", None, {"domain": "test1", "version": "test"} - ) - test_integration2 = loader.Integration( - hass, "custom_components.test", None, {"domain": "test2"} + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") + + assert ( + "The custom integration 'test_no_version' does not have a valid version key (None) in the manifest file and was blocked from loading." + in caplog.text ) - with patch("homeassistant.loader.async_get_custom_components") as mock_get: - mock_get.return_value = {"test1": test_integration1, "test2": test_integration2} - - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test1") - assert ( - "The custom integration 'test1' does not have a valid version key (test) in the manifest file and was blocked from loading." - in caplog.text - ) - - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test2") - assert ( - "The custom integration 'test2' does not have a valid version key (None) in the manifest file and was blocked from loading." - in caplog.text - ) + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test2") + assert ( + "The custom integration 'test_bad_version' does not have a valid version key (bad) in the manifest file and was blocked from loading." + in caplog.text + ) async def test_get_integration(hass): @@ -471,19 +464,11 @@ async def test_get_custom_components_safe_mode(hass): async def test_custom_integration_missing_version(hass, caplog): """Test trying to load a custom integration without a version twice does not deadlock.""" - test_integration_1 = loader.Integration( - hass, "custom_components.test1", None, {"domain": "test1"} - ) - with patch("homeassistant.loader.async_get_custom_components") as mock_get: - mock_get.return_value = { - "test1": test_integration_1, - } + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test1") - - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test1") + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") async def test_custom_integration_missing(hass, caplog): diff --git a/tests/testing_config/custom_components/test_bad_version/__init__.py b/tests/testing_config/custom_components/test_bad_version/__init__.py new file mode 100644 index 00000000000..e39053682e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_bad_version/__init__.py @@ -0,0 +1 @@ +"""Provide a mock integration.""" diff --git a/tests/testing_config/custom_components/test_bad_version/manifest.json b/tests/testing_config/custom_components/test_bad_version/manifest.json new file mode 100644 index 00000000000..69d322a33ad --- /dev/null +++ b/tests/testing_config/custom_components/test_bad_version/manifest.json @@ -0,0 +1,4 @@ +{ + "domain": "test_bad_version", + "version": "bad" +} \ No newline at end of file diff --git a/tests/testing_config/custom_components/test_no_version/__init__.py b/tests/testing_config/custom_components/test_no_version/__init__.py new file mode 100644 index 00000000000..e39053682e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_no_version/__init__.py @@ -0,0 +1 @@ +"""Provide a mock integration.""" diff --git a/tests/testing_config/custom_components/test_no_version/manifest.json b/tests/testing_config/custom_components/test_no_version/manifest.json new file mode 100644 index 00000000000..9054cf4f5e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_no_version/manifest.json @@ -0,0 +1,3 @@ +{ + "domain": "test_no_version" +} \ No newline at end of file From a904b1e37fade819eb0020336785900baa2b4b1b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 16:35:31 -0700 Subject: [PATCH 827/852] Set up cloud semi-dependencies at start (#51313) --- .../components/cloud/alexa_config.py | 10 +++-- .../components/cloud/google_config.py | 9 +++-- homeassistant/core.py | 4 +- homeassistant/helpers/start.py | 25 ++++++++++++ tests/components/cloud/test_google_config.py | 1 + tests/helpers/test_start.py | 39 +++++++++++++++++++ 6 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 homeassistant/helpers/start.py create mode 100644 tests/helpers/test_start.py diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index c7568d7ae25..7394936f355 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -17,7 +17,7 @@ from homeassistant.components.alexa import ( ) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST from homeassistant.core import HomeAssistant, callback, split_entity_id -from homeassistant.helpers import entity_registry +from homeassistant.helpers import entity_registry, start from homeassistant.helpers.event import async_call_later from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -107,8 +107,12 @@ class AlexaConfig(alexa_config.AbstractConfig): async def async_initialize(self): """Initialize the Alexa config.""" - if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: - await async_setup_component(self.hass, ALEXA_DOMAIN, {}) + + async def hass_started(hass): + if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, ALEXA_DOMAIN, {}) + + start.async_at_start(self.hass, hass_started) def should_expose(self, entity_id): """If an entity should be exposed.""" diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 41f62c32c39..65cbe8bb342 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -9,7 +9,7 @@ from homeassistant.components.google_assistant.const import DOMAIN as GOOGLE_DOM from homeassistant.components.google_assistant.helpers import AbstractConfig from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_OK from homeassistant.core import CoreState, split_entity_id -from homeassistant.helpers import entity_registry +from homeassistant.helpers import entity_registry, start from homeassistant.setup import async_setup_component from .const import ( @@ -86,8 +86,11 @@ class CloudGoogleConfig(AbstractConfig): """Perform async initialization of config.""" await super().async_initialize() - if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: - await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + async def hass_started(hass): + if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + + start.async_at_start(self.hass, hass_started) # Remove old/wrong user agent ids remove_agent_user_ids = [] diff --git a/homeassistant/core.py b/homeassistant/core.py index 7b5c93b15bb..b9bf97e7e6c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -374,7 +374,7 @@ class HomeAssistant: return task - def create_task(self, target: Coroutine) -> None: + def create_task(self, target: Awaitable) -> None: """Add task to the executor pool. target: target to call. @@ -382,7 +382,7 @@ class HomeAssistant: self.loop.call_soon_threadsafe(self.async_create_task, target) @callback - def async_create_task(self, target: Coroutine) -> asyncio.tasks.Task: + def async_create_task(self, target: Awaitable) -> asyncio.tasks.Task: """Create a task from within the eventloop. This method must be run in the event loop. diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py new file mode 100644 index 00000000000..e7e827ec5c3 --- /dev/null +++ b/homeassistant/helpers/start.py @@ -0,0 +1,25 @@ +"""Helpers to help during startup.""" +from collections.abc import Awaitable +from typing import Callable + +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import Event, HomeAssistant, callback + + +@callback +def async_at_start( + hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Awaitable] +) -> None: + """Execute something when Home Assistant is started. + + Will execute it now if Home Assistant is already started. + """ + if hass.is_running: + hass.async_create_task(at_start_cb(hass)) + return + + async def _matched_event(event: Event) -> None: + """Call the callback when Home Assistant started.""" + await at_start_cb(hass) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index bc430347e08..64d50250259 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -234,6 +234,7 @@ async def test_setup_integration(hass, mock_conf, cloud_prefs): assert "google_assistant" not in hass.config.components await mock_conf.async_initialize() + await hass.async_block_till_done() assert "google_assistant" in hass.config.components hass.config.components.remove("google_assistant") diff --git a/tests/helpers/test_start.py b/tests/helpers/test_start.py new file mode 100644 index 00000000000..35838f1ceaa --- /dev/null +++ b/tests/helpers/test_start.py @@ -0,0 +1,39 @@ +"""Test starting HA helpers.""" +from homeassistant import core +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.helpers import start + + +async def test_at_start_when_running(hass): + """Test at start when already running.""" + assert hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_at_start_when_starting(hass): + """Test at start when yet to start.""" + hass.state = core.CoreState.not_running + assert not hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(calls) == 1 From 837efaf29b2860249cd30e30aac4c46295149aa3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 16:35:08 -0700 Subject: [PATCH 828/852] Updated frontend to 20210531.1 (#51314) --- 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 61da986b06b..49b51a7864c 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==20210531.0" + "home-assistant-frontend==20210531.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e7e458efd70..80203972831 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210531.0 +home-assistant-frontend==20210531.1 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 3d27fa83bb1..3f895847641 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.0 +home-assistant-frontend==20210531.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04eaa89b32a..009e7313c57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.0 +home-assistant-frontend==20210531.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From bd279786bb36cfe6e633be61335f05f00c041957 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 16:43:14 -0700 Subject: [PATCH 829/852] Bumped version to 2021.6.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 76015e87360..6900c6c0b15 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 413fd1b255061aff12277f7d07ca279fccc4ae5d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Jun 2021 03:51:44 -0700 Subject: [PATCH 830/852] Trusted networks auth provider warns if detects a requests with x-forwarded-for header while the http integration is not configured for reverse proxies (#51319) * Trusted networks auth provider to require http integration configured for proxies to allow logging in with requests with x-forwarded-for header * Make it a warning --- .../auth/providers/trusted_networks.py | 111 +++++++++++++----- homeassistant/components/http/__init__.py | 1 + tests/auth/providers/test_trusted_networks.py | 74 ++++++++++-- 3 files changed, 146 insertions(+), 40 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 2f120e56652..e93587e91ca 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -14,10 +14,13 @@ from ipaddress import ( ip_address, ip_network, ) +import logging from typing import Any, Dict, List, Union, cast +from aiohttp import hdrs import voluptuous as vol +from homeassistant.components import http from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -86,40 +89,60 @@ class TrustedNetworksAuthProvider(AuthProvider): """Trusted Networks auth provider does not support MFA.""" return False + @callback + def is_allowed_request(self) -> bool: + """Return if it is an allowed request.""" + request = http.current_request.get() + if request is not None and ( + self.hass.http.use_x_forwarded_for + or hdrs.X_FORWARDED_FOR not in request.headers + ): + return True + + logging.getLogger(__name__).warning( + "A request contained an x-forwarded-for header but your HTTP integration is not set-up " + "for reverse proxies. This usually means that you have not configured your reverse proxy " + "correctly. This request will be blocked in Home Assistant 2021.7 unless you configure " + "your HTTP integration to allow this header." + ) + return True + async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" assert context is not None + + if not self.is_allowed_request(): + return MisconfiguredTrustedNetworksLoginFlow(self) + ip_addr = cast(IPAddress, context.get("ip_address")) users = await self.store.async_get_users() available_users = [ user for user in users if not user.system_generated and user.is_active ] for ip_net, user_or_group_list in self.trusted_users.items(): - if ip_addr in ip_net: - user_list = [ - user_id - for user_id in user_or_group_list - if isinstance(user_id, str) - ] - group_list = [ - group[CONF_GROUP] - for group in user_or_group_list - if isinstance(group, dict) - ] - flattened_group_list = [ - group for sublist in group_list for group in sublist - ] - available_users = [ - user - for user in available_users - if ( - user.id in user_list - or any( - group.id in flattened_group_list for group in user.groups - ) - ) - ] - break + if ip_addr not in ip_net: + continue + + user_list = [ + user_id for user_id in user_or_group_list if isinstance(user_id, str) + ] + group_list = [ + group[CONF_GROUP] + for group in user_or_group_list + if isinstance(group, dict) + ] + flattened_group_list = [ + group for sublist in group_list for group in sublist + ] + available_users = [ + user + for user in available_users + if ( + user.id in user_list + or any(group.id in flattened_group_list for group in user.groups) + ) + ] + break return TrustedNetworksLoginFlow( self, @@ -136,13 +159,22 @@ class TrustedNetworksAuthProvider(AuthProvider): users = await self.store.async_get_users() for user in users: - if not user.system_generated and user.is_active and user.id == user_id: - for credential in await self.async_credentials(): - if credential.data["user_id"] == user_id: - return credential - cred = self.async_create_credentials({"user_id": user_id}) - await self.store.async_link_user(user, cred) - return cred + if user.id != user_id: + continue + + if user.system_generated: + continue + + if not user.is_active: + continue + + for credential in await self.async_credentials(): + if credential.data["user_id"] == user_id: + return credential + + cred = self.async_create_credentials({"user_id": user_id}) + await self.store.async_link_user(user, cred) + return cred # We only allow login as exist user raise InvalidUserError @@ -163,6 +195,11 @@ class TrustedNetworksAuthProvider(AuthProvider): Raise InvalidAuthError if not. Raise InvalidAuthError if trusted_networks is not configured. """ + if not self.is_allowed_request(): + raise InvalidAuthError( + "No request or it contains x-forwarded-for header and that's not allowed by configuration" + ) + if not self.trusted_networks: raise InvalidAuthError("trusted_networks is not configured") @@ -183,6 +220,16 @@ class TrustedNetworksAuthProvider(AuthProvider): self.async_validate_access(ip_address(remote_ip)) +class MisconfiguredTrustedNetworksLoginFlow(LoginFlow): + """Login handler for misconfigured trusted networks.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the step of the form.""" + return self.async_abort(reason="forwared_for_header_not_allowed") + + class TrustedNetworksLoginFlow(LoginFlow): """Handler for the login flow.""" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 8bd20e31628..db198cb334a 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -252,6 +252,7 @@ class HomeAssistantHTTP: self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port + self.use_x_forwarded_for = use_x_forwarded_for self.trusted_proxies = trusted_proxies self.is_ban_enabled = is_ban_enabled self.ssl_profile = ssl_profile diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 39764fa4206..c68d6651e3c 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -5,9 +5,12 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol -from homeassistant import auth +from homeassistant import auth, const from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth +from homeassistant.setup import async_setup_component + +FORWARD_FOR_IS_WARNING = (const.MAJOR_VERSION, const.MINOR_VERSION) < (2021, 8) @pytest.fixture @@ -111,7 +114,17 @@ def manager_bypass_login(hass, store, provider_bypass_login): ) -async def test_trusted_networks_credentials(manager, provider): +@pytest.fixture +def mock_allowed_request(): + """Mock that the request is allowed.""" + with patch( + "homeassistant.auth.providers.trusted_networks.TrustedNetworksAuthProvider.is_allowed_request", + return_value=True, + ): + yield + + +async def test_trusted_networks_credentials(manager, provider, mock_allowed_request): """Test trusted_networks credentials related functions.""" owner = await manager.async_create_user("test-owner") tn_owner_cred = await provider.async_get_or_create_credentials({"user": owner.id}) @@ -128,7 +141,7 @@ async def test_trusted_networks_credentials(manager, provider): await provider.async_get_or_create_credentials({"user": "invalid-user"}) -async def test_validate_access(provider): +async def test_validate_access(provider, mock_allowed_request): """Test validate access from trusted networks.""" provider.async_validate_access(ip_address("192.168.0.1")) provider.async_validate_access(ip_address("192.168.128.10")) @@ -143,7 +156,7 @@ async def test_validate_access(provider): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) -async def test_validate_refresh_token(provider): +async def test_validate_refresh_token(provider, mock_allowed_request): """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: with pytest.raises(tn_auth.InvalidAuthError): @@ -153,7 +166,7 @@ async def test_validate_refresh_token(provider): mock.assert_called_once_with(ip_address("127.0.0.1")) -async def test_login_flow(manager, provider): +async def test_login_flow(manager, provider, mock_allowed_request): """Test login flow.""" owner = await manager.async_create_user("test-owner") user = await manager.async_create_user("test-user") @@ -180,7 +193,9 @@ async def test_login_flow(manager, provider): assert step["data"]["user"] == user.id -async def test_trusted_users_login(manager_with_user, provider_with_user): +async def test_trusted_users_login( + manager_with_user, provider_with_user, mock_allowed_request +): """Test available user list changed per different IP.""" owner = await manager_with_user.async_create_user("test-owner") sys_user = await manager_with_user.async_create_system_user( @@ -260,7 +275,9 @@ async def test_trusted_users_login(manager_with_user, provider_with_user): assert schema({"user": sys_user.id}) -async def test_trusted_group_login(manager_with_user, provider_with_user): +async def test_trusted_group_login( + manager_with_user, provider_with_user, mock_allowed_request +): """Test config trusted_user with group_id.""" owner = await manager_with_user.async_create_user("test-owner") # create a user in user group @@ -313,7 +330,9 @@ async def test_trusted_group_login(manager_with_user, provider_with_user): assert schema({"user": user.id}) -async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): +async def test_bypass_login_flow( + manager_bypass_login, provider_bypass_login, mock_allowed_request +): """Test login flow can be bypass if only one user available.""" owner = await manager_bypass_login.async_create_user("test-owner") @@ -344,3 +363,42 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): # both owner and user listed assert schema({"user": owner.id}) assert schema({"user": user.id}) + + +async def test_allowed_request(hass, provider, current_request, caplog): + """Test allowing requests.""" + assert await async_setup_component(hass, "http", {}) + + provider.async_validate_access(ip_address("192.168.0.1")) + + current_request.get.return_value = current_request.get.return_value.clone( + headers={ + **current_request.get.return_value.headers, + "x-forwarded-for": "1.2.3.4", + } + ) + + if FORWARD_FOR_IS_WARNING: + caplog.clear() + provider.async_validate_access(ip_address("192.168.0.1")) + assert "This request will be blocked" in caplog.text + else: + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("192.168.0.1")) + + hass.http.use_x_forwarded_for = True + + provider.async_validate_access(ip_address("192.168.0.1")) + + +@pytest.mark.skipif(FORWARD_FOR_IS_WARNING, reason="Currently a warning") +async def test_login_flow_no_request(provider): + """Test getting a login flow.""" + login_flow = await provider.async_login_flow({"ip_address": ip_address("1.1.1.1")}) + assert await login_flow.async_step_init() == { + "description_placeholders": None, + "flow_id": None, + "handler": None, + "reason": "forwared_for_header_not_allowed", + "type": "abort", + } From 0856232ea629a2d1670f2229766905d450d450cd Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Tue, 1 Jun 2021 09:45:37 +0200 Subject: [PATCH 831/852] Bump aiopvpc to apply quickfix for new electricity price tariff (#51320) Since 2021-06-01, the three PVPC price tariffs become one and only: '2.0 TD', and the JSON schema from the official API (data source of this integration) is slightly different. This patch allows a no-pain jump between the old tariffs and the new one. --- homeassistant/components/pvpc_hourly_pricing/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index c39d66163e0..bbbe18350c8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.1.1"], + "requirements": ["aiopvpc==2.1.2"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 3f895847641..9ca111d71ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.1.1 +aiopvpc==2.1.2 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 009e7313c57..1ee977e31f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.1.1 +aiopvpc==2.1.2 # homeassistant.components.webostv aiopylgtv==0.4.0 From 96191c07c9e5204c18b21b4b07aaaaaf514ffef3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 1 Jun 2021 10:41:34 +0200 Subject: [PATCH 832/852] Fix exception after removing Shelly config entry and stopping HA (#51321) * Fix device shutdown twice * Change if logic --- homeassistant/components/shelly/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index d2c56217afe..8fc7cf6be23 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -288,8 +288,10 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): def shutdown(self): """Shutdown the wrapper.""" - self.device.shutdown() - self._async_remove_device_updates_handler() + if self.device: + self.device.shutdown() + self._async_remove_device_updates_handler() + self.device = None @callback def _handle_ha_stop(self, _): From 42bf29856e5da45996984f1bd0ff60991f8f230e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Jun 2021 12:38:49 +0200 Subject: [PATCH 833/852] Update frontend to 20210601.0 (#51329) --- 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 49b51a7864c..0e0685cd772 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==20210531.1" + "home-assistant-frontend==20210601.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 80203972831..4aa67a73e14 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210531.1 +home-assistant-frontend==20210601.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9ca111d71ff..1b280e1a9a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.1 +home-assistant-frontend==20210601.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ee977e31f1..2ba64795d0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.1 +home-assistant-frontend==20210601.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 76527ab79a54c910e7852f081dc1577c83b6fe44 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Jun 2021 13:13:34 +0200 Subject: [PATCH 834/852] Bumped version to 2021.6.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6900c6c0b15..c1364fc5596 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From fac5b23b86325b7cf1567b9c23e40239b851e968 Mon Sep 17 00:00:00 2001 From: AJ Schmidt Date: Tue, 1 Jun 2021 02:44:56 -0400 Subject: [PATCH 835/852] update adext dependency (#51315) --- homeassistant/components/alarmdecoder/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index fa2bcca389f..a762d698545 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -2,7 +2,7 @@ "domain": "alarmdecoder", "name": "AlarmDecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", - "requirements": ["adext==0.4.1"], + "requirements": ["adext==0.4.2"], "codeowners": ["@ajschmidt8"], "config_flow": true, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1b280e1a9a7..19afc85cdc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -108,7 +108,7 @@ adafruit-circuitpython-mcp230xx==2.2.2 adb-shell[async]==0.3.1 # homeassistant.components.alarmdecoder -adext==0.4.1 +adext==0.4.2 # homeassistant.components.adguard adguardhome==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ba64795d0b..243d061224d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -51,7 +51,7 @@ accuweather==0.2.0 adb-shell[async]==0.3.1 # homeassistant.components.alarmdecoder -adext==0.4.1 +adext==0.4.2 # homeassistant.components.adguard adguardhome==0.5.0 From 6031f7ce992397338510f1173e2b7c36f9c53212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 1 Jun 2021 15:09:23 +0200 Subject: [PATCH 836/852] Add arch to payload (#51330) --- homeassistant/components/analytics/analytics.py | 2 ++ homeassistant/components/analytics/const.py | 1 + tests/components/analytics/test_analytics.py | 11 +++++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index e6e7ffac337..571ffd90f22 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -21,6 +21,7 @@ from .const import ( ANALYTICS_ENDPOINT_URL_DEV, ATTR_ADDON_COUNT, ATTR_ADDONS, + ATTR_ARCH, ATTR_AUTO_UPDATE, ATTR_AUTOMATION_COUNT, ATTR_BASE, @@ -157,6 +158,7 @@ class Analytics: payload[ATTR_SUPERVISOR] = { ATTR_HEALTHY: supervisor_info[ATTR_HEALTHY], ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED], + ATTR_ARCH: supervisor_info[ATTR_ARCH], } if operating_system_info.get(ATTR_BOARD) is not None: diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 16929a7131d..4688c578a00 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -16,6 +16,7 @@ LOGGER: logging.Logger = logging.getLogger(__package__) ATTR_ADDON_COUNT = "addon_count" ATTR_ADDONS = "addons" +ATTR_ARCH = "arch" ATTR_AUTO_UPDATE = "auto_update" ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 09f82e37fba..ee67a7e3935 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -132,7 +132,9 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): with patch( "homeassistant.components.hassio.get_supervisor_info", - side_effect=Mock(return_value={"supported": True, "healthy": True}), + side_effect=Mock( + return_value={"supported": True, "healthy": True, "arch": "amd64"} + ), ), patch( "homeassistant.components.hassio.get_os_info", side_effect=Mock(return_value={"board": "blue", "version": "123"}), @@ -157,7 +159,10 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): assert f"'uuid': '{MOCK_UUID}'" in caplog.text assert f"'version': '{MOCK_VERSION}'" in caplog.text - assert "'supervisor': {'healthy': True, 'supported': True}" in caplog.text + assert ( + "'supervisor': {'healthy': True, 'supported': True, 'arch': 'amd64'}" + in caplog.text + ) assert "'operating_system': {'board': 'blue', 'version': '123'}" in caplog.text assert "'installation_type':" in caplog.text assert "'integration_count':" not in caplog.text @@ -197,6 +202,7 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): return_value={ "healthy": True, "supported": True, + "arch": "amd64", "addons": [{"slug": "test_addon"}], } ), @@ -303,6 +309,7 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock): return_value={ "healthy": True, "supported": True, + "arch": "amd64", "addons": [{"slug": "test_addon"}], } ), From f54cbff223e55f2186a0b04246260ceb086779b2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Jun 2021 18:38:55 +0200 Subject: [PATCH 837/852] Always load middle to handle forwarded proxy data (#51332) --- .../auth/providers/trusted_networks.py | 40 ---------- homeassistant/components/http/__init__.py | 6 +- homeassistant/components/http/forwarded.py | 36 +++++++-- tests/auth/providers/test_trusted_networks.py | 74 ++----------------- tests/components/http/test_auth.py | 2 +- tests/components/http/test_forwarded.py | 63 +++++++++++----- 6 files changed, 85 insertions(+), 136 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index e93587e91ca..fd2014667f8 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -14,13 +14,10 @@ from ipaddress import ( ip_address, ip_network, ) -import logging from typing import Any, Dict, List, Union, cast -from aiohttp import hdrs import voluptuous as vol -from homeassistant.components import http from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -89,31 +86,9 @@ class TrustedNetworksAuthProvider(AuthProvider): """Trusted Networks auth provider does not support MFA.""" return False - @callback - def is_allowed_request(self) -> bool: - """Return if it is an allowed request.""" - request = http.current_request.get() - if request is not None and ( - self.hass.http.use_x_forwarded_for - or hdrs.X_FORWARDED_FOR not in request.headers - ): - return True - - logging.getLogger(__name__).warning( - "A request contained an x-forwarded-for header but your HTTP integration is not set-up " - "for reverse proxies. This usually means that you have not configured your reverse proxy " - "correctly. This request will be blocked in Home Assistant 2021.7 unless you configure " - "your HTTP integration to allow this header." - ) - return True - async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" assert context is not None - - if not self.is_allowed_request(): - return MisconfiguredTrustedNetworksLoginFlow(self) - ip_addr = cast(IPAddress, context.get("ip_address")) users = await self.store.async_get_users() available_users = [ @@ -195,11 +170,6 @@ class TrustedNetworksAuthProvider(AuthProvider): Raise InvalidAuthError if not. Raise InvalidAuthError if trusted_networks is not configured. """ - if not self.is_allowed_request(): - raise InvalidAuthError( - "No request or it contains x-forwarded-for header and that's not allowed by configuration" - ) - if not self.trusted_networks: raise InvalidAuthError("trusted_networks is not configured") @@ -220,16 +190,6 @@ class TrustedNetworksAuthProvider(AuthProvider): self.async_validate_access(ip_address(remote_ip)) -class MisconfiguredTrustedNetworksLoginFlow(LoginFlow): - """Login handler for misconfigured trusted networks.""" - - async def async_step_init( - self, user_input: dict[str, str] | None = None - ) -> FlowResult: - """Handle the step of the form.""" - return self.async_abort(reason="forwared_for_header_not_allowed") - - class TrustedNetworksLoginFlow(LoginFlow): """Handler for the login flow.""" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index db198cb334a..19e3437b79c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -232,10 +232,7 @@ class HomeAssistantHTTP: # forwarded middleware needs to go second. setup_security_filter(app) - # Only register middleware if `use_x_forwarded_for` is enabled - # and trusted proxies are provided - if use_x_forwarded_for and trusted_proxies: - async_setup_forwarded(app, trusted_proxies) + async_setup_forwarded(app, use_x_forwarded_for, trusted_proxies) setup_request_context(app, current_request) @@ -252,7 +249,6 @@ class HomeAssistantHTTP: self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port - self.use_x_forwarded_for = use_x_forwarded_for self.trusted_proxies = trusted_proxies self.is_ban_enabled = is_ban_enabled self.ssl_profile = ssl_profile diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 5c5726a2597..5c62a469924 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -14,7 +14,9 @@ _LOGGER = logging.getLogger(__name__) @callback -def async_setup_forwarded(app: Application, trusted_proxies: list[str]) -> None: +def async_setup_forwarded( + app: Application, use_x_forwarded_for: bool | None, trusted_proxies: list[str] +) -> None: """Create forwarded middleware for the app. Process IP addresses, proto and host information in the forwarded for headers. @@ -73,15 +75,37 @@ def async_setup_forwarded(app: Application, trusted_proxies: list[str]) -> None: # No forwarding headers, continue as normal return await handler(request) - # Ensure the IP of the connected peer is trusted - assert request.transport is not None + # Get connected IP + if ( + request.transport is None + or request.transport.get_extra_info("peername") is None + ): + # Connected IP isn't retrieveable from the request transport, continue + return await handler(request) + connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) - if not any(connected_ip in trusted_proxy for trusted_proxy in trusted_proxies): + + # We have X-Forwarded-For, but config does not agree + if not use_x_forwarded_for: _LOGGER.warning( - "Received X-Forwarded-For header from untrusted proxy %s, headers not processed", + "A request from a reverse proxy was received from %s, but your " + "HTTP integration is not set-up for reverse proxies; " + "This request will be blocked in Home Assistant 2021.7 unless " + "you configure your HTTP integration to allow this header", connected_ip, ) - # Not trusted, continue as normal + # Block this request in the future, for now we pass. + return await handler(request) + + # Ensure the IP of the connected peer is trusted + if not any(connected_ip in trusted_proxy for trusted_proxy in trusted_proxies): + _LOGGER.warning( + "Received X-Forwarded-For header from untrusted proxy %s, headers not processed; " + "This request will be blocked in Home Assistant 2021.7 unless you configure " + "your HTTP integration to allow this proxy to reverse your Home Assistant instance", + connected_ip, + ) + # Not trusted, Block this request in the future, continue as normal return await handler(request) # Multiple X-Forwarded-For headers diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index c68d6651e3c..39764fa4206 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -5,12 +5,9 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol -from homeassistant import auth, const +from homeassistant import auth from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth -from homeassistant.setup import async_setup_component - -FORWARD_FOR_IS_WARNING = (const.MAJOR_VERSION, const.MINOR_VERSION) < (2021, 8) @pytest.fixture @@ -114,17 +111,7 @@ def manager_bypass_login(hass, store, provider_bypass_login): ) -@pytest.fixture -def mock_allowed_request(): - """Mock that the request is allowed.""" - with patch( - "homeassistant.auth.providers.trusted_networks.TrustedNetworksAuthProvider.is_allowed_request", - return_value=True, - ): - yield - - -async def test_trusted_networks_credentials(manager, provider, mock_allowed_request): +async def test_trusted_networks_credentials(manager, provider): """Test trusted_networks credentials related functions.""" owner = await manager.async_create_user("test-owner") tn_owner_cred = await provider.async_get_or_create_credentials({"user": owner.id}) @@ -141,7 +128,7 @@ async def test_trusted_networks_credentials(manager, provider, mock_allowed_requ await provider.async_get_or_create_credentials({"user": "invalid-user"}) -async def test_validate_access(provider, mock_allowed_request): +async def test_validate_access(provider): """Test validate access from trusted networks.""" provider.async_validate_access(ip_address("192.168.0.1")) provider.async_validate_access(ip_address("192.168.128.10")) @@ -156,7 +143,7 @@ async def test_validate_access(provider, mock_allowed_request): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) -async def test_validate_refresh_token(provider, mock_allowed_request): +async def test_validate_refresh_token(provider): """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: with pytest.raises(tn_auth.InvalidAuthError): @@ -166,7 +153,7 @@ async def test_validate_refresh_token(provider, mock_allowed_request): mock.assert_called_once_with(ip_address("127.0.0.1")) -async def test_login_flow(manager, provider, mock_allowed_request): +async def test_login_flow(manager, provider): """Test login flow.""" owner = await manager.async_create_user("test-owner") user = await manager.async_create_user("test-user") @@ -193,9 +180,7 @@ async def test_login_flow(manager, provider, mock_allowed_request): assert step["data"]["user"] == user.id -async def test_trusted_users_login( - manager_with_user, provider_with_user, mock_allowed_request -): +async def test_trusted_users_login(manager_with_user, provider_with_user): """Test available user list changed per different IP.""" owner = await manager_with_user.async_create_user("test-owner") sys_user = await manager_with_user.async_create_system_user( @@ -275,9 +260,7 @@ async def test_trusted_users_login( assert schema({"user": sys_user.id}) -async def test_trusted_group_login( - manager_with_user, provider_with_user, mock_allowed_request -): +async def test_trusted_group_login(manager_with_user, provider_with_user): """Test config trusted_user with group_id.""" owner = await manager_with_user.async_create_user("test-owner") # create a user in user group @@ -330,9 +313,7 @@ async def test_trusted_group_login( assert schema({"user": user.id}) -async def test_bypass_login_flow( - manager_bypass_login, provider_bypass_login, mock_allowed_request -): +async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): """Test login flow can be bypass if only one user available.""" owner = await manager_bypass_login.async_create_user("test-owner") @@ -363,42 +344,3 @@ async def test_bypass_login_flow( # both owner and user listed assert schema({"user": owner.id}) assert schema({"user": user.id}) - - -async def test_allowed_request(hass, provider, current_request, caplog): - """Test allowing requests.""" - assert await async_setup_component(hass, "http", {}) - - provider.async_validate_access(ip_address("192.168.0.1")) - - current_request.get.return_value = current_request.get.return_value.clone( - headers={ - **current_request.get.return_value.headers, - "x-forwarded-for": "1.2.3.4", - } - ) - - if FORWARD_FOR_IS_WARNING: - caplog.clear() - provider.async_validate_access(ip_address("192.168.0.1")) - assert "This request will be blocked" in caplog.text - else: - with pytest.raises(tn_auth.InvalidAuthError): - provider.async_validate_access(ip_address("192.168.0.1")) - - hass.http.use_x_forwarded_for = True - - provider.async_validate_access(ip_address("192.168.0.1")) - - -@pytest.mark.skipif(FORWARD_FOR_IS_WARNING, reason="Currently a warning") -async def test_login_flow_no_request(provider): - """Test getting a login flow.""" - login_flow = await provider.async_login_flow({"ip_address": ip_address("1.1.1.1")}) - assert await login_flow.async_step_init() == { - "description_placeholders": None, - "flow_id": None, - "handler": None, - "reason": "forwared_for_header_not_allowed", - "type": "abort", - } diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 71c01630a67..6bd1d622b12 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -53,7 +53,7 @@ def app(hass): app = web.Application() app["hass"] = hass app.router.add_get("/", mock_handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) return app diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 2946c0b383c..4b7a3421b0a 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -28,7 +28,7 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) @@ -74,7 +74,7 @@ async def test_x_forwarded_for_with_trusted_proxy( app = web.Application() app.router.add_get("/", handler) async_setup_forwarded( - app, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] + app, True, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] ) mock_api_client = await aiohttp_client(app) @@ -83,6 +83,33 @@ async def test_x_forwarded_for_with_trusted_proxy( assert resp.status == 200 +async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): + """Test that we warn when processing is disabled, but proxy has been detected.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "127.0.0.1" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + + async_setup_forwarded(app, False, []) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) + + assert resp.status == 200 + assert ( + "A request from a reverse proxy was received from 127.0.0.1, but your HTTP " + "integration is not set-up for reverse proxies" in caplog.text + ) + + async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client): """Test that we get the IP from transport with untrusted proxy.""" @@ -97,7 +124,7 @@ async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("1.1.1.1")]) + async_setup_forwarded(app, True, [ip_network("1.1.1.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) @@ -119,7 +146,7 @@ async def test_x_forwarded_for_with_spoofed_header(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -148,7 +175,7 @@ async def test_x_forwarded_for_with_malformed_header( """Test that we get a HTTP 400 bad request with a malformed header.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) @@ -162,7 +189,7 @@ async def test_x_forwarded_for_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) @@ -193,7 +220,7 @@ async def test_x_forwarded_proto_without_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -245,7 +272,7 @@ async def test_x_forwarded_proto_with_trusted_proxy( app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.0/24")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.0/24")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -273,7 +300,7 @@ async def test_x_forwarded_proto_with_trusted_proxy_multiple_for(aiohttp_client) app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.0/24")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.0/24")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -301,7 +328,7 @@ async def test_x_forwarded_proto_not_processed_without_for(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_PROTO: "https"}) @@ -313,7 +340,7 @@ async def test_x_forwarded_proto_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -339,7 +366,7 @@ async def test_x_forwarded_proto_empty_element( """Test that we get a HTTP 400 bad request with empty proto.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -364,7 +391,7 @@ async def test_x_forwarded_proto_incorrect_number_of_elements( """Test that we get a HTTP 400 bad request with incorrect number of elements.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -397,7 +424,7 @@ async def test_x_forwarded_host_without_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -421,7 +448,7 @@ async def test_x_forwarded_host_with_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -446,7 +473,7 @@ async def test_x_forwarded_host_not_processed_without_for(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_HOST: "example.com"}) @@ -458,7 +485,7 @@ async def test_x_forwarded_host_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -478,7 +505,7 @@ async def test_x_forwarded_host_with_empty_header(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with empty host value.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( From bbd743368678ad3ad0230999c522e78fd2642366 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 1 Jun 2021 17:07:45 +0200 Subject: [PATCH 838/852] Improve time condition trace (#51335) --- homeassistant/helpers/condition.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index a59ad459874..23816b94a65 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -756,15 +756,18 @@ def time( ) if after < before: + condition_trace_update_result(after=after, now_time=now_time, before=before) if not after <= now_time < before: return False else: + condition_trace_update_result(after=after, now_time=now_time, before=before) if before <= now_time < after: return False if weekday is not None: now_weekday = WEEKDAYS[now.weekday()] + condition_trace_update_result(weekday=weekday, now_weekday=now_weekday) if ( isinstance(weekday, str) and weekday != now_weekday From d78694c9b8a3c0659b0bc85667c659797fe9e613 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Jun 2021 17:57:23 +0200 Subject: [PATCH 839/852] Fix time condition microsecond offset when using input helpers (#51337) --- homeassistant/helpers/condition.py | 1 - tests/helpers/test_condition.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 23816b94a65..a467d952683 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -752,7 +752,6 @@ def time( before_entity.attributes.get("hour", 23), before_entity.attributes.get("minute", 59), before_entity.attributes.get("second", 59), - 999999, ) if after < before: diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 9347d0bc025..2290ce9f679 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -791,6 +791,34 @@ async def test_time_using_input_datetime(hass): hass, after="input_datetime.pm", before="input_datetime.am" ) + # Trigger on PM time + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=18, minute=0, second=0), + ): + assert condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + assert not condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert condition.time(hass, after="input_datetime.pm") + assert not condition.time(hass, before="input_datetime.pm") + + # Trigger on AM time + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=6, minute=0, second=0), + ): + assert not condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + assert condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert condition.time(hass, after="input_datetime.am") + assert not condition.time(hass, before="input_datetime.am") + with pytest.raises(ConditionError): condition.time(hass, after="input_datetime.not_existing") From 941b02b73ee0f99b546b588b449b88b37a652292 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 1 Jun 2021 17:58:25 +0200 Subject: [PATCH 840/852] Fix Netatmo sensor logic (#51338) --- homeassistant/components/netatmo/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index ed75ddf2f7f..2dbbeb56c76 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -183,7 +183,7 @@ async def async_setup_entry(hass, entry, async_add_entities): await data_handler.register_data_class(data_class_name, data_class_name, None) data_class = data_handler.data.get(data_class_name) - if not (data_class and data_class.raw_data): + if data_class and data_class.raw_data: platform_not_ready = False async_add_entities(await find_entities(data_class_name), True) @@ -228,7 +228,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) data_class = data_handler.data.get(signal_name) - if not (data_class and data_class.raw_data): + if data_class and data_class.raw_data: nonlocal platform_not_ready platform_not_ready = False From 464c66f97f25f99a29caea9e96857cb2a4474032 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Tue, 1 Jun 2021 20:32:17 +0200 Subject: [PATCH 841/852] Fix SIA event data func (#51339) --- homeassistant/components/sia/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 08e0fce8ab2..66fdd7d95be 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -30,7 +30,7 @@ def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]: def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: """Create a dict from the SIA Event for the HA Event.""" return { - "message_type": event.message_type, + "message_type": event.message_type.value, "receiver": event.receiver, "line": event.line, "account": event.account, @@ -43,7 +43,7 @@ def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: "message": event.message, "x_data": event.x_data, "timestamp": event.timestamp.isoformat(), - "event_qualifier": event.qualifier, + "event_qualifier": event.event_qualifier, "event_type": event.event_type, "partition": event.partition, "extended_data": [ From 5ea798462f9a3c935e10aae4783495e26f776133 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Tue, 1 Jun 2021 20:07:51 +0300 Subject: [PATCH 842/852] Fix Snapcast state after restoring snapshot (#51340) --- homeassistant/components/snapcast/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index dcb4b62b35a..26bf4c903a6 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -198,6 +198,7 @@ class SnapcastGroupDevice(MediaPlayerEntity): async def async_restore(self): """Restore the group state.""" await self._group.restore() + self.async_write_ha_state() class SnapcastClientDevice(MediaPlayerEntity): @@ -326,6 +327,7 @@ class SnapcastClientDevice(MediaPlayerEntity): async def async_restore(self): """Restore the client state.""" await self._client.restore() + self.async_write_ha_state() async def async_set_latency(self, latency): """Set the latency of the client.""" From 89a374057dd1a48abe431cc4630e8030d91fec27 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 1 Jun 2021 19:26:54 +0200 Subject: [PATCH 843/852] Bump zwave-js-server-python to 0.26.0 (#51341) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index c68206373ba..5ce65fcbb35 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.25.1"], + "requirements": ["zwave-js-server-python==0.26.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 19afc85cdc9..f33792126ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2442,4 +2442,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.25.1 +zwave-js-server-python==0.26.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 243d061224d..cfffb2c5dfc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1324,4 +1324,4 @@ zigpy-znp==0.5.1 zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.25.1 +zwave-js-server-python==0.26.0 From f93acfc4c0aea37a72bbdf682a887b18342d5f6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Jun 2021 13:34:31 -0700 Subject: [PATCH 844/852] Merge system options into pref properties (#51347) * Make system options future proof * Update tests * Add types --- .../components/config/config_entries.py | 82 ++++++------- homeassistant/config_entries.py | 99 ++++++++-------- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/entity_registry.py | 2 +- homeassistant/helpers/update_coordinator.py | 2 +- tests/common.py | 6 +- .../bmw_connected_drive/test_config_flow.py | 1 - .../components/config/test_config_entries.py | 95 ++++++--------- .../forked_daapd/test_config_flow.py | 1 - .../forked_daapd/test_media_player.py | 1 - .../components/home_plus_control/conftest.py | 1 - .../homekit_controller/test_storage.py | 1 - .../components/homematicip_cloud/conftest.py | 1 - tests/components/hue/conftest.py | 1 - tests/components/hue/test_bridge.py | 4 - tests/components/hue/test_light.py | 1 - tests/components/huisbaasje/test_init.py | 3 - tests/components/huisbaasje/test_sensor.py | 2 - .../hvv_departures/test_config_flow.py | 3 - tests/components/smartthings/conftest.py | 1 - tests/components/unifi/test_device_tracker.py | 1 - tests/components/unifi/test_switch.py | 2 - tests/components/zwave/test_lock.py | 1 - tests/helpers/test_entity_platform.py | 4 +- tests/helpers/test_entity_registry.py | 6 +- tests/helpers/test_update_coordinator.py | 2 +- tests/test_config_entries.py | 108 ++++++++++-------- 27 files changed, 188 insertions(+), 245 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 9d88a9b5311..7fe5cb0d190 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,4 +1,6 @@ """Http views to control the config manager.""" +from __future__ import annotations + import aiohttp.web_exceptions import voluptuous as vol @@ -7,7 +9,7 @@ from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, @@ -31,7 +33,6 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command(config_entry_disable) hass.components.websocket_api.async_register_command(config_entry_update) hass.components.websocket_api.async_register_command(config_entries_progress) - hass.components.websocket_api.async_register_command(system_options_update) hass.components.websocket_api.async_register_command(ignore_config_flow) return True @@ -230,14 +231,21 @@ def config_entries_progress(hass, connection, msg): ) -def send_entry_not_found(connection, msg_id): +def send_entry_not_found( + connection: websocket_api.ActiveConnection, msg_id: int +) -> None: """Send Config entry not found error.""" connection.send_error( msg_id, websocket_api.const.ERR_NOT_FOUND, "Config entry not found" ) -def get_entry(hass, connection, entry_id, msg_id): +def get_entry( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + entry_id: str, + msg_id: int, +) -> config_entries.ConfigEntry | None: """Get entry, send error message if it doesn't exist.""" entry = hass.config_entries.async_get_entry(entry_id) if entry is None: @@ -249,49 +257,13 @@ def get_entry(hass, connection, entry_id, msg_id): @websocket_api.async_response @websocket_api.websocket_command( { - "type": "config_entries/system_options/update", + "type": "config_entries/update", "entry_id": str, - vol.Optional("disable_new_entities"): bool, - vol.Optional("disable_polling"): bool, + vol.Optional("title"): str, + vol.Optional("pref_disable_new_entities"): bool, + vol.Optional("pref_disable_polling"): bool, } ) -async def system_options_update(hass, connection, msg): - """Update config entry system options.""" - changes = dict(msg) - changes.pop("id") - changes.pop("type") - changes.pop("entry_id") - - entry = get_entry(hass, connection, msg["entry_id"], msg["id"]) - if entry is None: - return - - old_disable_polling = entry.system_options.disable_polling - - hass.config_entries.async_update_entry(entry, system_options=changes) - - result = { - "system_options": entry.system_options.as_dict(), - "require_restart": False, - } - - if ( - old_disable_polling != entry.system_options.disable_polling - and entry.state is config_entries.ConfigEntryState.LOADED - ): - if not await hass.config_entries.async_reload(entry.entry_id): - result["require_restart"] = ( - entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD - ) - - connection.send_result(msg["id"], result) - - -@websocket_api.require_admin -@websocket_api.async_response -@websocket_api.websocket_command( - {"type": "config_entries/update", "entry_id": str, vol.Optional("title"): str} -) async def config_entry_update(hass, connection, msg): """Update config entry.""" changes = dict(msg) @@ -303,8 +275,25 @@ async def config_entry_update(hass, connection, msg): if entry is None: return + old_disable_polling = entry.pref_disable_polling + hass.config_entries.async_update_entry(entry, **changes) - connection.send_result(msg["id"], entry_json(entry)) + + result = { + "config_entry": entry_json(entry), + "require_restart": False, + } + + if ( + old_disable_polling != entry.pref_disable_polling + and entry.state is config_entries.ConfigEntryState.LOADED + ): + if not await hass.config_entries.async_reload(entry.entry_id): + result["require_restart"] = ( + entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD + ) + + connection.send_result(msg["id"], result) @websocket_api.require_admin @@ -391,7 +380,8 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "state": entry.state.value, "supports_options": supports_options, "supports_unload": entry.supports_unload, - "system_options": entry.system_options.as_dict(), + "pref_disable_new_entities": entry.pref_disable_new_entities, + "pref_disable_polling": entry.pref_disable_polling, "disabled_by": entry.disabled_by, "reason": entry.reason, } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 33c18fc0d7c..eeaf0149cc2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,8 +11,6 @@ from types import MappingProxyType, MethodType from typing import Any, Callable, Optional, cast import weakref -import attr - from homeassistant import data_entry_flow, loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback @@ -152,7 +150,8 @@ class ConfigEntry: "options", "unique_id", "supports_unload", - "system_options", + "pref_disable_new_entities", + "pref_disable_polling", "source", "state", "disabled_by", @@ -170,7 +169,8 @@ class ConfigEntry: title: str, data: Mapping[str, Any], source: str, - system_options: dict, + pref_disable_new_entities: bool | None = None, + pref_disable_polling: bool | None = None, options: Mapping[str, Any] | None = None, unique_id: str | None = None, entry_id: str | None = None, @@ -197,7 +197,15 @@ class ConfigEntry: self.options = MappingProxyType(options or {}) # Entry system options - self.system_options = SystemOptions(**system_options) + if pref_disable_new_entities is None: + pref_disable_new_entities = False + + self.pref_disable_new_entities = pref_disable_new_entities + + if pref_disable_polling is None: + pref_disable_polling = False + + self.pref_disable_polling = pref_disable_polling # Source of the configuration (user, discovery, cloud) self.source = source @@ -535,7 +543,8 @@ class ConfigEntry: "title": self.title, "data": dict(self.data), "options": dict(self.options), - "system_options": self.system_options.as_dict(), + "pref_disable_new_entities": self.pref_disable_new_entities, + "pref_disable_polling": self.pref_disable_polling, "source": self.source, "unique_id": self.unique_id, "disabled_by": self.disabled_by, @@ -652,7 +661,6 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): title=result["title"], data=result["data"], options=result["options"], - system_options={}, source=flow.context["source"], unique_id=flow.unique_id, ) @@ -845,8 +853,18 @@ class ConfigEntries: self._entries = {} return - self._entries = { - entry["entry_id"]: ConfigEntry( + entries = {} + + for entry in config["entries"]: + pref_disable_new_entities = entry.get("pref_disable_new_entities") + + # Between 0.98 and 2021.6 we stored 'disable_new_entities' in a system options dictionary + if pref_disable_new_entities is None and "system_options" in entry: + pref_disable_new_entities = entry.get("system_options", {}).get( + "disable_new_entities" + ) + + entries[entry["entry_id"]] = ConfigEntry( version=entry["version"], domain=entry["domain"], entry_id=entry["entry_id"], @@ -855,15 +873,16 @@ class ConfigEntries: title=entry["title"], # New in 0.89 options=entry.get("options"), - # New in 0.98 - system_options=entry.get("system_options", {}), # New in 0.104 unique_id=entry.get("unique_id"), # New in 2021.3 disabled_by=entry.get("disabled_by"), + # New in 2021.6 + pref_disable_new_entities=pref_disable_new_entities, + pref_disable_polling=entry.get("pref_disable_polling"), ) - for entry in config["entries"] - } + + self._entries = entries async def async_setup(self, entry_id: str) -> bool: """Set up a config entry. @@ -962,11 +981,12 @@ class ConfigEntries: self, entry: ConfigEntry, *, - unique_id: str | dict | None | UndefinedType = UNDEFINED, - title: str | dict | UndefinedType = UNDEFINED, + unique_id: str | None | UndefinedType = UNDEFINED, + title: str | UndefinedType = UNDEFINED, data: dict | UndefinedType = UNDEFINED, options: Mapping[str, Any] | UndefinedType = UNDEFINED, - system_options: dict | UndefinedType = UNDEFINED, + pref_disable_new_entities: bool | UndefinedType = UNDEFINED, + pref_disable_polling: bool | UndefinedType = UNDEFINED, ) -> bool: """Update a config entry. @@ -978,13 +998,17 @@ class ConfigEntries: """ changed = False - if unique_id is not UNDEFINED and entry.unique_id != unique_id: - changed = True - entry.unique_id = cast(Optional[str], unique_id) + for attr, value in ( + ("unique_id", unique_id), + ("title", title), + ("pref_disable_new_entities", pref_disable_new_entities), + ("pref_disable_polling", pref_disable_polling), + ): + if value == UNDEFINED or getattr(entry, attr) == value: + continue - if title is not UNDEFINED and entry.title != title: + setattr(entry, attr, value) changed = True - entry.title = cast(str, title) if data is not UNDEFINED and entry.data != data: # type: ignore changed = True @@ -994,11 +1018,6 @@ class ConfigEntries: changed = True entry.options = MappingProxyType(options) - if system_options is not UNDEFINED: - old_system_options = entry.system_options.as_dict() - entry.system_options.update(**system_options) - changed = entry.system_options.as_dict() != old_system_options - if not changed: return False @@ -1401,34 +1420,6 @@ class OptionsFlow(data_entry_flow.FlowHandler): handler: str -@attr.s(slots=True) -class SystemOptions: - """Config entry system options.""" - - disable_new_entities: bool = attr.ib(default=False) - disable_polling: bool = attr.ib(default=False) - - def update( - self, - *, - disable_new_entities: bool | UndefinedType = UNDEFINED, - disable_polling: bool | UndefinedType = UNDEFINED, - ) -> None: - """Update properties.""" - if disable_new_entities is not UNDEFINED: - self.disable_new_entities = disable_new_entities - - if disable_polling is not UNDEFINED: - self.disable_polling = disable_polling - - def as_dict(self) -> dict[str, Any]: - """Return dictionary version of this config entries system options.""" - return { - "disable_new_entities": self.disable_new_entities, - "disable_polling": self.disable_polling, - } - - class EntityRegistryDisabledHandler: """Handler to handle when entities related to config entries updating disabled_by.""" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f0d691a1c8d..b22fb9ec2d2 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -397,7 +397,7 @@ class EntityPlatform: raise if ( - (self.config_entry and self.config_entry.system_options.disable_polling) + (self.config_entry and self.config_entry.pref_disable_polling) or self._async_unsub_polling is not None or not any(entity.should_poll for entity in self.entities.values()) ): diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index dbb3fae0e53..fc9ef575c7d 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -286,7 +286,7 @@ class EntityRegistry: if ( disabled_by is None and config_entry - and config_entry.system_options.disable_new_entities + and config_entry.pref_disable_new_entities ): disabled_by = DISABLED_INTEGRATION diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index e91acfaf82f..e83a2d0edc3 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -111,7 +111,7 @@ class DataUpdateCoordinator(Generic[T]): if self.update_interval is None: return - if self.config_entry and self.config_entry.system_options.disable_polling: + if self.config_entry and self.config_entry.pref_disable_polling: return if self._unsub_refresh: diff --git a/tests/common.py b/tests/common.py index 952350fe68c..03b53294db0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -732,7 +732,8 @@ class MockConfigEntry(config_entries.ConfigEntry): title="Mock Title", state=None, options={}, - system_options={}, + pref_disable_new_entities=None, + pref_disable_polling=None, unique_id=None, disabled_by=None, reason=None, @@ -742,7 +743,8 @@ class MockConfigEntry(config_entries.ConfigEntry): "entry_id": entry_id or uuid_util.random_uuid_hex(), "domain": domain, "data": data or {}, - "system_options": system_options, + "pref_disable_new_entities": pref_disable_new_entities, + "pref_disable_polling": pref_disable_polling, "options": options, "version": version, "title": title, diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 75ca9b4aa1c..6a0bd210387 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -29,7 +29,6 @@ FIXTURE_CONFIG_ENTRY = { CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], }, "options": {CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, - "system_options": {"disable_new_entities": False}, "source": config_entries.SOURCE_USER, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 570d847e86e..0e1b471cbd5 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -87,10 +87,8 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": True, "supports_unload": True, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": None, "reason": None, }, @@ -101,10 +99,8 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.SETUP_ERROR.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": None, "reason": "Unsupported API", }, @@ -115,10 +111,8 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": core_ce.DISABLED_USER, "reason": None, }, @@ -340,10 +334,8 @@ async def test_create_account(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "title": "Test Entry", "reason": None, }, @@ -415,10 +407,8 @@ async def test_two_step_flow(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "title": "user-title", "reason": None, }, @@ -698,7 +688,7 @@ async def test_two_step_options_flow(hass, client): } -async def test_update_system_options(hass, hass_ws_client): +async def test_update_prefrences(hass, hass_ws_client): """Test that we can update system options.""" assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -706,64 +696,45 @@ async def test_update_system_options(hass, hass_ws_client): entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) - assert entry.system_options.disable_new_entities is False - assert entry.system_options.disable_polling is False - - await ws_client.send_json( - { - "id": 5, - "type": "config_entries/system_options/update", - "entry_id": entry.entry_id, - "disable_new_entities": True, - } - ) - response = await ws_client.receive_json() - - assert response["success"] - assert response["result"] == { - "require_restart": False, - "system_options": {"disable_new_entities": True, "disable_polling": False}, - } - assert entry.system_options.disable_new_entities is True - assert entry.system_options.disable_polling is False + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is False await ws_client.send_json( { "id": 6, - "type": "config_entries/system_options/update", + "type": "config_entries/update", "entry_id": entry.entry_id, - "disable_new_entities": False, - "disable_polling": True, + "pref_disable_new_entities": True, } ) response = await ws_client.receive_json() assert response["success"] - assert response["result"] == { - "require_restart": True, - "system_options": {"disable_new_entities": False, "disable_polling": True}, - } - assert entry.system_options.disable_new_entities is False - assert entry.system_options.disable_polling is True + assert response["result"]["require_restart"] is False + assert response["result"]["config_entry"]["pref_disable_new_entities"] is True + assert response["result"]["config_entry"]["pref_disable_polling"] is False - -async def test_update_system_options_nonexisting(hass, hass_ws_client): - """Test that we can update entry.""" - assert await async_setup_component(hass, "config", {}) - ws_client = await hass_ws_client(hass) + assert entry.pref_disable_new_entities is True + assert entry.pref_disable_polling is False await ws_client.send_json( { - "id": 5, - "type": "config_entries/system_options/update", - "entry_id": "non_existing", - "disable_new_entities": True, + "id": 7, + "type": "config_entries/update", + "entry_id": entry.entry_id, + "pref_disable_new_entities": False, + "pref_disable_polling": True, } ) response = await ws_client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "not_found" + assert response["success"] + assert response["result"]["require_restart"] is True + assert response["result"]["config_entry"]["pref_disable_new_entities"] is False + assert response["result"]["config_entry"]["pref_disable_polling"] is True + + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is True async def test_update_entry(hass, hass_ws_client): @@ -785,7 +756,7 @@ async def test_update_entry(hass, hass_ws_client): response = await ws_client.receive_json() assert response["success"] - assert response["result"]["title"] == "Updated Title" + assert response["result"]["config_entry"]["title"] == "Updated Title" assert entry.title == "Updated Title" diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index a99f91d3f91..668f1be0a4f 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -46,7 +46,6 @@ def config_entry_fixture(): title="", data=data, options={}, - system_options={}, source=SOURCE_USER, entry_id=1, ) diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 032e3dde22c..a2e0050c3d9 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -282,7 +282,6 @@ def config_entry_fixture(): title="", data=data, options={CONF_TTS_PAUSE_TIME: 0}, - system_options={}, source=SOURCE_USER, entry_id=1, ) diff --git a/tests/components/home_plus_control/conftest.py b/tests/components/home_plus_control/conftest.py index 4b60f2623c4..78a0da41fb8 100644 --- a/tests/components/home_plus_control/conftest.py +++ b/tests/components/home_plus_control/conftest.py @@ -35,7 +35,6 @@ def mock_config_entry(): }, source="test", options={}, - system_options={"disable_new_entities": False}, unique_id=DOMAIN, entry_id="home_plus_control_entry_id", ) diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index b1c3ee9ff4c..aa0a5e55057 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -95,7 +95,6 @@ async def test_storage_is_removed_on_config_entry_removal(hass, utcnow): "TestData", pairing_data, "test", - system_options={}, ) assert hkid in hass.data[ENTITY_MAP].storage_data diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index b5dd6105e0f..c720df4a1bb 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -62,7 +62,6 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: unique_id=HAPID, data=entry_data, source=SOURCE_IMPORT, - system_options={"disable_new_entities": False}, ) return config_entry diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index b5c2aec3042..648337d7539 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -127,7 +127,6 @@ async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None): domain=hue.DOMAIN, title="Mock Title", data={"host": hostname}, - system_options={}, ) mock_bridge.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index eb5c93862fe..034acf88efa 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -181,7 +181,6 @@ async def test_hue_activate_scene(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -215,7 +214,6 @@ async def test_hue_activate_scene_transition(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -249,7 +247,6 @@ async def test_hue_activate_scene_group_not_found(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -278,7 +275,6 @@ async def test_hue_activate_scene_scene_not_found(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 5efb74d015f..f4f663c23ae 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -179,7 +179,6 @@ async def setup_bridge(hass, mock_bridge): "Mock Title", {"host": "mock-host"}, "test", - system_options={}, ) mock_bridge.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index dde62a9c78b..390dc6c304d 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -41,7 +41,6 @@ async def test_setup_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) @@ -81,7 +80,6 @@ async def test_setup_entry_error(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) @@ -122,7 +120,6 @@ async def test_unload_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index c753c89627d..45ce20af628 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -34,7 +34,6 @@ async def test_setup_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) @@ -90,7 +89,6 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 4a18e639315..9c510bb3db0 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -256,7 +256,6 @@ async def test_options_flow(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) @@ -306,7 +305,6 @@ async def test_options_flow_invalid_auth(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) @@ -346,7 +344,6 @@ async def test_options_flow_cannot_connect(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index be822371030..2a7b5ed7084 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -61,7 +61,6 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): "Test", {CONF_INSTALLED_APP_ID: str(uuid4())}, SOURCE_USER, - system_options={}, ) broker = DeviceBroker( hass, config_entry, Mock(), Mock(), devices or [], scenes or [] diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 62a52b500f9..d583cad86c3 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -943,7 +943,6 @@ async def test_restoring_client(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - system_options={}, options={}, entry_id=1, ) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 5c4a65e0a78..ad277f18a8d 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -793,7 +793,6 @@ async def test_restore_client_succeed(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - system_options={}, options={}, entry_id=1, ) @@ -884,7 +883,6 @@ async def test_restore_client_no_old_state(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - system_options={}, options={}, entry_id=1, ) diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index f265b36dcb6..04d46620013 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -286,7 +286,6 @@ async def setup_ozw(hass, mock_openzwave): "Mock Title", {"usb_path": "mock-path", "network_key": "mock-key"}, "test", - system_options={}, ) await hass.config_entries.async_forward_entry_setup(config_entry, "lock") await hass.async_block_till_done() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 944f02d46c0..65a46f33cd8 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -60,9 +60,7 @@ async def test_polling_only_updates_entities_it_should_poll(hass): async def test_polling_disabled_by_config_entry(hass): """Test the polling of only updated entities.""" entity_platform = MockEntityPlatform(hass) - entity_platform.config_entry = MockConfigEntry( - system_options={"disable_polling": True} - ) + entity_platform.config_entry = MockConfigEntry(pref_disable_polling=True) poll_ent = MockEntity(should_poll=True) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index a124e1e6da1..fe445e32c96 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -513,12 +513,12 @@ async def test_disabled_by(registry): assert entry2.disabled_by is None -async def test_disabled_by_system_options(registry): - """Test system options setting disabled_by.""" +async def test_disabled_by_config_entry_pref(registry): + """Test config entry preference setting disabled_by.""" mock_config = MockConfigEntry( domain="light", entry_id="mock-id-1", - system_options={"disable_new_entities": True}, + pref_disable_new_entities=True, ) entry = registry.async_get_or_create( "light", "hue", "AAAA", config_entry=mock_config diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index a0ce751aed8..7023798f2b4 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -376,7 +376,7 @@ async def test_async_config_entry_first_refresh_success(crd, caplog): async def test_not_schedule_refresh_if_system_option_disable_polling(hass): """Test we do not schedule a refresh if disable polling in config entry.""" - entry = MockConfigEntry(system_options={"disable_polling": True}) + entry = MockConfigEntry(pref_disable_polling=True) config_entries.current_entry.set(entry) crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) crd.async_add_listener(lambda: None) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index e9e864c4491..556f06fce54 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -574,6 +574,13 @@ async def test_saving_and_loading(hass): ) assert len(hass.config_entries.async_entries()) == 2 + entry_1 = hass.config_entries.async_entries()[0] + + hass.config_entries.async_update_entry( + entry_1, + pref_disable_polling=True, + pref_disable_new_entities=True, + ) # To trigger the call_later async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) @@ -596,6 +603,8 @@ async def test_saving_and_loading(hass): assert orig.data == loaded.data assert orig.source == loaded.source assert orig.unique_id == loaded.unique_id + assert orig.pref_disable_new_entities == loaded.pref_disable_new_entities + assert orig.pref_disable_polling == loaded.pref_disable_polling async def test_forward_entry_sets_up_component(hass): @@ -813,14 +822,19 @@ async def test_updating_entry_system_options(manager): domain="test", data={"first": True}, state=config_entries.ConfigEntryState.SETUP_ERROR, - system_options={"disable_new_entities": True}, + pref_disable_new_entities=True, ) entry.add_to_manager(manager) - assert entry.system_options.disable_new_entities + assert entry.pref_disable_new_entities is True + assert entry.pref_disable_polling is False - entry.system_options.update(disable_new_entities=False) - assert not entry.system_options.disable_new_entities + manager.async_update_entry( + entry, pref_disable_new_entities=False, pref_disable_polling=True + ) + + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is True async def test_update_entry_options_and_trigger_listener(hass, manager): @@ -2557,48 +2571,18 @@ async def test_updating_entry_with_and_without_changes(manager): entry.add_to_manager(manager) assert manager.async_update_entry(entry) is False - assert manager.async_update_entry(entry, data={"second": True}) is True - assert manager.async_update_entry(entry, data={"second": True}) is False - assert ( - manager.async_update_entry(entry, data={"second": True, "third": 456}) is True - ) - assert ( - manager.async_update_entry(entry, data={"second": True, "third": 456}) is False - ) - assert manager.async_update_entry(entry, options={"second": True}) is True - assert manager.async_update_entry(entry, options={"second": True}) is False - assert ( - manager.async_update_entry(entry, options={"second": True, "third": "123"}) - is True - ) - assert ( - manager.async_update_entry(entry, options={"second": True, "third": "123"}) - is False - ) - assert ( - manager.async_update_entry(entry, system_options={"disable_new_entities": True}) - is True - ) - assert ( - manager.async_update_entry(entry, system_options={"disable_new_entities": True}) - is False - ) - assert ( - manager.async_update_entry( - entry, system_options={"disable_new_entities": False} - ) - is True - ) - assert ( - manager.async_update_entry( - entry, system_options={"disable_new_entities": False} - ) - is False - ) - assert manager.async_update_entry(entry, title="thetitle") is False - assert manager.async_update_entry(entry, title="newtitle") is True - assert manager.async_update_entry(entry, unique_id="abc123") is False - assert manager.async_update_entry(entry, unique_id="abc1234") is True + + for change in ( + {"data": {"second": True, "third": 456}}, + {"data": {"second": True}}, + {"options": {"hello": True}}, + {"pref_disable_new_entities": True}, + {"pref_disable_polling": True}, + {"title": "sometitle"}, + {"unique_id": "abcd1234"}, + ): + assert manager.async_update_entry(entry, **change) is True + assert manager.async_update_entry(entry, **change) is False async def test_entry_reload_calls_on_unload_listeners(hass, manager): @@ -2863,3 +2847,35 @@ async def test__async_abort_entries_match(hass, manager, matchers, reason): assert result["type"] == "abort" assert result["reason"] == reason + + +async def test_loading_old_data(hass, hass_storage): + """Test automatically migrating old data.""" + hass_storage[config_entries.STORAGE_KEY] = { + "version": 1, + "data": { + "entries": [ + { + "version": 5, + "domain": "my_domain", + "entry_id": "mock-id", + "data": {"my": "data"}, + "source": "user", + "title": "Mock title", + "system_options": {"disable_new_entities": True}, + } + ] + }, + } + manager = config_entries.ConfigEntries(hass, {}) + await manager.async_initialize() + + entries = manager.async_entries() + assert len(entries) == 1 + entry = entries[0] + assert entry.version == 5 + assert entry.domain == "my_domain" + assert entry.entry_id == "mock-id" + assert entry.title == "Mock title" + assert entry.data == {"my": "data"} + assert entry.pref_disable_new_entities is True From fc24b34408ff332a546ec6ec2c56d8d60cfbf152 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 1 Jun 2021 15:28:56 -0500 Subject: [PATCH 845/852] Handle incomplete Sonos alarm event payloads (#51353) --- homeassistant/components/sonos/speaker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index acd53e1f877..957851dfbee 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -372,7 +372,8 @@ class SonosSpeaker: @callback def async_dispatch_alarms(self, event: SonosEvent) -> None: """Create a task to update alarms from an event.""" - update_id = event.variables["alarm_list_version"] + if not (update_id := event.variables.get("alarm_list_version")): + return if update_id in self.processed_alarm_events: return self.processed_alarm_events.append(update_id) From e4e3d5f81480d74d9a24312fc8cbfa828ec016fa Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Jun 2021 22:35:13 +0200 Subject: [PATCH 846/852] Update frontend to 20210601.1 (#51354) --- 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 0e0685cd772..42f29f36976 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==20210601.0" + "home-assistant-frontend==20210601.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4aa67a73e14..6dc7bedaab8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210601.0 +home-assistant-frontend==20210601.1 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index f33792126ad..a057d319362 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210601.0 +home-assistant-frontend==20210601.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfffb2c5dfc..8ad75872263 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210601.0 +home-assistant-frontend==20210601.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From dc17d664eb6204846f0259935d0dc58c3c39c27b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Jun 2021 13:39:48 -0700 Subject: [PATCH 847/852] Bumped version to 2021.6.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c1364fc5596..8b6e95e005a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From f8d68e47b8aa4b1f0b829035e0c1f5195d5a0385 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Jun 2021 10:00:24 +0200 Subject: [PATCH 848/852] Do not attempt to unload non loaded config entries (#51356) --- homeassistant/config_entries.py | 3 +++ tests/components/smartthings/test_binary_sensor.py | 2 ++ tests/components/smartthings/test_cover.py | 2 ++ tests/components/smartthings/test_fan.py | 2 ++ tests/components/smartthings/test_light.py | 2 ++ tests/components/smartthings/test_lock.py | 2 ++ tests/components/smartthings/test_scene.py | 2 ++ tests/components/smartthings/test_sensor.py | 2 ++ tests/components/smartthings/test_switch.py | 2 ++ 9 files changed, 19 insertions(+) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index eeaf0149cc2..49892937217 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -392,6 +392,9 @@ class ConfigEntry: self.reason = None return True + if self.state == ConfigEntryState.NOT_LOADED: + return True + if integration is None: try: integration = await loader.async_get_integration(hass, self.domain) diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 7e8ab7d2c9b..f3d548c1e39 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.smartthings import binary_sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -91,6 +92,7 @@ async def test_unload_config_entry(hass, device_factory): "Motion Sensor 1", [Capability.motion_sensor], {Attribute.motion: "inactive"} ) config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "binary_sensor") # Assert diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 44c2b2f9285..aad7a4b037e 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -19,6 +19,7 @@ from homeassistant.components.cover import ( STATE_OPENING, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -191,6 +192,7 @@ async def test_unload_config_entry(hass, device_factory): "Garage", [Capability.garage_door_control], {Attribute.door: "open"} ) config_entry = await setup_platform(hass, COVER_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN) # Assert diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 6cdfa5b8917..2a66fc646c7 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -17,6 +17,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -186,6 +187,7 @@ async def test_unload_config_entry(hass, device_factory): status={Attribute.switch: "off", Attribute.fan_speed: 0}, ) config_entry = await setup_platform(hass, FAN_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "fan") # Assert diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index c9dbb094161..81062adf934 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -19,6 +19,7 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -306,6 +307,7 @@ async def test_unload_config_entry(hass, device_factory): }, ) config_entry = await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "light") # Assert diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 1168108656e..86c8d534a71 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -9,6 +9,7 @@ from pysmartthings.device import Status from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -103,6 +104,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Lock_1", [Capability.lock], {Attribute.lock: "locked"}) config_entry = await setup_platform(hass, LOCK_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "lock") # Assert diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 647389eeb42..288fae046f5 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -5,6 +5,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er @@ -44,6 +45,7 @@ async def test_unload_config_entry(hass, scene): """Test the scene is removed when the config entry is unloaded.""" # Arrange config_entry = await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, SCENE_DOMAIN) # Assert diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 0f148b8931f..4af88e27fe4 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -9,6 +9,7 @@ from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN from homeassistant.components.smartthings import sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, @@ -116,6 +117,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Sensor 1", [Capability.battery], {Attribute.battery: 100}) config_entry = await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") # Assert diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 21d508bcbc2..7c202fad12e 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -12,6 +12,7 @@ from homeassistant.components.switch import ( ATTR_TODAY_ENERGY_KWH, DOMAIN as SWITCH_DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -95,6 +96,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Switch 1", [Capability.switch], {Attribute.switch: "on"}) config_entry = await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "switch") # Assert From b7153fe25fc6f8f3ced0b50c4fa8872c3faa4e44 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 1 Jun 2021 21:35:12 -0600 Subject: [PATCH 849/852] Bump pyiqvia to 1.0.0 (#51357) --- homeassistant/components/iqvia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 779e62de4fb..75249ded6a1 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.20.3", "pyiqvia==0.3.1"], + "requirements": ["numpy==1.20.3", "pyiqvia==1.0.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a057d319362..759e4580a92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1482,7 +1482,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.3.1 +pyiqvia==1.0.0 # homeassistant.components.irish_rail_transport pyirishrail==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ad75872263..8b4244b4517 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -823,7 +823,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.3.1 +pyiqvia==1.0.0 # homeassistant.components.isy994 pyisy==3.0.0 From 089374b7e27cf38a398338367a97fcb8a24b6ab9 Mon Sep 17 00:00:00 2001 From: gadgetmobile <57815233+gadgetmobile@users.noreply.github.com> Date: Wed, 2 Jun 2021 14:02:37 +0200 Subject: [PATCH 850/852] Fix BleBox wLightBoxS and gateBox support (#51367) Co-authored-by: bbx-jp <83213200+bbx-jp@users.noreply.github.com> --- CODEOWNERS | 2 +- homeassistant/components/blebox/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index faa623456f1..2bee90dcf99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -64,7 +64,7 @@ homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria -homeassistant/components/blebox/* @gadgetmobile +homeassistant/components/blebox/* @bbx-a @bbx-jp homeassistant/components/blink/* @fronzbot homeassistant/components/blueprint/* @home-assistant/core homeassistant/components/bmp280/* @belidzs diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 00b4b61c507..39c0d37e2e3 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,7 +3,7 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==1.3.2"], - "codeowners": ["@gadgetmobile"], + "requirements": ["blebox_uniapi==1.3.3"], + "codeowners": ["@bbx-a", "@bbx-jp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 759e4580a92..5e976904b36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -358,7 +358,7 @@ bimmer_connected==0.7.15 bizkaibus==0.1.1 # homeassistant.components.blebox -blebox_uniapi==1.3.2 +blebox_uniapi==1.3.3 # homeassistant.components.blink blinkpy==0.17.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b4244b4517..0050f48917f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ bellows==0.24.0 bimmer_connected==0.7.15 # homeassistant.components.blebox -blebox_uniapi==1.3.2 +blebox_uniapi==1.3.3 # homeassistant.components.blink blinkpy==0.17.0 From 7938f69dc5f9e0a90918825cb90caca75ee4fba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 2 Jun 2021 17:16:04 +0200 Subject: [PATCH 851/852] Fix Tibber timestamps parsing (#51368) --- homeassistant/components/tibber/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 70dfa54c70a..f2ff23dfe5d 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -385,7 +385,7 @@ class TibberRtDataHandler: if live_measurement is None: return - timestamp = datetime.fromisoformat(live_measurement.pop("timestamp")) + timestamp = dt_util.parse_datetime(live_measurement.pop("timestamp")) new_entities = [] for sensor_type, state in live_measurement.items(): if state is None or sensor_type not in RT_SENSOR_MAP: From e3994e8029cc51faceac61b14fae409150a33405 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Jun 2021 17:29:50 +0200 Subject: [PATCH 852/852] Bumped version to 2021.6.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8b6e95e005a..229646d74d1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)